はじめに
Node.js向けに書かれたJavaScriptのプログラムをブラウザ上で動作させるには require() によるモジュールの参照を解決した単一のJavaScriptファイルに変換する必要があります1。
少し前まではこの作業には Browserify が使われることが多かったようですが、現在は「画像もCSSも何でも1つにします」が売りの webpack 一色になっている感じです。ですが、この webpack、とってもわかりにくいのです。設定ファイル webpack.config.js を見たときに私は TeX や sendmail の悪夢を思い出しました2。この黒魔術3を読み解いてみましょう。
webpack の仕事
画像とかCSSとかのことはおいておくと、webpackの仕事は以下の3つになります。
-
複数の
.jsファイルの依存関係を解決し、1つにバンドルする
これが本来業務です。 -
.jsファイルを読み込むときに変換する
ES6で書かれたスクリプトを Babel でES5に変換したりする部分です。他のパッケージに丸投げですね。 -
圧縮したり、ソースマップを出力したりする
追加の仕事ですが、これもサポートしないとと使ってもらえないですからね。
webpack への仕事の指示は2つの方法で与えることができます。
-
コマンドラインオプション
コマンドを起動するたびに変えられるので、やったりやらなかったりする仕事を指示するのに向いています。 -
設定ファイル webpack.config.js
必ず行うルーチンワークを指示します。細かい指示が可能です。
webpack の分かりづらさは、設定ファイル webpack.config.js で使われる語彙の独特さに原因があると思います。それぞれの仕事の設定方法を見ていきましょう。
バンドル処理
webpack は以下の3つの単位でファイルをまとめることができます。
- アプリケーション全体で1つのファイルにまとめる
- ページごとに1つのファイルにまとめる
- 互いに依存しない複数のファイルを1つにまとめる
webpack.config.js では entry と output で指示します。コマンドラインでも指定できるようですが、毎回同じ指定になるのでwebpack.config.jsに書くべきでしょう。
1. アプリケーション全体で1つのファイルにまとめる
webpack が元々想定していたと思われるまとめ方です。入力にはそのページの「メイン」となる.jsファイルを指定します。webpack はそこから require を芋づる式にたどってファイルをまとめます。index.jsからたどってbundle.jsにまとめる場合、以下の設定になります。
entry: "index.js",
output: { path: __dirname, filename: "bundle.js" }
input/output ではなく、entry/output です。これはいいとしても、なぜ output だけ絶対パスなのか、なぜ output は path と filename に分かれているのか、謎が多いです。
2. ページごとに1つのファイルにまとめる
しかし、普通のWebアプリには複数のページがあり、そのそれぞれに「メイン」があるのではないでしょうか。ページごとのメイン page1.js、page2.js、page3.js を page1.bundle.js、page2.bundle.js、page3.bundle.jsにまとめる場合、以下の設定になります。
entry: {
page1: "page1.js",
page2: "page2.js",
page3: "page3.js",
},
output: { path: __dirname, filename: "[name].bundle.js" }
3. 互いに依存しない複数のファイルを1つにまとめる
まれに互いに依存しない複数のファイルをまとめることが必要になる場合があります4。
file1.js、file2.js、file3.jsを単一のbundle.jsにまとめる場合、以下の設定になります。
entry: [ "file1.js", "file2.js", "file3.js" ],
output: { path: __dirname, filename: "bundle.js" }
変換処理
各々の入力ファイルを読出しバンドル処理に渡す前に変換を行います。適切にJavaScriptに変換できるのであれば、CSSや画像ファイルもソースに指定できます。この処理を行うモジュールのことをwebpackではloaderと呼んでいます。名前がわかりにくいですね。
.jsを Babel におまかせで変換する場合、Babelの提供するwebpack向けのインタフェースbabel-loaderを以下のように設定します。
module: {
rules: [
{ test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader' }
]
},
loaderなのになぜかmoduleという項目に設定するところがさらにわかりにくいです。loaderはUnixのパイプのように複数つなげて適用することができるので、どのファイルにどのloader 群を設定するのかという設定方法になります。これを rules に1つ1つ設定します。
ファイルの特定方法にはいろいろあってまた複雑なのですが、ここでは test と exclude を使っています。test はloaderを適用すべきファイルの正規表現ですが、名前から全く想像できません。ひどい命名センスですね。
loader は実際に前処理を行うJavaScriptのモジュール名です。おそらくはrequireでロードしますのでnpmのモジュール名と一致させればいいです。前述したようにloaderは複数適用できます。その場合はloaderの箇所をuseにして配列で指定するようです。しかし use って何だ?いい加減にしてくれ!5
出力処理
おそらくはバンドル処理が終わって1つになったファイルに対して処理します。圧縮処理はpluginが担当し、ソースマップ出力はdevtoolが担当するようです。2つの概念の違いが判らないですが、判らなくてもたぶん大丈夫です。
出力処理はデバッグ時とリリース時で違うと思うので、コマンドラインから指定した方がいいです。package.json の scripts に以下のように設定します。
"scripts": {
"build": "webpack --devtool inline-source-map",
"release": "webpack -p"
}
npm run build でデバッグ用のソースマップつきの出力が得られます。npm run release だとソースマップなしで圧縮します。コマンドラインからだと簡単ですね!
細かい話をすると、plugin とは特定の出力機能を担当する webpack のオブジェクトのようですが、-p で webpack おすすめの圧縮方法が使われるのでこちらの設定方法の方が簡単です6。devtool は処理をスイッチさせるための単なる名称のようです。機能の指定方法が、モジュール名(loader)だったり、インスタンスの参照(plugin)だったり、スイッチの名称(devtool)だったり不統一にもほどがあります!
おわりに
とりあえず最低限の仕事をするためのwebpack.config.jsを解読しました。webpackのサイトにはちゃんとマニュアルもあるので読んでみるとよいでしょう。ですが黒魔術なので分かりにくいですよ。Node界に登場したバッドノウハウですね!
-
大域変数を使えば1つにする必要はないのだが、Node.jsが流行る以前から
.jsファイルを1つにまとめて圧縮することが流行っていたのでこの流れと思われる ↩ -
そもそもこのファイル、拡張子が
.jsであることから分かるように「プログラム」です。webpackがプログラムの一部としてロードして使用します ↩ -
Browserifyの方がサイトデザインは魔術っぽいのにね ↩
-
babel-polyfillを使う場合などがこれにあたります ↩
-
私が設計するなら
input_filter: [ { match: /\.js$/, exclue: /node_modules/, modules: [ 'babel-loader' ] } ]と設定するようにしますけどね。 ↩ -
webpack.conf.js内でpuluginを指定するときはその場でnewを使ってインスタンスを生成していました。もう本当に黒魔術です。 ↩