フロントまわりのツールは群雄割拠してる感がすごくて手を出せずにいたのですが、Rails5.1 で gem webpacker が導入されたこともあってwebpack/yarn構成に魅力を感じています。
今どきのモジュール/パッケージ管理したいので、まずはwebpackです。
概要
-
webpack = JavaScriptファイルのバンドラ
- モジュール単位に分離されたjsファイルをひとつのjsファイルにまとめる(バンドルする)
- 結合の過程で、jsファイルの依存関係を解決する
- CommonJS, AMD, ES6 Moduleなど復数のモジュールシステムに対応する
-
競合はrequire.js(依存解決), browserify(バンドル)など
-
オープンソース, MITライセンス
導入
npmでインストールする。
プロダクション環境ではバンドルした状態で配置するわけなので、開発環境のみを指定することになる(--save-dev
)。
$ npm --save-dev install webpack
公式によると、グローバルにインストールするのは推奨しないとのこと。
ローカルにインストールした場合、webpackコマンドはnode_modules/.bin/
以下に格納される。
$ ./node_modules/.bin/webpack -v
#=> 2.6.0
基本
ディレクトリを構成する
webpackはディレクトリを構成を制限しない。
今回はwebpackのテスト用として以下のような構成にしておく:
/
src/ # jsファイル(モジュール)を保持
entry.js # エントリポイント(最初に読まれるjs)
ModuleA.js # モジュールA(entry.jsが使う)
ModuleB.js # モジュールB(モジュールAが使う)
dist/ # バンドルの出力先
bundle.js # バンドルされたjsファイル(成果物)
webpack.config.js # webpack設定ファイル
index.html # デモ表示用の画面
また出力先のディレクトリ(今回はdist/
)はバージョン管理に含める必要がないので、.gitignore
等にて除外設定しておく。
設定ファイルを書く
設定を定義する。
現時点では、以下を記述している:
- エントリポイントは
entry.js
である - 出力先は、
dist/bundle.js
である
// 設定ファイルはwebpack = node.js上で実行されるので、
// require()のようなnode(CommonJS)のメソッドが使える。
var path = require('path');
module.exports = {
entry : './src/entry.js',
output : {
filename : 'bundle.js',
path : path.resolve(__dirname, 'dist')
}
};
エントリポイントとモジュールを実装する
モジュールA/Bに加えて、エントリポイント>モジュールA>モジュールB の順の依存関係を定義する。
webpackではモジュールは復数のスタイルで実装できる(CommonJS, AMD, ES6...)が今回はCommonJSスタイルで書いてみる。
// モジュールAに依存することを定義
var A = require('./ModuleA');
// モジュールを呼べるか試す
A.echo();
console.error('entry.js echo');
// モジュールBに依存することを定義
var B = require('./ModuleB');
// モジュールAを定義
var ModuleA = function ModuleA(){
}
ModuleA.echo = function(){
// モジュールを呼べるか試す
B.echo();
console.error('Module A echo');
}
// モジュールAを登録
module.exports = ModuleA;
// モジュールBを定義
var ModuleB = function ModuleB(){
}
ModuleB.echo = function(){
console.error('Module B echo');
}
// モジュールBを登録
module.exports = ModuleB;
バンドルを作る
webpack
コマンドによってバンドルを作成する
$./node_modules/.bin/webpack --config webpack.config.js
#=> [0] ./src/ModuleA.js 176 bytes {0} [built]
#=> [1] ./src/ModuleB.js 132 bytes {0} [built]
#=> [2] ./src/entry.js 102 bytes {0} [built]
dist/bundle.js
が作成される。
バンドルをHTMLに読み込ませる
最後に、作成したバンドルをHTMLから読み込む。
<html>
<body>
<script src="dist/bundle.js"></script>
</body>
</html>
コンソールに以下のように表示され、モジュールが指定した順番で読めたことが確認できた。
> Module B echo
> Module A echo
> entry.js echo
設定例
設定ファイルは普通のjsファイルなので、制御構文は制限なく使える。
またES6やJSX, TypeScriptで記述することもできる(参考)。
復数エントリポイントから復数バンドルを作る
module.exports = {
entry : {
// ページごとに異なるエントリポイントを設ける
pageA : './src/entryA.js',
pageB : './src/entryB.js',
pageC : './src/entryC.js',
},
output : {
// エントリ名によって動的にバンドルファイルを命名するよう設定する
// 結果としてentryA.bundle.js, entryB.bundle.js, entryC.bundle.js ができる
filename : '[name].bundle.js',
// ...
}
// ...
};
出力名には[name]
のほか、[chunkhash]
(ハッシュ値)も使える。
トランスパイルする
ローダ(loaders)を設定することで、トランスパイルを含むデータの変換ができる。
ローダはwebpackとは別のnpmパッケージとして提供されているので、まずは必要なものを入れる。
$ npm --save-dev install css-loader # CSS
$ npm --save-dev install coffee-loader # CoffeeScript
$ npm --save-dev install ts-loader # TypeScript
$ npm --save-dev install babel-loader babel-preset-es2015 babel-preset-react # React(Babel/ES6/JSX)
次に、どのファイルにどのローダを適用するのか設定する。
module.exports = {
module : {
rules : [
{ test : /\.css$/, use: 'css-loader' },
],
},
};
React用のES6/JSXについて
webpackはES6のimport/export文__のみ__をサポートしている。
他のES6構文については対応外なので、使いたい場合は上記のbabel-loaderなどを使ってトランスパイルする必要がある、とのこと。
rules: [
{
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: {
presets: [ 'es2015', 'react' ],
},
}],
},
],
css-loaderについて
css-loaderは、CSSファイルをモジュールとしてロードする。
ロードされたCSSはjs内ではオブジェクトに格納された単なる文字列として扱われる様子。
(使い所がいまいちよくわからなかった)
開発手法
バンドルを自動更新する
開発中にコードを書く→webpackを実行してバンドル を繰り返すのは手間なので、ソースファイルを監視して上書きされる都度リコンパイルする機能がある。
なおエディタの書き込み設定によっては正しく動作しない場合があるので、設定を確認すべしとのこと。
(vimの場合はset backupcopy=yes
を設定)
watchオプションを使う
webpackコマンドのwatch
オプションにより、ファイルの変更を監視できる。
オプションを指定してコマンドを実行するとwebpackが常駐し、ソースの変更を監視する。
# 監視モードで起動
# 起動時に一度バンドルが作られる
$./node_modules/.bin/webpack --progress --watch --config webpack.cofig.js
> 0% compiling
> Webpack is watching the files…
>
> Hash: 50e4d8f603a5f848f559
> Version: webpack 2.6.0
> Time: 92ms
> Asset Size Chunks Chunk Names
> bundle.js 3.17 kB 0 [emitted] main
> [0] ./src/ModuleA.js 174 bytes {0} [built]
> [1] ./src/ModuleB.js 129 bytes {0} [built]
> [2] ./src/entry.js 73 bytes {0} [built]
# entry.jsを更新してみる
# 更新が検出され、自動的にリコンパイルがかかる
>Hash: 984319926ae2e2cd15df
> Version: webpack 2.6.0
> Time: 45ms
> Asset Size Chunks Chunk Names
> bundle.js 3.17 kB 0 [emitted] main
> [2] ./src/entry.js 77 bytes {0} [built]
> + 2 hidden modules
watchオプションは監視と自動更新に限定した機能なので、開発用のhttpサーバは別途容易する必要がある。
webpack-dev-serverを使う
webpack-dev-server
パッケージはwatch
オプションの機能に加えて、デバッグ環境/httpサーバを提供する。
npmパッケージとしてインストールする。
$ npm --save-dev install webpack-dev-server
インストール後、コマンドでサーバを起動する。
$ ./node_modules/.bin/webpack-dev-server --config webpack.config.js
http://localhost:8080/*.html
でその内容を確認できる。
(デフォルトでは/*.html
が表示される)。
またwatch
と同じく自動更新が有効になるが、
dev-serverはバンドルを__ファイルではなくメモリ上に置く__。
サーバを起動中にソースを更新してもバンドルファイルは更新されないので、テスト用のHTMLはメモリ上のバンドルを参照する必要がある。
メモリ上のバンドルはデフォルトでは/*.js
で参照できる。
<script src="/bundle.js"></script>
オプションのdevServer
ディレクティブで挙動を設定できる。
var path = require('path');
module.exports = {
// entry, outputなど...
devServer : {
port : 8080, // ポートを指定
progress : true, // 変換の進捗をコンソールに表示
inline : true, // インライン/iframeモードの指定(通常インラインでいい)
clientLogLevel : 'info', // バンドル作成に関するログのレベル(none, error, warning, info)
contentBase : path.join(__dirname, '/'), // サーバの基準パス(ドキュメントルート)
publicPath : '/', // オンメモリのバンドルファイルの仮想的なパス
hot : true, // HMRの利用
watchOptions : {
poll : true // ファイルの更新が正しく検知されない場合に利用
},
},
};
HMR(Hot Module Replacement)を有効にすると、ブラウザをリロードせずに自動的にモジュールが更新される。
デバッグする
バンドルは復数ファイルを結合して作成されるので、実際のファイルとは構成が異なる。
実行時エラーがあってデバッグする場合などのための機能が存在する。
SourceMapを指定する
SourceMap機能を使うと、エラー時にブラウザコンソールに表示されるデバッグ情報を変更できる。
この設定によって、バンドル前のソースのエラー位置を特定できる。
SourceMapの種類はdevtool
ディレクティブで指定する。
var path = require('path');
module.exports = {
// entry, outputなど...
// ソースのファイル名と行数を表示するよう設定
devtool: 'inline-source-map',
};
デプロイ手法
開発後のデプロイ時に想定される問題点を挙げてみた。
いずれも未調査 / 方針未定のため以下、所感程度に。
難読化する
webpack本体に圧縮・難読化・暗号化に類する機能はない。
外部のプラグインを組み合わせて実現する例が見つかった。
いずれもnpmパッケージをインストールした上で、設定ファイルのplugins
ディレクティブに組み込んで利用する。
設定を他環境に対応する
開発/デプロイ環境などで設定が異なる場合にどう管理すべきか。
なんとなく考えうるのは以下:
- 設定ファイルを環境別に作っておき、使い分ける
- "設定ファイルの設定ファイル"を作っておき、上位のタスクランナーが設定ファイルを動的に生成してバンドルを実行する(rails/webpackerはこの方法だった)
ビルドに組み込む
webpackerはバンドル作成のためのツールなわけなので、例えば単体テストを走らせるなどタスクランナーとしての使い方を想定してない(たぶんできない)。
CI的にビルドを自動化することを考えると、より上位の汎用的なタスクランナーが、そのタスクの一部としてwebpackを呼び出して利用するかたちが想定される。
タスクランナーの選択肢は多く、どれを導入するか迷うところ:
- npm run, yarn runなど(簡易的なフロント用タスクランナー)
- gulp, grantなど(専用のタスクランナー)
- rakeなど(サーバサイドのタスクランナー)
サーバ側のビルド工程とどのように関係させるか(あるいは、分断すべきか)も検討したい。