webpackはサンドイッチ製造機名乗ってるのに具材の加工しかしてくれない
これは 東京理科大学 Advent Calendar 2016 の7日目の記事であり, 煽り記事である.
東京理科大学 Advent Calendar 2016 - Qiita
なお, 私は東京理科大学の学生 ではない のだが, 縁があって『神楽坂一丁目通信局』というサークルに邪魔している. 『神楽坂一丁目通信局』には, ここで紹介するような技術を取り扱っている人間がいる (はずだ) .
tl; dr
- webpackはWebのパッケーングツールであるはずなのにJavaScript専門である.
- 公式で提供されている
extract-text-webpack-plugin
を用いればHTMLやCSSなどの他のテキストファイルも扱えるようになる. - loaderやpluginによりさらなる拡張が可能. JavaScriptの出力をなくしてしまうことさえ.
- webpackの設計は改善できる
webpackとは
さて, そもそもwebpackとは何だったか. 公式には次のようにある.
webpackはモジュールバンドラーです. その主な目的はブラウザーで使用するためにJavaScriptのファイルをバンドルすることですが, 他のリソースやデータを処理, バンドル, パッケージングするといっただけの目的にも使えます.
webpack/README.md at 5d4e1acd39b9f654c00b3e66f002bb279e65f8a3 · webpack/webpack
つまりwebpackはJavaScriptのバンドラーで, それに付随する形で他のファイルも扱えるということだ. おかしい. Webというのは, そもそもHTMLが主で, それを修飾するためにCSSやJavaScript, 各種画像が用いられたのではなかったのか. この記事では, webpackをwebpackたらしめるための改善を考える.
webpackの仕組み
まず, webpackがどのように動作するか説明しよう. まずwebpackは__entry point__というJavaScriptを読み込み,
それを loader が処理してCommonJS形式のモジュールを文字列で出力する (例えば"module.exports = \"foo\""
).
最後に, モジュールをwebpackのJavaScriptテンプレートにバンドルし, ファイル (asset)
として出力する.
これで分かるように, webpackはJavaScriptを扱うことしか考えられていない. jspackと改名すべきではないか.
file-loader
しかし, 読者の方はこう思うかもしれない. 現実にはwebpackはJavaScript以外のものも扱う.
file-loader
があるではないか.
file-loader
はその名の通りファイルのloaderであり, webpackが公式で提供している.
しかし, loaderといってもファイルをそのままモジュールにするのではなく,
assetとしてファイルを出力し, そのファイルのパスをエクスポートするモジュールを作成するというのがfile-loader
の本来の役割だ.
この点, ファイルをJavaScriptと同等に扱っているとは言えない.
extract-text-webpack-plugin
loaderがだめなら plugin はどうだろう? pluginはwebpackの動作のより広範囲に関われる.
その発想から生まれたのがwebpackが公式で公開しているextract-text-webpack-plugin
である.
実のところ, これはloaderとpluginの両方を組み合わせたものであるのだが,
最後のモジュールをバンドルする過程に入る前に, (あくまでもエクスポートされた)
文字列をファイルとして出力し, モジュールを削除するpluginとして動作している. 例えば,
"module.exports = \"foo\""
というモジュールは, foo
と書かれたファイルになる.
これにより, webpackを用いてJavaScript以外のテキストファイルを扱うことが可能になる.
公式で公開されているhtml-loader
やcss-loader
を用いれば, すぐにHTMLやCSSを扱うことが可能になる.
やったね!
html-loader
の問題点
当然, html-loader
を使って次のようなHTMLファイルを処理したくなるはずだ.
<!DOCTYPE html>
<html>
<body>
<script src="./index.js"></script>
</body>
</html>
これで, ./index.js
をentry pointとしてassetを作成し, そのパスがsrc
属性に代入することを期待する.
しかし, これはそう簡単ではない. 標準では, 実装されてない.
JavaScriptのloaderを書く
では, それを実現する JavaScriptのloader を書いてみよう. webpackのバンドル機能を用いて JavaScriptを生成し, ファイルとして出力, そのパスをモジュールにして返すloaderである. 必要な知識は以下の通り.
loaderがアクセスできる_compilation
オブジェクトが, 内部への様々なインターフェイスを提供する.
アンダースコアで始まってるし読んじゃいけないのではと思うかもしれないが,
ここで使うインターフェイスはおおよそextract-text-webpack-plugin
でも使われているものなので,
それがある限り破壊的変更などは起こり得ないはずである.
_compilation
オブジェクトはcreateChildCompiler
という関数を持つ. これにより,
webpackのバンドル機能を活用できる. compilerは何度も作成できないので,
一度作成したらキャッシュしておく. 一度作成したcompilerは繰り返し使えるようである.
標準で提供されているSingleEntryPlugin
により, entry pointをcompilerに登録できる.
依存関係とエラー, そしてassetを伝搬させる必要がある. でないと, watchできなかったり, 謎の不具合が起きたり, 必要なファイルが出力されなかったりする.
最終的に30行程度になる. 容易に維持できる大きさだ.
応用
先の説明では, 生成したJavaScriptをファイルとして出力したが, インラインスクリプトとしてJavaScriptをHTMLに埋め込む応用例も考えられる. 例えば, HTMLには次のように記述する.
<script>${require("./loader/js.js!./index.js")}</script>
${}
はES2015の文字列テンプレートの書式で, html-loader
はこれを理解できるHTMLテンプレートエンジンでもある.
この記述により, module.exports
の値がscript
タグ内に展開されるはずである.
先の説明でファイルパスを出力していたところ, entry pointのassetからJavaScriptのソースを抜き取って, assetを伝搬しないようにすれば, この用途に使えるloaderを作成できる.
この方法を用いるときに注意すべきなのが, これはHTTPパイプラインや複数コネクションによる並列化を阻害するということである. HTMLからは様々なファイルに対する参照が含まれているが, これはHTMLファイルがダウンロードされ, パースされるまで分からない. JavaScriptをインライン化するとこれを遅延する. これにより, JavaScriptをダウンロードしている間の並列化が阻害される.
一方で, インライン化することによる利点もある. リクエストの数が減少するため, それによるオーバーヘッドが低減される. また, JavaScriptを問答無用でダウンロードさせるため, JavaScriptのロード完了までの時間は減少するだろう. これはHTTPパイプラインと同等の利点である (もっとも, パイプライン化されるのはHTMLとインライン化されたもののみであるが). HTML内の他のリソースへの参照が少ない, JavaScriptからXMLHttpRequestやFetch APIで多数のリソースへ参照するといった場合には, インライン化は有益だろう.
もっとも, それによる変化はミリ秒程度だと思うが.
CSSの場合, 標準のcss-loader
の出力が文字列のモジュールなので, それをそのままインライン化できる.
JavaScriptのassetを削除する
もはやentry pointに対応して作られるJavaScriptのassetは要らない. 出力する前にpluginで削除してしまえ. そのようなpluginは10行で書ける. もうこうなるとモジュールバンドラーとは何なのかよく分からなくなってくる.
さらなる改善
以上で, webpackが真のwebpackたる存在になった. しかし, どう考えてもこのやり方は汚い.
webpackのモジュールはJavaScriptのモジュールの文字列である点
実際にそうなってるのはJavaScriptを最初にロードしたときだけだ. それ以外の場合,
JSON.stringify
を使ってJavaScriptのオブジェクトを文字列に変換するなどという汚いやり方が使われている.
JavaScriptのモジュールである必要は, 本質的にない.
webpackのモジュールは出力する形式を考慮できない点
例えば, JavaScript内からJavaScriptをrequire
するとき, ファイルパスで参照するとき,
インラインするときなど, 様々な場合が考えられる. webpackはこれに対応できない.
最終的な出力がwebpack内に組み込まれてしまっている点
webpackがモジュールを予め用意されたテンプレートにバンドルし, JavaScriptとして出力する部分はwebpack内に組み込まれてしまっており,
これをユーザーが使ったり, また置き換えたりするのは困難である.
それゆえに_compilation
からcreateChildCompiler
にアクセスする必要が出たり,
extract-text-webpack-plugin
でloaderとpluginの両方のインターフェイスを用いて汚いハックをしなければならなくなったりする.
webpack開発者はextract-text-webpack-plugin
を開発したときに設計が現実に合わなくなってることに気づくべきだった.
まあ, こういろいろと文句を言ったが, 現時点ではこれが最善であり, また, 現在開発されている次期webpackも最善であり続けるであろう. 現状を踏まえて, 適切に活用したい.