先日Developers Summitデビューしました。こんにちは、先生です。
前回公開した記事「エンジニアがいい感じにフロントエンド開発を爆速化できる環境構築の手順」の反響が大きかったので、そこで使われているWebPackというModule Bundlerをもう少し深く掘り下げていきたいと思います。
WebPackとは
WebPackは静的なファイルの依存関係を解決しつつ結合したり分割したりするツールです。非常に多機能でカスタマイズの幅が広いのが特徴です。
http://webpack.github.io/docs/
個人的な経緯ですが、require.js -> Browserifyを経てWebPackに落ち着いたところです。
WebPackはnpmを使ってインストールします。
npm install webpack -g
※ npmが使えない方はまずNode.jsをインストールしてください。
【参考】「Gulp入門 - Node.jsをインストール」
今日はWebPackでできる代表的な以下の機能をご紹介します。
- JSファイルを1つのJSファイルに結合する
- JSファイルをいくつかのJSファイルに結合する
- JSファイルを分割し、必要なタイミングで読み込むようにする
- JSファイルの中にHTMLファイルを組み込む
- JSファイルの中にCSSファイルを組み込む
- JSファイルの中にBase64化した画像を組み込む
今回のdemo用のファイルをGithubに用意してあります。
こちらのソースと合わせてご覧いただけると良いかと思います。1. JSファイルを1つのJSファイルに結合する[demo1]
上のソースの中の[demo1]をご覧ください。
https://github.com/frontainer/ligblog-sample/tree/master/webpack/demo1/
以下のような分割されたJavaScriptを用意します。
app.js
var sub = require('./sub'); sub('test');
sub.js
module.exports = function(str) { alert(str); };
以下のコマンドでJSを結合します。
webpack app.js dist/app.js
distディレクトリにapp.jsができました。
これを実行するとalertでtestと表示されます。このようにパッと結合だけ使うだけならすごく簡単です。
2. JSファイルをいくつかのJSファイルに結合する[demo2]
次に[demo2]をご覧ください。
https://github.com/frontainer/ligblog-sample/tree/master/webpack/demo2/
今度は結合させつつ、複数ファイルを出力したいと思います。
想定としては以下のような感じです。dist/app.js <- サイト全体で使う dist/index.js <- indexページで使う dist/detail.js <- detailページで使う
全体で使うものと、個別のページで使うものと分けたものを作ります。
以下のようなスクリプトを用意します。
app.js
var sub = require('./sub'); sub('app');
index.js
var sub = require('./sub'); sub(‘index');
detail.js
var sub = require('./sub'); sub(‘detail');
sub.js
module.exports = function(str) { alert(str); };
WebPackでは毎回コマンドでオプションを指定しなくても良いように、設定ファイルを作っておくことができます。
スクリプトと同じ階層にwebpack.config.jsという名前で、以下のように記述したものを配置します。
webpack.config.js
module.exports = { entry: { app: './app.js', index: './index.js', detail: './detail.js' }, output: { path: __dirname + "/dist", filename: "[name].js" } };
そして以下のコマンドを実行します。
webpack
WebPackコマンドを実行したときに、実行時のディレクトリにwebpack.config.jsがあった場合には自動的に読みに行ってくれます。
これで複数のJSファイルが出力されます。
dist/app.js dist/index.js dist/detail.js
実行結果
このように複数のファイルを同時に出力できるところもWebPackの魅力の1つです。しかし、出力されたファイルの中を見てみるとapp.jsとindex.js、detail.jsには同じwebpackで出力された関数が記述されています。
/******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) /******/ return installedModules[moduleId].exports; /******/ /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ exports: {}, /******/ id: moduleId, /******/ loaded: false /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.loaded = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(0); /******/ }) /************************************************************************/
<script src="dist/app.js"></script> <script src="dist/index.js"></script>
このように使いたいのに、ちょっと無駄ですよね。
そこでプラグインを使ってこの共通関数を出力する先を限定してみます。webpack.config.jsを以下のように書き換えます。
var webpack = require('webpack'); module.exports = { entry: { app: './app.js', index: './index.js', detail: './detail.js' }, output: { path: __dirname + "/dist", filename: "[name].js"; }, plugins: [ new webpack.optimize.CommonsChunkPlugin('app','app.js') ] };
このままではWebPackがないよと怒られてしまいますので、現在のディレクトリにWebPackをインストールします。
npm install webpack —save-dev
そして再びWebPackコマンドを実行すると…
index.js
webpackJsonp([2],[ /* 0 */ /***/ function(module, exports, __webpack_require__) { var sub = __webpack_require__(1); sub('index'); /***/ } ]);
app.jsにだけwebpack関数が記述され、index.jsからは消えました。
webpack.config.jsにはたくさん設定項目があります。
すべて把握するまで時間がかかるかと思いますが、soucemapを出力したりエイリアスを設定したりなど便利な設定がたくさんあるのでぜひ目を通してみてください。Webpack - configuration
http://webpack.github.io/docs/configuration.html3. JSファイルを分割し、必要なタイミングで読み込むようにする[demo3]
次に[demo3]をご覧ください。
https://github.com/frontainer/ligblog-sample/tree/master/webpack/demo3/
さて、ここまではすべて1つのファイルにしてきましたが、大きなシステムになってくるとサイズがどんどん大きくなってしまい、初期のレスポンスが悪くなってしまいます。
そこで今度は分割したJSファイルを呼ばれたときに非同期で読み込んで実行するようにしたいと思います。(要するにrequire.js的に使うということです)
以下のようなスクリプトを用意します。
app.js
window.setTimeout(function() { require.ensure([],function(sub) { var sub = require('./sub'); sub('test'); }); },3000);
sub.js
module.exports = function(str) { alert(str); };
webpack.config.jsはこのようにしておきます。
webpack.config.js
module.exports = { entry: './app.js', output: { path: __dirname + "/dist", filename: "bundle.js" } };
WebPackコマンドを実行します。
webpack
3秒後にtestとalertが表示されます。demo1との違いは、Chrome Dev Toolを開いてみると分かります。
demo1
demo3
若干分かりにくいですが、3秒後の処理を実行したタイミングでスクリプトが読み込まれています。
WebPackでは結合するところと分割するところを分けて出力することができるので、パフォーマンスの調整などが非常にやりやすくなっています。4. JSファイルの中にHTMLファイルを組み込む[demo4]
次に[demo4]をご覧ください。
https://github.com/frontainer/ligblog-sample/tree/master/webpack/demo4/
今度はWebPackのloaderを使って、JS内にHTMLを埋め込んでみます。HTMLを読み込むためにhtml-loaderを使います。
- html-loader
https://www.npmjs.com/package/html-loader
npm install html-loader —save-dev
html-loaderをインストールしたらwebpack.config.jsを以下のように修正します。
module.exports = { entry: './app.js', output: { path: __dirname + "/dist", filename: "bundle.js"; }, module: { loaders: [ { test: /\.html$/, loader: 'html-loader' }, ] }, };
続いて埋め込みたいHTMLを作成します。
今回はこんな感じのものを用意しました。<div> <header> <h1>title</h1> <nav> <ul> <li><a href="">a</a></li> <li><a href="">b</a></li> <li><a href="">c</a></li> <li><a href="">d</a></li> <li><a href="">e</a></li> </ul> </nav> </header> <section> <h2>sub title</h2> <p>content</p> </section> <footer> copyright </footer> </div>
そしてapp.jsに以下のように記述します。
app.js
var html = require('./static.html'); document.body.innerHTML = html;
記述したらWebPackコマンドを実行します。
webpack
すると、以下のように実行されます。
生成されたJSを見ると、HTMLが文字列にされているのが分かります。
module.exports = "<div>\n <header>\n <h1>title</h1>\n <nav>\n <ul>\n <li><a href=\"\"></a></li>\n <li><a href=\"\"></a></li>\n <li><a href=\"\"></a></li>\n <li><a href=\"\"></a></li>\n <li><a href=\"\"></a></li>\n </ul>\n </nav>\n </header>\n <section>\n <h2>sub title</h2>\n <p>content</p>\n </section>\n <footer>\n copyright\n </footer>\n</div>";
これによって外部のHTMLとして編集し、 WebPackを使ってJSに埋め込んでテンプレートなどとして使うことができます。
Emmetやエディタのハイライトが活用できますし、unserscore.jsのテンプレートをJSに直接埋め込むこともできて大変便利です。AngularJSを使っている場合にはdirectiveのtemplateに渡したり$templateCacheに渡したりが簡単になります。
5. JSファイルの中にCSSファイルを組み込む[demo5]
次に[demo5]をご覧ください。
https://github.com/frontainer/ligblog-sample/tree/master/webpack/demo5/
次はHTMLだけでなくCSSも埋め込んでみます。今度はcss-loaderとstyle-loaderを使います。
- css-loader
https://www.npmjs.com/package/css-loader - style-loader
https://www.npmjs.com/package/style-loader
npm install css-loader —save-dev npm install style-loader —save-dev
webpack.config.jsを以下のように修正します。
module.exports = { entry: './app.js', output: { path: __dirname + "/dist", filename: "bundle.js"; }, module: { loaders: [ { test: /\.html$/, loader: 'html-loader' }, { test: /\.css$/, loaders: ['style-loader','css-loader'] }, ] }, };
埋め込みたいCSSも用意しましょう。
header { border-bottom: 1px solid #ccc; } nav ul { list-style: none; padding: 0; } nav ul li { float: left; } section { clear: both; } footer { font-size: 10px; }
そしてapp.jsに以下のように記述します。
app.js
require('./style.css'); var html = require('./static.html'); document.body.innerHTML = html;
記述したらWebPackコマンドを実行します。
webpack
すると、以下のように実行されます。
埋め込まれたHTMLにスタイルがあたっているのが分かります。生成されたJSを見ると、CSSも文字列にされています。
exports.push([module.id, "header {\n border-bottom: 1px solid #ccc;\n}\nnav ul {\n list-style: none;\n padding: 0;\n}\nnav ul li {\n float: left;\n}\nsection {\n clear: both;\n}\nfooter {\n font-size: 10px;\n}", ""]);
css-loaderでcssを文字列としてJSに埋め込み、style-loaderを使うことでhead内にstyleとして出力されるようになります。
これを活用するとHTML/CSS/JSを1セットにしたコンポーネントを作ることができます。
コンポーネントを組み合わせて作るようなコンテンツで力を発揮する機能です。IDをうまく振ればWebComponentsライクに使うこともできると思います。6. JSファイルの中にBase64化した画像を組み込む[demo6]
次に[demo6]をご覧ください。
https://github.com/frontainer/ligblog-sample/tree/master/webpack/demo6/
最後に画像もJSファイルに埋め込んでみます。画像ファイルを埋め込むためにurl-loaderを使います。
- url-loader
https://www.npmjs.com/package/url-loader
npm install url-loader —save-dev
webpack.config.jsを以下のように修正します。
module.exports = {
entry: './app.js',
output: {
path: __dirname + "/dist",
filename: "bundle.js"
},
module: {
loaders: [
{ test: /\.html$/, loader: 'html-loader' },
{ test: /\.css$/, loaders: ['style-loader','css-loader'] },
{ test: /\.png$/, loaders: ['url-loader'] },
]
},
};
埋め込みたい画像を用意します。
そしてapp.jsに以下のように記述します。
require('./style.css');
var html = require('./static.html');
document.body.innerHTML = html;
var img = new Image();
img.src = require('./image.png');
document.body.appendChild(img);
記述したらWebPackコマンドを実行します。
webpack
生成されたJSを見ると、Base64化された画像が文字列として埋め込まれています。
これをimg要素のsrcに渡してあげると表示させることができます。module.exports = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAYAAAB5fY51AAAeIElEQVR42u2dDa8d1XWG+Qf+CfkJ+QmRDZQSotzKKRSwwKEIDIVwHVs1Hw6+JCStTB2uKHETmhbXoUVqG+eq(略)"
DevToolで見てみると画像がリクエストを投げずに表示されているのが分かります。
画像の埋め込みは使う場面を選びますが、例えばローディング画像など決まったものや要素を覆うカバー画像などを埋め込んでしまい、HTTPリクエストを減らすなどの活用が考えられます。
まとめ
いかがでしたでしょうか。
今回紹介した機能以外にもJSを圧縮したり、画像を圧縮したり、CoffeeScriptやTypeScriptをコンパイルしてみたり、React.jsを使った場合にはjsxをコンパイルしたりなど、多くのことができます。
多機能ゆえの学習コストはかかりますが、これを使いこなしてあらゆる状況に対応していけると面白いのではないでしょうか。
ぜひ試してみてください。
Enjoy WebPack!!
【先生のフロントエンド講座】
※ エンジニアがいい感じにフロントエンド開発を爆速化できる環境構築の手順
※ フロントエンド開発を裏から支えるデバッグアプリケーション4選
※ Gulp.js入門 – コーディングを10倍速くする環境を作る方法まとめ
※ LIGのソースコードレビュー会で使用している共有ツールとライブラリの活用方法の紹介
※ AngularJSで作成したシングルページアプリケーションをGoogleアナリティクスでトラッキングさせる方法
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。