Webpackの通常のビルドは遅い
Sailsはデフォルトでgrunt-watchタスクを使っているので、最初はこれを利用してファイルを変更するたびにgrunt-webpackを実行してjsをリビルドしていました。
しかし、WebpackでVue.jsのソースを普通にビルドすると、私のローカル環境では15秒以上かかっていました。開発効率を著しく下げていたので高速化する方法を調べました。
webpackのビルド高速化の効果を測ってみたという記事によると、インクリメンタルビルドにすれば早くなりそう。公式のドキュメントによると、インクリメンタルビルドはwebpack-dev-serverの機能らしいので、webpack-dev-serverをsails.jsに導入する方法を調べました。
webpack-dev-serverを既存のサーバーと結合する
webpack-dev-serveはHTML/CSS/JSなどの静的ファイルをサーブするだけのExpressサーバーです。既存のAPIサーバー(今回はSails.js )と結合して開発するための設定が必要です。
webpack-dev-serverからjsを取得する
webpack-dev-serverは、Expressのサーバーをポート8080(デフォルト)で起動して、ビルドしたjsをメモリに保持し、メモリからサーブします。ファイルシステムに書き込まないので通常のビルドに比べて高速です。
しかし、ファイルに書き出されないため、既存のサーバー(Sailsなど)がwebpack-dev-serverでビルドされたコードを取得するには、localhost:8080から取得するように指定する必要があります。
例:
<script src="/js/app.js"></script>
<script src="http://localhost:8080/js/app.js"></script>
Sailsでテンプレートエンジンにejsを使っている場合、sail.config.environment
でソースの参照元を分ければOK。
<% if (sails.config.environment === 'development') { %>
<script src="http://localhost:8080/js/app.js"></script>
<% } else { %>
<script src="/js/app.js"></script>
<% } %>
contentBaseとoutput.path
Sailsでは、デフォルトで .tmp/public/以下にコンパイルされたアセットファイルを出力し、このディレクトリからファイルをサーブする仕様になっています。よって、webpackのoutputの設定は以下のようになります。
output: {
path: path.join(__dirname, '../../.tmp/public'),
filename: 'js/[name].js', // ※この`js/`をpathに含めると動かない!
},
そして、localhost:8080/js/app.jsへのリクエストが.tmp/public/js/app.jsを返すようにするための設定がcontentBase。
grunt.config.set('webpack-dev-server', {
options: {
contentBase: '.tmp/public', // このディレクトリ以下のファイルをサーブする
inline: true,
hot: true
},
outputの設定が非常に気持ち悪いのですが、どうやらoutput.pathがcontentBaseと一致している必要があるようで、js/
をfilenameではなくpathの方に含めると、localhost:8080/js/app.jsはファイルとして保存された.tmp/public/js/app.jsを返してしまい、リビルドの内容が反映されませんでした。
webpackの公式ドキュメントにはそんなこと書いてないから、リビルドが反映されない原因を探すのに苦労しました・・・
CORSエラー
ここまで設定してsailsサーバーとwebpack-dev-serverを起動すると、CORSエラー(No 'Access-Control-Allow-Origin' header is present on the requested resource. )が出ました。localhost:1337からlocalhost:8080にリクエストを送っているからですね。以下のようにheadersの設定をdev-serverの設定に追加して解決しました。
grunt.config.set('webpack-dev-server', {
options: {
contentBase: '.tmp/public',
headers: {
'Access-Control-Allow-Origin': '*' // CORSエラー回避
}
},
これでwebpackの設定は基本的に完了です。
HotModuleReplacement
webpack-dev-serverとVue.jsでHotModuleReplacementができます!HotModuleReplacementとは、ファイル変更を検知したら自動的にブラウザのjsファイルをアップデートしてくれるというもの。ブラウザ自体をリフレッシュするのとは違い、フロントエンドで保持してるデータは消えません。
- webpack-dev-serverの設定に
inline: true
とhot: true
を追加し、 - HotModuleReplacementPluginというプラグインを入れ、
- output.publicPathにdev serverのURL
を指定することで使えます。
grunt.config.set('webpack-dev-server', {
options: {
contentBase: '.tmp/public',
inline: true, // inlineモード
hot: true // HotModuleReplacementをしますよ、の設定
},
dev: {
webpack: {
plugins: [
new webpack.HotModuleReplacementPlugin(), // HotModuleReplacementに必要なプラグイン
],
output: {
path: path.join(__dirname, '../../.tmp/public'),
filename: 'js/build/[name].js',
publicPath: "http://localhost:8080/" // dev serverのURL。
},
これで動いたけど、ブラウザのコンソールでUncaught RangeError: Maximum call stack size exceeded
のエラーが。直し方わからず、とりあえず実害もなさそうなので、一旦放置してます。わかる方いたら教えてください。
grunt-webpackの設定ファイル
上記の設定を踏まえたgrunt-webpackの設定ファイル例です。
const webpack = require('webpack');
const path = require('path');
const _ = require('lodash');
// プロダクションビルド用の設定。繰り返したくないので変数にまとめておく。
const buildConfig = {
entry: {
app: './client/web/app.js', // ビルド対象のエントリーファイル
},
output: {
path: path.join(__dirname, '../../.tmp/public'), // 絶対パスが必要。__dirnameは{アプリの絶対パス}/tasks/config
filename: 'js/[name].js', // js/はpathには含めちゃダメ
},
module: {
loaders: [
{test: /\.vue$/, loader: 'vue-loader'},
{test: /\.js$/, loader: 'babel-loader', query: {presets: ['es2015']}}
]
},
};
module.exports = function (grunt) {
// webpackの設定
grunt.config.set('webpack', {
build: buildConfig, // 上で作った設定そのまま
});
// webpack-dev-serverの設定
grunt.config.set('webpack-dev-server', {
options: {
contentBase: '.tmp/public', // サーブするディレクトリの設定
inline: true, // HMRのため
hot: true, // HMRしますよのサイン
headers: {
'Access-Control-Allow-Origin': '*' // CORSエラー対策
}
},
dev: {
webpack: _.extend(buildConfig, { // webpackの設定と違うところを設定
plugins: [
new webpack.HotModuleReplacementPlugin(), // HMRのためのプラグイン
],
output: {
path: path.join(__dirname, '../../.tmp/public'),
filename: 'js/[name].js',
publicPath: 'http://localhost:8080/' // HMRのために必要
},
devtool: '#source-map', // source-mapは本番ビルドには不要
})
}
});
grunt.registerTask('webpack', [
'webpack:build'
]);
grunt.registerTask('webpack-dev-server', [
'webpack-dev-server:dev'
]);
grunt.loadNpmTasks('grunt-webpack');
};
sails lift
でwebpack-dev-serverも起動する
最後に、sailsの起動コマンドsails lift
でwebapck-dev-serverも起動する設定を行います。
sailsでjsのビルドを担うのはgruntのcompileAssetsタスク。これがプロダクションモードでも開発モードでも呼ばれています。
module.exports = function(grunt) {
grunt.registerTask('compileAssets', [
'clean:dev',
'webpack:build', // webpackのタスクを追加。cleanタスクより後におく。
'jst:dev',
'less:dev',
'copy:dev',
'coffee:dev'
]);
};
開発環境では、webpackタスクの代わりにwebpack-dev-serverタスクを実行したいので、新しいタスクをcompileAssetsDevとして追加します。
module.exports = function(grunt) {
grunt.registerTask('compileAssetsDev', [
'clean:dev',
'jst:dev',
'less:dev',
'copy:dev',
'coffee:dev',
'webpack-dev-server', // 最後に置かないと後続のタスクが実行されないっぽい
]);
};
そしてdefaultタスクでこれを実行します。
module.exports = function (grunt) {
grunt.registerTask('default', [
'compileAssetsDev',
'linkAssets',
'watch'
]);
};
まとめ
webpack-dev-serverは、設定はハマりポイントが多くて大変でしたが、超便利です(特にHotModuleReload)。後公式ドキュメントがいまいちわかりにくい・・・
もうちょっとWEBに情報が充実してくると使いやすくなると思います。