Webpack でルーターのような動作をさせるパターン

昔のプロジェクトでページ描画に使われている依存ファイルについては、あるディレクトリ以下のどこかに実際の対象ファイルがあり、それを探して選び出してくるという関係になっていました。つまり、 require() などに直接変換すれば良いというものではありませんでした。
closure-loader がそんな感じではあります。

今回は、ルーターのような働きをする関数を設けて依存性解決を行います。入れ子だったり流入口が複数ある場合に役立つのかもしれません。

サンプルプロジェクトはこちら
この中では、独自ダグの単純な置き換えだけをやっています。(属性やタグ内の内容はまだ渡せません。)

やること

こんな感じの HTML ファイルを用意して、独自定義タグ <x-note><x-paragraph> の内容を挿入・入替します。
(ややこしいですが、 Custom Elements として定義はしていないです。)

index.html
1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html>
<body>
<x-note></x-note>
<p>Note will be inserted above.<br/>Paragraph will be inserted below.</p>
<x-paragraph></x-paragraph>
</body>
</html>

これらの独自定義タグは、今回は引数のないようなコンポーネントになっています。

x-note.html
1
2
3
<div>
<p>This is note.</p>
</div>
x-paragraph.html
1
2
3
<div>
<p>Hello World</p>
</div>

config

これを利用する config は以下のようになります。今回は入れ子構造は考慮していないことと、 “extract-text-webpack-plugin” を利用しているため、実際のケースではもう少し変わると思います。

webpack.config.dev.js
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
test: /src.pages.[\w-]+\.html$/,
use: ExtractTextPlugin.extract({
use: [
{
loader: "./dev/loader.js",
options: {
prefixes: ['x-'],
paths: [path.resolve(__dirname, './src/components')],
fileExt: '.html'
}
}
]
})
},
{
test: /src.components.[\w-]+\.html$/,
use: [
{
loader: 'html-loader'
}
]
}

対象となる接頭辞と探索するパスの一覧、拡張子を設定し、これにマッチするものがあれば require をして依存関係を解決するようにしてあげます。

loader

そのために、 loader から引数を与えるとファイルの場所を探し出して返すような関数を定義して、これを require するようなコードとして埋め込むようにすれば OK です。

loader.js
22
23
24
const passingOptions = {'name': componentName, ...options};

const request = componentRouter(context, passingOptions);
component-router.js
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module.exports = function (loaderContext, options) {
const roots = Array.isArray(options.paths) ? options.paths : [options.paths];
let filePath = null;
for (const root of roots) {
const files = glob.sync(path.join(root, '**', options.name + options.fileExt));
if (files.length === 1) {
filePath = files[0];
break;
} else if (files.length > 1) {
loaderContext.callback(`Component "${options.name}" duplicated`);
return "";
}
}
if (filePath === null) {
loaderContext.callback(`Component "${options.name}" not found`);
return "";
}
return loaderUtils.stringifyRequest(loaderContext, require.resolve(filePath));
};

単純に少数のファイルとして定義されている場合ではこのままで大丈夫だと思います。
アーカイブ済みファイルの中にあったり探索対象のファイルが多かったり、はたまた別の言語で記述されていたりする場合もあるかもしれませんが、 JavaScript の Object を用いたキャッシュを行えば、パフォーマンスの悪さは軽減できるかもしれません。

まとめ

require 対象ファイルのパスが明示されず、その都度 探索しなければならない場合の例を紹介しました。

この例では、まだ属性やタグに囲まれた内容を利用できませんので、これを改善する必要があります。