Webpack your bags
このページは、Webpackの公式ドキュメント内で紹介されていたMaxime Fabre氏によるWebpack your bags - madewithloveの記事を madewithloveブログの「Content is published under the Creative Commons BY-SA license」に基づいて翻訳したものになります。
ブログで紹介されているWebpackのバージョンは1系のものになります。 2017年2月現在Webpackは1系を非推奨として2の使用を勧めています。
もしかしたら、あなたは既にここで紹介するWebpackと呼ばれるクールなツールについて聞いたことがあるかもしれません。 ある人は「このツールはGulpのようなビルドツールだ」と言い、またある人は「これはBrowserifyのようなバンドラーだ」と言い、 このツールのことをよく知らない人は少し混乱してしまうかもしれません。
一方でこのツールについてよく知っていたとしても、ホームページでWebpackは両方であると紹介されているため、 もしかしたら、まだ混乱している方もいるかもしれませんね。
正直なところ、初めてWebpackのサイトの"what Webpack is(Webpackとは?)"に目を通した私は、イライラしてタブを閉じてしまいました。 何しろ私は既にビルド・システムを持っており、完全にそれに満足していました。 それに、あなたが私のように流行り廃りの激しいJavaScriptシーンに身を置いているのであれば、 もしかしたら過去にその激しい時流に飛び乗ろうとして痛い目を見たことがあるのかもしれませんね。
現在はこのツールをある程度使用したことで、まだ抵抗がある人達に対して、 より明確に「Webpackとは何なのか」、そしてより重要な「何がそこまで素晴らしく、使用する価値があるのか」を説明するべく、 私はこの記事を書こうと判断しました。
Webpackとは?
最初に提示した質問に今すぐ答えてみましょう。Webpackはビルド・システムか?それとも、モジュール・バンドラーか? えーと、これは両方で…あり、だからと行って両方行えるというわけでは無く、両方が混合していると言えます。 Webpackはあなたのアセットのビルドはせず、それとは別にモジュールをバンドルしますが、 この時あなたのアセットがモジュールそのものであると考えられます。
もう少し正確に説明すると、あなたの全てのSassファイルのビルドと全ての画像の最適化、そしてそれらが含まれたものが一旦隅に置かれる代わりに、 あなたの全てのモジュールが束ねられ、ページ上にそれらが含められます。(翻訳に自信なし)
import stylesheet from 'styles/my-styles.scss';
import logo from 'img/my-logo.svg';
import someTemplate from 'html/some-template.html';
console.log(stylesheet); // "body{font-size:12px}"
console.log(logo); // "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5[...]"
console.log(someTemplate) // "<html><body><h1>Hello</h1></body></html>"
あなたの全てのアセットは、モジュールそのものであると考えられることで、 import、修正、操作、そして最後に最終的なバンドルへ束ねられることが可能になります。
これを動作させるためには、Webpackの設定にローダー(loader)を登録する必要があります。 ローダーは小さなプラグインであり、基本的には「この種類のファイルに遭遇したら、この処理を行ってください」というものです。 下記はあるローダーの例になります。
{
// .tsファイルをimportしたら、TypeScriptとして解析
test: /\.ts/,
loader: 'typescript',
},
{
// 画像はimage-webpackを使用して圧縮し(imageminのラッパー)、
// data64のURLとしてインライン化
test: /\.(png|jpg|svg)/,
loaders: ['url', 'image-webpack'],
},
{
// SCSSファイルはnode-sassで解析してautoprefixerに渡し、
// CSSの文字列として結果を返す
test: /\.scss/,
loaders: ['css', 'autoprefixer', 'sass'],
}
食物連鎖(チェーン)を経て、最終的に全てのローダーは文字列を返します。 これはWebpackがそれらをJavaScriptのモジュールへラップすることを可能にします。 一例として、ローダーによって変換されたあなたのSassファイルは、内部的には次のようになっているかもしれません。
export default 'body{font-size:12px}';
一体どうして、このような事をするのでしょうか?
Webpackが何をするのかを理解すると、このアプローチによってなし得る利点は何なのかという2つ目の疑問が頭をよぎることになるでしょう。 「画像とCSS?それをJavaScriptの中で?気は確かですか?」
我々は長きにわたり、なんとかしてHTTPリクエストを少しでも減らすために、 全てのものを1つのファイルに連結するよう教育され、訓練されてきました。
最近では、多くの人びとが全てのアセットを1つのapp.js
ファイルにバンドルし、
それを全てのページで読み込むという大いなる過ちに導く結果になっています。
つまり、開いたページで必要としない大量のアセットを読み込むために、多くの時間が費されているということです。
あるいは、あなたはそういったことをせずに、手動でアセットを読み込むようなことをしているかもしれませんが、 これは依存性ツリーの維持と、どのページがこの依存性を必要としていたか?、 どのページがスタイルシートAとBの影響を受けるのか?、といった追跡が乱雑になることは確実でしょう。
どちらのアプローチも正しくありませんし、間違っています。 Webpackはこれらの中間であると考えられるのであれば、ビルドシステムでもバンドラーでも無く、 茶目っ気たっぷりなスマート・モジュール・パッキング・システムであると言えます。
適切に構築できれば、あなたがしたことであっても、あなたのスタックをより理解し、 あなた以上に最良の最適化の方法を導き出すことになるでしょう。
小さなアプリケーションをビルドしてみましょう
Webpackの利点をより理解しやすくするために、小さなプリケーションを構築し、それにアセットをバンドルします。
このチュートリアルを実施するにあたり、Webpackを使用して動作させる際に頭痛の種となる依存性の違いによる問題を避けるために、
Node4(または5)とnpm3で実行することを推奨します。
もし、まだnpm3を入れてないのであれば、npm install npm@3 -g
を実行してインストールすることができます。
$ node --version
v5.7.1
$ npm --version
3.6.0
また、毎回node_modules/.bin/webpack
と打ち込むことを避けるために、
node_modules/.bin
を環境変数のPATH
に追加することをお薦めします。
これ以降の例は全て、実行するコマンドラインのnode_modules/.bin
部分を明記しません。
Basic bootstrapping
それではプロジェクトを作成して、Webpackをインストールしてみましょう。 後でデモを行うために、jQueryも取得しておきます。
$ npm init -y
$ npm install jquery --save
$ npm install webpack --save-dev
次にアプリケーションの素のES5でエントリー・ポイントを作成しましょう。
// src/index.js
var $ = require('jquery');
$('body').html('Hello');
webpack.config.js
ファイル内に、Webpackの設定を作成してみましょう。
Webpackの設定はオブジェクトのexportが必要な単なるJavaScriptになっています。
// webpack.config.js
module.exports = {
entry: './src',
output: {
path: 'builds',
filename: 'bundle.js',
},
};
entry
は、それらのファイルがアプリケーションのエントリー・ポイントであることをWebpackに伝えます。
これはアプリケーションの主(main)ファイルであり、依存性ツリーのトップに位置されるものです。
次にコンパイル時にbuilds
ディレクトリに、bundle.js
という名前でバンドルすることを伝えます。
それでは、これに従ってindexのHTMLを設定しましょう。
<!DOCTYPE html>
<html>
<body>
<h1>My title</h1>
<a>Click me</a>
<script src="builds/bundle.js"></script>
</body>
</html>
webpackを実行してみましょう。
全てが問題なく実行されれば、bunlde.js
が適切にコンパイルされたことを伝えるメッセージが表示されるはずです。
$ webpack
Hash: d41fc61f5b9d72c13744
Version: webpack 1.12.14
Time: 301ms
Asset Size Chunks Chunk Names
bundle.js 268 kB 0 [emitted] main
[0] ./src/index.js 53 bytes {0} [built]
+ 1 hidden modules
ここでWebpackは、1つの隠しモジュール(+ 1 hidden modules)が、
エントリー・ポイント(index.js
)と同様にbundle.js
に含まれることを伝えています。
これはjQueryであり、デフォルトでWebpackは開発者のものではないモジュールをこのように隠します。
Webpackによってコンパイルされる全てのモジュールが確認できるように、
--display-modules
フラグを渡してみましょう。
$ webpack --display-modules
bundle.js 268 kB 0 [emitted] main
[0] ./src/index.js 53 bytes {0} [built]
[1] ./~/jquery/dist/jquery.js 259 kB {0} [built]
webpack --watch
を実行してファイルの変更を監視させて、必要に応じて自動的に再コンパイルさせることも可能です。
最初のローダーの設定
ここでWebpackは、CSS、HTML、またはあらゆる種類のものをimport出来ることについて、話をしたことを思い出してください。 どこでそれが適用されているのでしょうか?
あなたがここ数年のWebコンポーネントに向かう大きな流れ(Angular 2, Vue, React, Polymer, X-Tag, 他)の中に身を置いているのであれば、 もしかしたら、アプリケーションを(1つの連結された一枚岩のようなUI部品にする代わりに) 自身のUI部品を含む再利用可能な小さな設定(Webコンポーネント)にすることによって、 メンテナンス性を高めていくという考えを耳にしているかもしれません。(ここでは簡略化して説明しています)
コンポーネントが真に自己完結するためには、自身の依存関係にあるもの全ての要求(require)も含まれなければいけません。 ボタン・コンポーネントを考えた場合、当然HTMLはあるとして、何かしらと相互作用させるためのJavaScriptと、 あるいはスタイルも必要になるかもしれません。
もし全てものが必要なときにだけ読み込まれるとしたら、それは素晴らしいことではないでしょうか? ボタン・コンポーネントがimportされた時にだけ、関連するアセット全てを取得するということです。
まずはボタンを作成してみましょう。
ほとんどの方がES2015に慣れ親しんでいると想定し、最初のローダーとしてBabelを追加します。
Webpackにローダーをインストールするために、2つのことが必要になります。
1つ目はnpm install {whatever}-loader
、2つ目はWebpackの設定のmodule.loaders
部分にそれを追加することです。
我々はBabelを欲しているので次のようにします。
$ npm install babel-loader --save-dev
また、このケースではローダーがBabelをインストールしてくれないため、Babel自身のインストールも必要になります。
babel-core
とes2015
のプリセットが必要です。
$ npm install babel-core babel-preset-es2015 --save-dev
次にそのプリセットの使用をBabelに伝えるための.babelrc
ファイルを作成します。
これはコード上で何の変換をBabelに実行させるかを設定するシンプルなJSONファイルです。
このケースでは、es2015
プリセットを使用することを指定します。
// .babelrc
{ "presets": ["es2015"] }
Babelのセットアップと設定ができたので、Webpackの設定を更新します。何がしたいのかを整理しましょう。
Babelに.js
で終わる全てのファイルを実行させたいところですが、
Webpackは全ての依存性を辿ってしまうため、jQueryのようなサード・パーティ製のコードをBabelが実行することは避けたいところです。
そのため、これに少しフィルターを付け加えます。
ローダーはinclude
、exclude
両方のルールを持つことができます。
これには文字列、正規表現、コールバックを好きなように指定することが可能です。
ここでは我々が作成したファイルのみBabelに実行してもらいたいので、
我々のソース・ディレクトリのみをinclude
します。
module.exports = {
entry: './src',
output: {
path: 'builds',
filename: 'bundle.js',
},
module: {
loaders: [
{
test: /\.js/,
loader: 'babel',
include: __dirname + '/src',
}
],
}
};
Babelが手元にあるので、index.js
をES6で書き直しましょう。
以降の例・サンプルは全てES6になります。
import $ from 'jquery';
$('body').html('Hello');
小さなコンポーネントを作成
それでは、SCSSスタイルとHTMLテンプレートそして幾つかの挙動を持つ、小さなボタン・コンポーネントを作成してみましょう。 必要なものをインストールします。 まずは非常に軽量なテンプレーティング・パッケージであるMustacheを必要としますが、 SassとHTMLファイルのローダーも必要になります。
また、あるローダーから別のローダーへ結果をパイプすることが可能であり、
ここではCSSローダーがSassローダーの結果を取り扱うことも必要になります。
CSSが用意出来たら、それらを扱う方法が複数存在します。
ここではCSSを取得してそれをページに動的に注入するstyle-loader
と呼ばれるローダーを使用します。
$ npm install mustache --save
$ npm install css-loader style-loader html-loader sass-loader node-sass --save-dev
次にローダーを別のローダーに"パイプ(pipe)"することをWebpackに伝えるために、
一連のローダーを右から左に渡し、!
で区切ります。
あるいは、loader
属性の代わりに、loaders
属性を使用して配列を指定することも可能です。
{
test: /\.js/,
loader: 'babel',
include: __dirname + '/src',
},
{
test: /\.scss/,
loader: 'style!css!sass',
// または
loaders: ['style', 'css', 'sass'],
},
{
test: /\.html/,
loader: 'html',
}
ローダーが用意出来たので、ボタンを作成してみましょう。
// src/Components/Button.scss
.button {
background: tomato;
color: white;
}
// src/Components/Button.html
<a class="button" href="{{link}}">{{text}}</a>
// src/Components/Button.js
import $ from 'jquery';
import template from './Button.html';
import Mustache from 'mustache';
import './Button.scss';
export default class Button {
constructor(link) {
this.link = link;
}
onClick(event) {
event.preventDefault();
alert(this.link);
}
render(node) {
const text = $(node).text();
// ボタンの描画
$(node).html(
Mustache.render(template, {text})
);
// リスナーを割り当て
$('.button').click(this.onClick.bind(this));
}
}
これでButton.js
は100%自己完結するものとなり、
いつ、どのコンテキストでimportされても動作し、適切に描画されます。
これであと必要なことは、ページ上でボタンを描画することだけです。
// src/index.js
import Button from './Components/Button';
const button = new Button('google.com'); button.render('a');
Webpackを実行しページを再読み込みしましょう。 ボタンが実行されることを確認できるはずです。
ここまでで、ローダーのセットアップとアプリケーション各部の依存性の定義について学ぶことができました。 この例はこれで特に問題が無いように見えますが、更に追求してみましょう。
コード分割(Code splitting)
この例は完成されていますが、我々は常にこのボタンを必要としないかもしれません。 幾つかのページではボタンは描画されないかもしれません。 こういったケースでは、ボタンのスタイル、テンプレート、Mustache、等のボタンに関するもののimportは避けたいですよね?
こういったケースで、コード分割を使用します。 このコード分割こそが、「一枚岩のバンドル(Monolithic bundle) vs 保守性のない手動import(Unmaintainable manual imports)」の問題に対するWebpackの答えです。 "分割ポイント(split points)"という領域をコード内に定義することで、簡単に別々のファイルに分割し、それらを必要に応じて読み込ませるという仕組みです。 文法は非常にシンプルです。
import $ from 'jquery';
// これが分割ポイント
require.ensure([], () => {
// この中のコードと、この中でimportされるすべてものものが
// 別々のファイルになります。
const library = require('some-big-library');
$('foo').click(() => library.doSomething());
});
require.ensure
コールバック内の全てのコードがチャンクへ分割されて別々にバンドルされ、
Webpackは必要なときにのみ、Ajaxリクエストを通して読みこむようになります。
分かりやすく説明すると、次のような構造になります。
bundle.js
|- jquery.js
|- index.js // 作成した主(main)ファイル
chunk1.js
|- some-big-libray.js
|- index-chunk.js // コールバック内のコード
そして、あなたがchunk1.js
をどこかでimport、または読み込みをする必要はありません。
Webpackは必要な時にだけ、要求に応じてそれを読み込みます。
これは、この例で行いたい様々なロジックによって、あなたのコードのチャンクをラップできることを意味します。
この例では、ページ上にリンクがある場合にのみ、Buttonコンポーネントを必要とします。
if (document.querySelectorAll('a').length) {
require.ensure([], () => {
const Button = require('./Components/Button').default; //.defaultに注意!
const button = new Button('google.com');
button.render('a');
});
}
require
を使用する際に、もしdefault exportをしたい場合は、
.default
を指定した手動によるgrabが必要になることに注意してください。
これは、require
がdefaultと標準のexportの両方を処理しないため、あなたがどちらを返すかを指定しないといけないためです。
importではこのためのシステムが用意されているため、予めそれを知ることができます。(翻訳に自信なし)
(例: import foo from 'bar'
vs import {baz} from 'bar'
)
コード分割によって、Webpackからの出力結果は異なるものになるはずです。
どのチャンクに何のモジュールが含まれるのか確認できるように、
--display-chunks
フラグを付けて実行してみましょう。
$ webpack --display-modules --display-chunks
Hash: 43b51e6cec5eb6572608
Version: webpack 1.12.14
Time: 1185ms
Asset Size Chunks Chunk Names
bundle.js 3.82 kB 0 [emitted] main
1.bundle.js 300 kB 1 [emitted]
chunk {0} bundle.js (main) 235 bytes [rendered]
[0] ./src/index.js 235 bytes {0} [built]
chunk {1} 1.bundle.js 290 kB {0} [rendered]
[1] ./src/Components/Button.js 1.94 kB {1} [built]
[2] ./~/jquery/dist/jquery.js 259 kB {1} [built]
[3] ./src/Components/Button.html 72 bytes {1} [built]
[4] ./~/mustache/mustache.js 19.4 kB {1} [built]
[5] ./src/Components/Button.scss 1.05 kB {1} [built]
[6] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built]
[7] ./~/css-loader/lib/css-base.js 1.51 kB {1} [built]
[8] ./~/style-loader/addStyles.js 7.21 kB {1} [built]
ご覧のとおり、我々のエントリー・ポイント(bundle.js
)にはWebpackのロジックのみが含まれ、
それ以外のもの(jQuery、Mustache、Button)は1.bundle.js
チャンク内に含まれ、
我々がアンカーのあるページを開いた時にのみ、これが読み込まれます。
Ajaxを使用してこれらを読み込む際に、チャンクがどこにあるのかWebpackが知ることが出来るように、 設定に下記の数行を加える必要があります。
path: 'builds',
filename: 'bundle.js',
publicPath: 'builds/',
output.publicPath
オプションは、Webpackにページの視点からのビルドしたアセットの場所を伝えます。
(そのため、このケースでは/builds/
になります)
この状態でページにアクセスすると、全て問題なく動作していることが確認できますが、 より重要なこととして確認しなければいけないことは、アンカーのあるページを訪れた際に、我々のチャンクが適切に読み込まれているか、ということです。
もしアンカーの無いページであれば、bundle.js
のみが読み込まれるでしょう。
これはアプリケーションにおける重いロジックを賢く切り離し、各ページに対して真に必要とするものだけをrequireすることを可能にします。
また、分割ポイントに名前を付けることも可能であるため、1.bundle.js
の代わりに、より分かりやすいチャンク名を付けてみましょう。
これはrequire.ensure
に、3つ目の引数を渡すことで行います。
require.ensure([], () => {
const Button = require('./Components/Button').default;
const button = new Button('google.com');
button.render('a');
}, 'button'); //←3つ目の引数
1.bundle.js
の代わりに、button.bundle.js
が生成されます。
2つ目のコンポーネントを追加しましょう。
下記の2つ目のコンポーネントを追加して、動作するか確認してみましょう。
.header {
font-size: 3rem;
}
<header class="header">{{text}}</header>
import $ from 'jquery';
import Mustache from 'mustache';
import template from './Header.html';
import './Header.scss';
export default class Header {
render(node) {
const text = $(node).text();
$(node).html(
Mustache.render(template, {text})
);
}
}
アプリケーションに描画してみましょう。
// アンカー(a)があれば、Buttonコンポーネントをそこに描画します
if (document.querySelectorAll('a').length) {
require.ensure([], () => {
//(訳注: 元記事には末尾の.defaultがありませんでした。これが無いと動作しない?)
const Button = require('./Components/Button').default;
const button = new Button('google.com');
button.render('a');
});
}
// タイトル(h1)があれば、Headerコンポーネントをそこに描画します
if (document.querySelectorAll('h1').length) {
require.ensure([], () => {
//(訳注: 元記事には末尾の.defaultがありませんでした。これが無いと動作しない?)
const Header = require('./Components/Header').default;
new Header().render('h1');
});
}
Webpackが出力するものを確認したいので、--display-chunks --display-modules
フラグを付けて実行します。
$ webpack --display-modules --display-chunks
Hash: 178b46d1d1570ff8bceb
Version: webpack 1.12.14
Time: 1548ms
Asset Size Chunks Chunk Names
bundle.js 4.16 kB 0 [emitted] main
1.bundle.js 300 kB 1 [emitted]
2.bundle.js 299 kB 2 [emitted]
chunk {0} bundle.js (main) 550 bytes [rendered]
[0] ./src/index.js 550 bytes {0} [built]
chunk {1} 1.bundle.js 290 kB {0} [rendered]
[1] ./src/Components/Button.js 1.94 kB {1} [built]
[2] ./~/jquery/dist/jquery.js 259 kB {1} {2} [built]
[3] ./src/Components/Button.html 72 bytes {1} [built]
[4] ./~/mustache/mustache.js 19.4 kB {1} {2} [built]
[5] ./src/Components/Button.scss 1.05 kB {1} [built]
[6] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built]
[7] ./~/css-loader/lib/css-base.js 1.51 kB {1} {2} [built]
[8] ./~/style-loader/addStyles.js 7.21 kB {1} {2} [built]
chunk {2} 2.bundle.js 290 kB {0} [rendered]
[2] ./~/jquery/dist/jquery.js 259 kB {1} {2} [built]
[4] ./~/mustache/mustache.js 19.4 kB {1} {2} [built]
[7] ./~/css-loader/lib/css-base.js 1.51 kB {1} {2} [built]
[8] ./~/style-loader/addStyles.js 7.21 kB {1} {2} [built]
[9] ./src/Components/Header.js 1.62 kB {2} [built]
[10] ./src/Components/Header.html 64 bytes {2} [built]
[11] ./src/Components/Header.scss 1.05 kB {2} [built]
[12] ./~/css-loader!./~/sass-loader!./src/Components/Header.scss 192 bytes {2} [built]
両方のコンポーネントがjQueryとMustacheを必要としたことで、 これらの依存性が我々の各チャンク内で重複してしまい、あなたはこれを大きな問題であると捉えるかもしれません。 そして、これは我々の望むところではありません。
Webpackは、デフォルトで最低限の最適化しか行いません。 ただし、多くの集合知から成るプラグインが、この状況を好転させる手助けをしてくれます。
プラグインは、特定のファイルの集まりの上でしか実行されない、そして"パイプ役"としてのローダーとは異なり、 全てのファイル上で実行され、より高度なことを行い、必ずしも変換に関することを行う必要はありません。 Webpackは、一部のプラグインを活用することで様々な最適化を可能にします。
その1つに、このケースで我々が関心を寄せるCommonChunksPluginがあります。
このプラグインは、チャンクで再発する依存性を分析して、それらを別の場所に抜き出します。
これは、完全に別のファイル(vendor.js
のような)に分割したり、
主(main)ファイルにしてしまうことが可能です。
このケースでは、我々は共通する依存性をエントリーファイルに移動したいと考えています。 これは、全てのページでjQueryとMustacheが必要になると、同様に依存性の移動が必要になってしまうためです。 それでは、設定ファイルを更新してみましょう。
var webpack = require('webpack');
module.exports = {
entry: './src',
output: {
// ...
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'main', // 依存性を主(main)ファイルに移す
children: true, // 全ての子に対しても共通する依存性を探す
minChunks: 2, // この回数、依存性に遭遇したら抜き出す
}),
],
module: {
// ...
}
};
再びWebpackを実行すると、状況がよくなっていることが確認できます。
ここでのmain
とは、デフォルトのチャンクの名前です。
chunk {0} bundle.js (main) 287 kB [rendered]
[0] ./src/index.js 550 bytes {0} [built]
[2] ./~/jquery/dist/jquery.js 259 kB {0} [built]
[4] ./~/mustache/mustache.js 19.4 kB {0} [built]
[7] ./~/css-loader/lib/css-base.js 1.51 kB {0} [built]
[8] ./~/style-loader/addStyles.js 7.21 kB {0} [built]
chunk {1} 1.bundle.js 3.28 kB {0} [rendered]
[1] ./src/Components/Button.js 1.94 kB {1} [built]
[3] ./src/Components/Button.html 72 bytes {1} [built]
[5] ./src/Components/Button.scss 1.05 kB {1} [built]
[6] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built]
chunk {2} 2.bundle.js 2.92 kB {0} [rendered]
[9] ./src/Components/Header.js 1.62 kB {2} [built]
[10] ./src/Components/Header.html 64 bytes {2} [built]
[11] ./src/Components/Header.scss 1.05 kB {2} [built]
[12] ./~/css-loader!./~/sass-loader!./src/Components/Header.scss 192 bytes {2} [built]
もし、name: 'vendor'
を指定していたとしたら、
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
children: true,
minChunks: 2,
}),
そのチャンクがまだ存在しないため、Webpackはbuilds/vendor.js
を作成し、
我々はHTML内でそれを手動でimportする必要があるでしょう。
<script src="builds/vendor.js"></script>
<script src="builds/bundle.js"></script>
async: true
を指定し、共通のチャンク名を提供しないことで、
非同期で共通の依存性を読みこませるようにすることも可能です。
Webpackには、このように強力で賢い最適化が数多く備わっています。
私はこれら全てにとても名前を付けられませんが、訓練として我々のアプリケーションの製品版を作成してみましょう。
To production and beyond
OK、まずは幾つかのプラグインを設定に追加してみましょう。
ただし、NODE_ENV
がproduction
の場合にのみ、それらを読み込みたいので、
設定に幾つかのロジックを加えましょう。
設定ファイルはあくまでJSファイルなので、とても簡単に実装することができます。
var webpack = require('webpack');
var production = process.env.NODE_ENV === 'production';
var plugins = [
new webpack.optimize.CommonsChunkPlugin({
name: 'main', // 依存性を主(main)ファイルに移す
children: true, // 全ての子に対しても共通する依存性を探す
minChunks: 2, // この回数、依存性に遭遇したら抜き出す
}),
];
if (production) {
plugins = plugins.concat([
// 製品版のプラグインをここに指定
]);
}
module.exports = {
entry: './src',
output: {
path: 'builds',
filename: 'bundle.js',
publicPath: 'builds/',
},
plugins: plugins,
// ...
};
次に、Webpackにも製品版でON/OFFされる幾つかの設定を持たせます。
module.exports = {
debug: !production,
devtool: production ? false : 'eval',
1つ目の設定はローダーをデバッグモードから切り替えます。 これは、ローカルでのデバッグ作業を手助けするコードが含まれなくなることを意味します。
2つ目はsourcemapsの生成に関するものです。
Webpackには幾つかのソースマップの描画方法が備わっており、
eval
はローカル環境における最適の方法になります。
製品版では、ソースマップは無効化するため、気にする必要はないでしょう。 それでは、製品版のプラグインを追加してみましょう。
if (production) {
plugins = plugins.concat([
// このプラグインは同名のチャンクとファイルを探し、
// キャッシュ向上のために、これらをマージします
new webpack.optimize.DedupePlugin(),
// このプラグインは、チャンクとモジュールが
// アプリケーション内でどれだけ使用されているかによって
// 最適化を行います。
new webpack.optimize.OccurenceOrderPlugin(),
// このプラグインはWebpackに作成されるチャンクのサイズが小さくなりすぎることで、
// 読み込み効率が悪くなることを防ぎます。
new webpack.optimize.MinChunkSizePlugin({
minChunkSize: 51200, // ~50kb
}),
// このプラグインは最終的なバンドルの全てのJavaScriptコードを
// 圧縮(minify)します。
new webpack.optimize.UglifyJsPlugin({
mangle: true,
compress: {
warnings: false, // uglificationの警告を隠します
},
}),
// このプラグインは、製品版でfalseを設定することができる様々な変数を定義し、
// 最終的なバンドルでのコンパイルから、それらに関連するコードを避けるためことができます。
new webpack.DefinePlugin({
__SERVER__: !production,
__DEVELOPMENT__: !production,
__DEVTOOLS__: !production,
'process.env': {
BABEL_ENV: JSON.stringify(process.env.NODE_ENV),
},
}),
]);
}
これは私が最もよく使用するプラグインですが、Webpackはこれだけではなく数多くのプラグインを提供しているので、 あなたはあなたのモジュールとチャンクを調整するのに、よりふさわしいものを選ぶことができます。 また、ユーザーによって提供されている様々なことを解決するためのプラグインを、npm上で見つけることもできます。 この記事の最後に、有用なプラグインのリンクを紹介しています。
ここで別の側面から製品版のアセットのことを考えると、あなたのアセットにバージョン付けがされるのが理想的でしょう。
我々がoutput.filename
にbundle.js
を設定したことを覚えてしますか?
このオプションで使用することが出来る変数が幾つか存在し、
その1つが[hash]
であり、これは最終的なバンドルの内容のハッシュに対応するものになります。
これを使用して設定のコードを書き換えてみましょう。
また、チャンクにもバージョン付けをしたいので、
同じことを行うためにoutput.chunkFilename
を追加します。
output: {
path: 'builds',
filename: production ? '[name]-[hash].js' : 'bundle.js',
chunkFilename: '[name]-[chunkhash].js',
publicPath: 'builds/',
},
この単純なプリケーションでは、コンパイルされたバンドルの名前を動的に取得する方法が無いため、
この例では製品版のアセットにだけ、バージョン付けを行います。(翻訳に自信なし)
また、製品版のビルドを行う前に、builds
フォルダ内をクリーンアップすることもしたいので、
次のようにしてサード・パーテイ製のプラグインをインストールしましょう。
$ npm install clean-webpack-plugin --save-dev
そして、設定を追加します。
var webpack = require('webpack');
var CleanPlugin = require('clean-webpack-plugin');
// ...
if (production) {
plugins = plugins.concat([
// builds/フォルダ内を
// 最終的なアセットのコンパイル前にクリーンアップします。
new CleanPlugin('builds'),
OK、気の利いた設定環境が完成しました。 結果を比較してみましょう。
$ webpack
bundle.js 314 kB 0 [emitted] main
1-21660ec268fe9de7776c.js 4.46 kB 1 [emitted]
2-fcc95abf34773e79afda.js 4.15 kB 2 [emitted]
$ NODE_ENV=production webpack
main-937cc23ccbf192c9edd6.js 97.2 kB 0 [emitted] main
Webpackが行ったことを確認すると、まず第1に、この例のアプリケーションが非常に軽量であり、 2つをわざわざ非同期チャンクとしてHTTPリクエストする価値が無いため、 Webpackはこれらをエントリーポイントに戻ってマージします。 第2に、全てのものが圧縮(minify)されています。
我々は322kbのデータに対して合計で3つのHTTPリクエストを行っていましたが、 これが97kbのデータに対する1つのHTTPリクエストになりました。
しかしWebpackの役割は、バカみたいに大きな1つのJSファイルになることを食い止めることでは無かったのですか?
そうです、その通りです、ただし、このようなことが起こってしまったのはアプリケーションがあまりにも小さいからです。 ここで、あなたは何が何時何処でマージされるのかを考えなくてよい、ということを想像してみてください。
もし、突然あなたのチャンクが多くの依存性を必要とするようになってしまった場合、 マージされていたチャンクが非同期チャンクに移されます。 また、これらのチャンクが別々に読み込んでいるものがあまりにも似すぎている場合に、 それらをマージしたりするかもしれません。(翻訳に自信なし)
ルールを設定すると、それ以降、 Webpackは自動的にあなたのアプリケーションを可能なかぎり最適化します。 手作業を行わず、何の依存性が何処にあるのか、または何処で必要とされるのか、そのようなことは一切考えず、 全てを自動化してしまいましょう。
あなたは、私がHTMLとCSSの圧縮の設定を何も行なっていことに気づいているかもしれません。
これは、予めdebug
オプションをfalseにしておくと、
css-loader
とhtml-loader
がデフォルトでそれらをケアするためです。
また、Uglifyが別のプラグインである理由は、
Webpack自身がJSローダーであるため、Webpackにjs-loader
が存在しないためです。
抜き出し(Extraction)
このチュートリアルの冒頭でのスタイルが、webページにライブ的に注入されることで、 目障りな点滅(ちらつき)が発生していることに、あなたは既に気づいているかもしれません。 Webpackが現在のビルドで集めた全てのスタイルを、最終的なCSSファイルにパイプするだけで良いのではないでしょうか? 外部プラグインを助けを借りて、この問題を解決しましょう。
$ npm install extract-text-webpack-plugin --save-dev
このプラグインが行う事はまさに私が述べた通り、 最終的なバンドルから特定の種類のコンテンツを集め、 他のものとパイプすることです。 ほとんどのケースで、CSSのために使用されます。 それでは設定してみましょう。
var webpack = require('webpack');
var CleanPlugin = require('clean-webpack-plugin');
var ExtractPlugin = require('extract-text-webpack-plugin');
var production = process.env.NODE_ENV === 'production';
var plugins = [
new ExtractPlugin('bundle.css'), // <=== コンテンツがパイプされるべき場所 (エントリポイントで指定されるCSSだけが対象)
//new ExtractPlugin('bundle.css', {allChunks: true}), //全てのチャンクのCSSも抜き出したい場合
new webpack.optimize.CommonsChunkPlugin({
name: 'main', // 依存性を主(main)ファイルに移す
children: true, // 全ての子に対しても共通する依存性を探す
minChunks: 2, // この回数、依存性に遭遇したら抜き出す
}),
];
// ...
module.exports = {
// ...
plugins: plugins,
module: {
loaders: [
{
test: /\.scss/,
loader: ExtractPlugin.extract('style', 'css!sass'),
},
// ...
],
}
};
extract
メソッドは2つの引数を取得します。
1つ目は抜き出したコンテンツを使用して、チャンクの場合に行うこと('style'
)、
2つ目は主(main)ファイルの場合に行うこと('css!sass'
)になります。
チャンクの場合は、生成されたものへCSSを追加することが出来ないため、
事前にここでstyle
ローダーを使用しますが、主ファイルで見つかる全てのスタイルは、
builds/bundle.css
ファイルへパイプされます。
アプリケーション用の小さな主スタイルシートを追加して、確認してみましょう。
body {
font-family: sans-serif;
background: darken(white, 0.2);
}
import './styles.scss';
// ファイルの残り部分…
Webpackを実行し、期待した通りHTML内でimportするbundle.css
があることを確認します。
$ webpack
bundle.js 318 kB 0 [emitted] main
1-a110b2d7814eb963b0b5.js 4.43 kB 1 [emitted]
2-03eb25b4d6b52a50eb89.js 4.1 kB 2 [emitted]
bundle.css 59 bytes 0 [emitted] main
もし、チャンクのスタイルも抜き出したいのであれば、
ExtractTextPlugin('bundle.css', {allChunks: true})
オプションを渡すことも出来ます。
また、ファイル名に変数を使用できることも覚えておいてください。
例えば、スタイルシートにバージョン付けをしたいのであれば、
JavaScriptファイルで行ったようにExtractTextPlugin('[name]-[hash].css')
と指定します。
Images all the people
ここまででJavaScriptファイルに関して説明してきましたが、 画像やフォント等のコンクリート(concrete)・アセットについて全く話をしていませんでした。 Webpackではそれらの作業をどのように行い、どうすれば最良の最適化が出来るのでしょうか? 背景に使用するための画像を、webから手に入れましょう。 Geocitiesで手に入れる人々を見てきましたが、 どれもCoolなものばかりでした。
この画像をimg/puppy.jpg
として保存し、これに沿ってSassファイルを更新しましょう。
body {
font-family: sans-serif;
background: darken(white, 0.2);
background-image: url('../img/puppy.jpg');
background-size: cover;
}
このまま実行しても、Webpackは「このJPGを一体どうしたいんだ!?」ということを伝えてきます。
ローダーを指定していないので当然です。
コンクリート・アセットを処理するためのネイティブのローダーには、
file-loader
とurl-loader
の2つが存在します。
- 1つ目は特に何か変更することも無く、アセットへのURLを返すだけであり、 処理中にそのファイルのバージョン付け(翻訳に自信なし)を可能にしてくれます。(デフォルトの挙動)
-
2つ目はアセットを
data:image/jpeg;base64
のURLへインライン化します。
実際に、どちらを使用するかは臨機応変に対応する必要があります。 もし、使用する背景画像が2Mbの画像である場合、あなたはそれをインライン化したくは無いでしょうし、別途読み込むのが望ましいでしょう。 一方で、それが2kbの小さなアイコンである場合、インライン化してHTTPリクエストを節約するのが好ましいでしょう。 そこで、この両方を満たすセットアップをしてみましょう。
$ npm install url-loader file-loader --save-dev
{
test: /\.(png|gif|jpe?g|svg)$/i,
loader: 'url?limit=10000',
},
ここで、limit
クエリー・パラメーターをurl-loader
に渡しており、
これは「もし、アセットが10kbより小さければインライン化し、そうでなければfile-loader
にフォールバックして、
それを参照する」ということを伝えています。
この文法はクエリー文字列(query string)と呼ばれ、ローダーの設定に使用されますが、 代わりにオブジェクトを通じてローダーを設定することも可能です。
{
test: /\.(png|gif|jpe?g|svg)$/i,
loader: 'url',
query: {
limit: 10000,
}
}
それでは実行してみましょう。
bundle.js 15 kB 0 [emitted] main
1-b8256867498f4be01fd7.js 317 kB 1 [emitted]
2-e1bc215a6b91d55a09aa.js 317 kB 2 [emitted]
bundle.css 2.9 kB 0 [emitted] main
ここでJPGに関する情報が確認できないのは、子犬の画像が設定サイズよりも小さく、インライン化されたためです。
WebpackがサイズとHTTPリクエスの比率によって、コンクリート・アセットを賢く最適化してくれるので、とても頼りになります。
ローダーの読み込みには優れたものが存在し、更にこれをパイプすることが可能です。
最も一般的なものの1つに、image-loaderを使用して、
全ての画像をバンドルされる前にimagemin
(画像圧縮)を通すことが挙げられるでしょう。
?bypassOnDebug
のクエリー文字列によって、製品版でのみ実行することも可能です。
このようなプラグインが数多く存在するので、この記事の最後のリストを確認しておくことをお薦めします。
We'll do it live dammit
ここまでに製品版ビルドについて気にすることが多かったので、ここでローカルの環境に目を向けてみましょう。 あなたはビルドツールの話題において、通常であれば取り上げられるであろうLiveReloadやBrowserSync、 あるいはあなたの好みのライブ・リロードについての話が抜けていることに気づいているかもしれません。
ただし、ページ全体を再読み込みするのは愚か者のやることです。 一歩進んだHot Module Replacement (HMR)、またはHot Reloadと呼ばれる機能を使用してみましょう。
この考え方は、Webpackは依存性ツリーの各モジュールの位置を正確に把握しているため、 その変更点を新しいファイルを使用したツリーの一部である単純なパッチで表すことができるというものです。(翻訳に自信なし) 分かりやすく言うと、あなたの行った変更がスクリーン上で、ページの再読み込み無しに反映されるということです。
HMRを使用するためには、ホット・アセット(hot assets)を提供してくれるサーバーが必要になります。
Webpackに付属するdev-server
をこのために利用できるので、インストールしましょう。
$ npm install webpack-dev-server --save-dev
下記のコマンドを実行して、サーバーを起動します。
$ webpack-dev-server --inline --hot
1つ目のフラグは、WebpackにそのHMRの処理ロジックをページに埋め込む(iframe内にページを含ませる代わりに)ように指示し、
2つ目のフラグはHMRを有効にするように指示しています。
http://localhost:8080/webpack-dev-server/
にアクセスしてみましょう。
確認すると今までどおりのページですが、Sassファイルの1つを編集してみると…
webpack-dev-server
は自身のローカル・サーバーとして使用することが可能です。
もし、常にHMRを使用するつもりなのであれば、設定にそのように指定することが可能です。
output: {
path: 'builds',
filename: production ? '[name]-[hash].js' : 'bundle.js',
chunkFilename: '[name]-[chunkhash].js',
publicPath: 'builds/',
},
devServer: {
hot: true,
},
これで、webpack-dev-server
を実行すれば、常にHMRモードになります。
ここでwebpack-dev-server
を使用してホット・アセットを提供していますが、
Expressサーバーのように他のオプションも使用できることに注意してください。
Webpackは、HMR機能を他のサーバーと繋げるためのミドルウェアを提供します。
Get clean or die lintin'
もし、あなたがこのチュートリアルを注意深く読み進めているのであれば、
何故ローダーはmodule.loaders
内に入れられているのに、プラグインはそうでは無いのかと疑問に思っているかもしれません。
もちろん、それは別のものをmodule
内に置くことが可能だからです!
Webpackにはローダー(loader)をだけでなく、pre-loaderとpost-loaderもあり、そ れぞれコード上で主ローダーの前と後に実行されます。 例を用いて試してみましょう。 この記事のコードは滅茶苦茶なコードなので、変換前にこのコードをESLintにかけてみましょう。
$ npm install eslint eslint-loader babel-eslint --save-dev
そして、失敗することを見越した最小限の.eslintrc
ファイルを作成してみましょう。
parser: 'babel-eslint'
rules:
quotes: 2
pre-loaderを追加します。
これまでと同じ文法を使用しますが、これをmodule.preLoaders
の中に書きます。
module: {
preLoaders: [
{
test: /\.js/,
loader: 'eslint',
}
],
Webpackを実行すると、失敗することが確認できます。
$ webpack
Hash: 33cc307122f0a9608812
Version: webpack 1.12.2
Time: 1307ms
Asset Size Chunks Chunk Names
bundle.js 305 kB 0 [emitted] main
1-551ae2634fda70fd8502.js 4.5 kB 1 [emitted]
2-999713ac2cd9c7cf079b.js 4.17 kB 2 [emitted]
bundle.css 59 bytes 0 [emitted] main
+ 15 hidden modules
ERROR in ./src/index.js
/Users/anahkiasen/Sites/webpack/src/index.js
1:8 error Strings must use doublequote quotes
4:31 error Strings must use doublequote quotes
6:32 error Strings must use doublequote quotes
7:35 error Strings must use doublequote quotes
9:23 error Strings must use doublequote quotes
14:31 error Strings must use doublequote quotes
16:32 error Strings must use doublequote quotes
18:29 error Strings must use doublequote quotes
別のpre-loaderの例を試してみましょう。 現在、各コンポーネントに対して同じ名前のスタイルシート、同じ名前のテンプレートをimportしています。 pre-loaderを使用して、モジュールの名前と関連付くファイルを自動的に読み込むようにしましょう。
$ npm install baggage-loader --save-dev
{
test: /\.js/,
loader: 'baggage?[file].html=template&[file].scss',
}
これはWebpackに、もし同じ名前のHTMLファイルがあれば、テンプレートとしてそれを読み込み、 また同じ名前のSassファイルもimportすることを指示しています。 これで、下記のコンポーネントを
import $ from 'jquery';
import template from './Button.html';
import Mustache from 'mustache';
import './Button.scss';
次のように変更することができます。
import $ from 'jquery';
import Mustache from 'mustache';
ご覧のようにpre-loaderは非常に強力で、post-loaderにも同じことが言えます。 この記事の最後にある有用なローダーの一覧を確認しておいてください。 様々なケースで役立つローダーがきっと見つかるはずです。
Would you like to know more?
現在の我々のアプリケーションはかなり小さいものですが、 これが大きくなり始めると、実際の依存性ツリーがどうなっているのかを、より正確に理解できることが重要になるでしょう。
何が正しくて何が間違っているのか、何がアプリケーションのボトルネックになっているかなど、 内部的にWebpackはこれらのことを全て把握していますが、あなたはWebpackにそれを見せてもらうために、丁寧に尋ねる必要があります。 下記のコマンドを実行してprofileファイルを作成することで、これを行うことが可能です。
webpack --profile --json > stats.json
1つ目のフラグは、Webpackにprofileファイルを生成することを指示し、 2つ目のフラグは、それをJSONで生成することを指示し、 最後はJSONファイルにこの全ての出力をパイプしています。
現在、これらのprofileファイルを解析する複数のWebサイトが存在しますが、 Webpackはこの情報を解読するための公式サイトを提供しています。 Webpack Analyzeにアクセスし、 あなたのJSONファイルをそこでimportしてみてください。 Modulesタブを開けば、あなたの依存性ツリーを視覚的に表したものが確認できるはずです。
赤いドットは、最終的なバンドルで問題のある箇所を示します。 我々のケースではjQueryに問題があるとマーキングされており、これは全てモジュールの中で最も比重が大きいためです。
他の全てのタブも見回してみてください。 この小さなアプリケーションでは学べるものは多くはないですが、 このツールが依存性ツリーと最終的なバンドルの見解を得る非常に重要なツールの1つであることは間違いないでしょう。
先程話したように、他のサービスもprofileファイルを分析します。 私が好きなサービスの1つにWebpack Visualizerがあり、 これはバンドルに占めるものの割合をドーナツ型のチャートでスピンアップします。 下記は我々のアプリケーションによるチャートです。
That's all folks
私にとって、WebpackはGruntまたはGlupを完全に置き換えるものとなっており、 以前行っていたことのほとんどをWebpackで処理し、残りはnpm scriptを使用しています。 我々が行ってきた一般的なタスクの例の1つに、APIドキュメントをAglioを使用してHTMLに変換することが挙げられますが、 次のようにしてそれを簡単に行うことができます。
{
"scripts": {
"build": "webpack",
"build:api": "aglio -i docs/api/index.apib -o docs/api/index.html"
}
}
バンドルやアセットに無関係な複雑なタスクがGlupのスタックに積まれている場合でも、 Webpackは他のビルド・システムを使用して申し分なく処理してくれます。 この例では、Gulp内にWebpackを統合させています。
var gulp = require('gulp');
var gutil = require('gutil');
var webpack = require('webpack');
var config = require('./webpack.config');
gulp.task('default', function(callback) {
webpack(config, function(error, stats) {
if (error) throw new gutil.PluginError('webpack', error);
gutil.log('[webpack]', stats.toString());
callback();
});
});
WebpackはNode APIも持つため、別のビルド・システム内でも簡単に使用することが可能で、 どのようなケースでも、それを吊るすことが出来るラッパーを見つけることが出来るでしょう。
何はともあれ、これで私はあなたにWebpackの概要を十分に示せたと思います。 あなたは、この記事で多くのことが説明されたと考えているかもしれませんが、 複数のエントリーポイント(multiple entry points)、prefetching、コンテキスト置換(context replacement)等、表面を撫でただけに過ぎません。
Webpackは非常に強力なツールではあるものの、 あなたの使い慣れたツールと比較した際に、設定文法が分かりづらく、それに見合うコストを払わなければいけないでしょう。 私はそれを否定しません。
ですが、あなたがWebpackの飼いならし方さえ理解すれば、それは甘えた猫のようになついてくれるでしょう。(翻訳に自信なし) 私は幾つかのプロジェクトでWebpackを使用して、自分の頭では何のアセットが何処で何時必要だったのかを、正直思い浮かべることが出来ないような、 多くの最適化と自動化を行ってきました。
リソース
Content is published under the Creative Commons BY-SA license.
このページは、ページトップのリンク先のWebpack your bags - madewithloveの記事を翻訳した内容で構成されています。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。