CLOVER🍀

That was when it all began.

webpackを使ってJavaScriptのビルド/minify(+Bower)

最近のフロントエンド事情には全然詳しくないのですが、ちょっと以下のようなことをやろうかなと思いまして。

  • 複数のJavaScriptをまとめたい
  • まとめたJavaScriptはminifyしたい
  • SourceMapも作成したい
  • Bowerを使って依存関係も解決したい

Bowerで解決しようとしているのは、他に手段をよく知らないのとクライアントサイドのJavaScriptの依存関係を書くのは、Bowerが一般的なのかなと思いまして。

で、こういうことをやろうとした時、webpackとBrowserifyが目につくのですが、今回はwebpackでやってみました。

webpack module bundler

以降に、やったことを書いていきます。なお、Bowerで引き込むライブラリは、jQueryとします。

あとで、Browseriry版も書きました。

Broserifyを使ってJavaScriptのビルド/minify(+Bower) - CLOVER

※このエントリ公開後に、@makingさんから「Bowerいらないのでは?」というツッコミをいただいたので、最後にBowerなし版を追記しました

セットアップ

まずは、以下のコマンドを実行。

$ npm init
$ npm install -g gulp webpack bower
$ npm install gulp gulp-util bower webpack --save-dev
$ bower init
$ bower install jquery --save

バージョン情報。

$ node --version
v4.1.0
$ npm --version
2.14.3
$ bower --version
1.5.2

参考)
http://webpack.github.io/docs/installation.html

webpackを使い倒す - Thujikun blog

対象のスクリプトとHTML

動作確認用のソースコードとして、内容はちょっと微妙ですが、このようなものを用意。

エントリポイントです。
scripts/main.js

var $ = require("jquery");
var init = require("./init");
var messages = require("./messages");

init();

$(function() {
    $("#show-message1").on("click", function() {
        $("#message1").text(messages.get("message1"));
    });
    $("#show-message2").on("click", function() {
        $("#message2").text(messages.get("message2"));
    });
});

scripts/init.js

var $ = require("jquery");
module.exports = function() {
    $(function() {
        $("#title").text("webpackとBowerのテストです");
    });
};

scripts/messages.js

module.exports = {
    msgs: {
        "message1": "Hello Workd",
        "message2": "こんにちは、世界"
    },
    get: function(id) {
        return this.msgs[id];
    }
};

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>webpack &amp; Bower テストページ</title>
</head>
<body>
  <h1 id="title"></h1>
  <div>
    <input id="show-message1" type="button" value="メッセージ1を表示">
    <input id="show-message2" type="button" value="メッセージ2を表示">
  </div>
  <span id="message1"></span><br>
  <span id="message2"></span><br>
  <script type="text/javascript" src="dist/scripts/app.js"></script>
</body>
</html>

gulpfile.jsとwebpack.config.jsを書く

続いて、gulpとwebpackの設定を書いていきます。

まずは、gulpの設定から。
gulpfile.js

var gulp = require("gulp");
var gutil = require("gulp-util");
var webpack = require("webpack");
var webpackConfig = require("./webpack.config.js");

gulp.task("build", function(callback) {
    var config = Object.create(webpackConfig);
    webpack(config, function(err, stats) {
        if(err) throw new gutil.PluginError("webpack", err);
        gutil.log("[webpack]", stats.toString({
            // output options
        }));
        callback();
    });
});

gulp.task("default", ["build"]);

ほぼ、ここのまんまです。今回はwebpack-dev-serverは外していますが。

http://webpack.github.io/docs/usage-with-gulp.html

続いて、webpackの設定。

webpack.config.js 
var path = require("path");
var webpack = require("webpack");

module.exports = {
    cache: true,
    entry: {
        app: "./scripts/main.js"  // エントリポイント
    },
    output: {
		path: path.join(__dirname, "dist/scripts"),  // 出力先
		publicPath: "dist/scripts/",  // HTMLなどから参照する時のパス
		filename: "[name].js",  // 生成されるファイルの名前
		chunkFilename: "[chunkhash].js",
        // sourceMapFilename: "[file].map"
    },
    devtool: "#source-map",  // sourcemapの作成
    resolve: {
        root: [path.join(__dirname, "bower_components")]
    },
    plugins: [
        new webpack.ResolverPlugin(
            new webpack.ResolverPlugin.DirectoryDescriptionFilePlugin("bower.json", ["main"])
        ),
        new webpack.optimize.UglifyJsPlugin()  // minify
    ]
};

resolveの部分と、ResolverPluginの中身でBowerとの連携、UglifyJsPluginでminifyの設定となります。

SourceMapを出力するのは、devtoolの指定です。

参考)
http://webpack.github.io/docs/usage-with-bower.html

https://github.com/webpack/webpack-with-common-libs/blob/master/gulpfile.js
https://github.com/webpack/webpack-with-common-libs/blob/master/webpack.config.js

http://webpack.github.io/docs/configuration.html

webpackでbower使って外部ライブラリの依存解決する

webpack + bowerで依存解決がうまくいかない時はaliasをつかうとよいかも - フロントエンドとコーヒー

webpackを使い倒す - Thujikun blog

実行

この状態でビルドしてみます。

$ gulp
[16:08:31] Using gulpfile ~/xxxxx/gulpfile.js
[16:08:31] Starting 'build'...
[16:08:34] [webpack] Hash: 4cc1371f9c4dbb2c89f2
Version: webpack 1.12.2
Time: 3314ms
     Asset     Size  Chunks             Chunk Names
    app.js  86.1 kB       0  [emitted]  app
app.js.map   704 kB       0  [emitted]  app
chunk    {0} app.js, app.js.map (app) 248 kB [rendered]
    [0] ./scripts/main.js 348 bytes {0} [built]
    [1] ./bower_components/jquery/dist/jquery.js 248 kB {0} [built]
    [2] ./scripts/init.js 136 bytes {0} [built]
    [3] ./scripts/messages.js 167 bytes {0} [built]

WARNING in app.js from UglifyJs
Condition always true [./bower_components/jquery/dist/jquery.js:9170,0]
[16:08:34] Finished 'build' after 3.34 s
[16:08:34] Starting 'default'...
[16:08:34] Finished 'default' after 11 μs

ビルド後のファイルと、SourceMapができています、と。

$ ls -l dist/scripts
合計 776
-rw-rw-r-- 1 xxxxx xxxxx  86161  921 16:08 app.js
-rw-rw-r-- 1 xxxxx xxxxx 704141  921 16:08 app.js.map

あとは、一緒に置いていたindex.htmlをブラウザで開いて確認すれば、動作していることが確認できます。

Bowerなし版

このエントリを公開後、@makingさんにこんなツッコミをいただきました。

ふむ、npmだけで完結できる場合はBowerなくてもいいんですか。それならそれでもよいですね。

ただし…

手順としては、構築時のコマンドがこうなって
※Bower関連がなくなり、jQueryがnpm installになりました

$ npm init
$ npm install -g gulp webpack
$ npm install gulp gulp-util webpack jquery --save-dev

gulpfile.jsは変わりません。
gulpfile.js

var gulp = require("gulp");
var gutil = require("gulp-util");
var webpack = require("webpack");
var webpackConfig = require("./webpack.config.js");

gulp.task("build", function(callback) {
    var config = Object.create(webpackConfig);
    webpack(config, function(err, stats) {
        if(err) throw new gutil.PluginError("webpack", err);
        gutil.log("[webpack]", stats.toString({
            // output options
        }));
        callback();
    });
});

gulp.task("default", ["build"]);

webpackの設定から、Bowerまわりをごっそり落としました。
webpack.config.js

var path = require("path");
var webpack = require("webpack");

module.exports = {
    cache: true,
    entry: {
        app: "./scripts/main.js"  // エントリポイント
    },
    output: {
		path: path.join(__dirname, "dist/scripts"),  // 出力先
		publicPath: "dist/scripts/",  // HTMLなどから参照する時のパス
		filename: "[name].js",  // 生成されるファイルの名前
		chunkFilename: "[chunkhash].js",
        // sourceMapFilename: "[file].map"
    },
    devtool: "#source-map",  // sourcemapの作成
    plugins: [
        new webpack.optimize.UglifyJsPlugin()  // minify
    ]
};

@makingさん、ありがとうございました!

Broserifyを使ってJavaScriptのビルド/minify(+Bower)

先ほどwebpackで書いたエントリ、

webpackを使ってJavaScriptのビルド/minify(+Bower) - CLOVER

のBrowserify版です。

やりたいことは同じで、

  • 複数のJavaScriptをまとめたい
  • まとめたJavaScriptはminifyしたい
  • SourceMapも作成したい
  • Bowerを使って依存関係も解決したい

というもの。先ほどはこれをwebpackで書きましたが、今度はBrowserifyで書いてみます。

Browserify

Bowerで引き込むライブラリは、同じくjQueryとします。

※最後に、Bowerなし版も付けています

セットアップ

まずは、以下のコマンドを実行。

$ npm init
$ npm install -g gulp browserify bower
$ npm install gulp bower debowerify browserify bower-resolve gulp-uglify gulp-sourcemaps vinyl-source-stream vinyl-buffer --save-dev
$ bower init
$ bower install jquery --save

バージョン情報。

$ node --version
v4.1.0
$ npm --version
2.14.3
$ bower --version
1.5.2

対象のスクリプトとHTML

基本的に、webpackの時と同じです。ちょっとメッセージが異なるくらい。

エントリポイント。
scripts/main.js

var $ = require("jquery");
var init = require("./init");
var messages = require("./messages");

init();

$(function() {
    $("#show-message1").on("click", function() {
        $("#message1").text(messages.get("message1"));
    });
    $("#show-message2").on("click", function() {
        $("#message2").text(messages.get("message2"));
    });
});

scripts/init.js

var $ = require("jquery");
module.exports = function() {
    $(function() {
        $("#title").text("BrowserifyとBowerのテストです");
    });
};

scripts/messages.js

module.exports = {
    msgs: {
        "message1": "Hello Workd",
        "message2": "こんにちは、世界"
    },
    get: function(id) {
        return this.msgs[id];
    }
};

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Browserify &amp; Bower テストページ</title>
</head>
<body>
  <h1 id="title"></h1>
  <div>
    <input id="show-message1" type="button" value="メッセージ1を表示">
    <input id="show-message2" type="button" value="メッセージ2を表示">
  </div>
  <span id="message1"></span><br>
  <span id="message2"></span><br>
  <script type="text/javascript" src="dist/scripts/app.js"></script>
</body>
</html>

gulpfile.jsを書く

webpackの時とは異なり、こちらはgulpfile.jsだけで完結します。
gulpfile.js

var gulp = require("gulp");
var browserify = require("browserify");
var bowerResolve = require("bower-resolve");
var source = require("vinyl-source-stream");
var buffer = require("vinyl-buffer");
var uglify = require("gulp-uglify");
var sourcemaps = require("gulp-sourcemaps");

gulp.task("javascript", function() {
    bowerResolve.init(function() {
        browserify({
            entries: "./scripts/main.js"  // ビルド対象のファイル
            // debug: true  // trueにすると、//# sourceMappingURL=data:application/json;charset:utf-8;base64,〜でsourcemapが出力される
        })
            .require(bowerResolve("jquery"), { expose: "jquery" }) // Bowerのファイルを、Browserifyでも使えるように
            .bundle()
            .pipe(source("app.js"))  // ビルド後のファイル名
            .pipe(buffer())
            .pipe(sourcemaps.init({loadMaps: true}))  // sourcemap作成
            .pipe(uglify())  // minify
            .pipe(sourcemaps.write("./"))
            .pipe(gulp.dest("./dist/scripts/"));  // 生成先の指定
    });
});

gulp.task("default", ["javascript"]);

BowerとBrowserifyの組み合わせには、bower-resolveというものを使っています。

bower-resolve - npm

init後に

    bowerResolve.init(function() {

以下のように呼び出すことで、BrowserifyでビルドするJavaScript上でもBowerのファイルをrequireできるようになるみたいです。

            .require(bowerResolve("jquery"), { expose: "jquery" }) // Bowerのファイルを、Browserifyでも使えるように

minifyはgulp-uglifyで、SourceMapの作成はgulp-sourcemapsで行っています。

            .pipe(sourcemaps.init({loadMaps: true}))  // sourcemap作成
            .pipe(uglify())  // minify
            .pipe(sourcemaps.write("./"))

で、これらの間をつなぐのにvinyl-source-stream、vinyl-bufferが必要みたいです。

            .pipe(source("app.js"))  // ビルド後のファイル名
            .pipe(buffer())

参考)
Browserify + Uglify2 with sourcemaps

Browserify を使ってみる 2 – Source Map – アカベコマイリ

npmとbrowserifyを使ったクライアントサイドのウェブアプリ開発 | Web Scratch

Browserify + gulp-uglify で JS ファイルを連結 & minify – Gulp で作る Web フロントエンド開発環境 #3 – PSYENCE:MEDIA

Browserify: それはrequire()を使うための魔法の杖

gulpでbrowserifyを使う

Browserify を使ってみる – アカベコマイリ

gulpとBrowserifyでJSをビルドしてみた

Browserify + GulpでクライアントのJavaScript / CoffeeScriptでrequire | EasyRamble

実行

ここまでやったら、ビルド。

$ gulp
[16:41:28] Using gulpfile ~/xxxxx/gulpfile.js
[16:41:28] Starting 'javascript'...
[16:41:28] Finished 'javascript' after 1.23 ms
[16:41:28] Starting 'default'...
[16:41:28] Finished 'default' after 6.25 μs

同じく、ビルド後のファイルとSourceMapができていますと。

$ ls -l dist/scripts
合計 488
-rw-rw-r-- 1 xxxxx xxxxx  85334  921 16:41 app.js
-rw-rw-r-- 1 xxxxx xxxxx 409691  921 16:41 app.js.map

Bowerなし版

こちらも、Bowerなしでやれるそうな。セットアップ時のコマンドが以下の様になります。

$ npm init
$ npm install -g gulp browserify
$ npm install gulp browserify gulp-uglify gulp-sourcemaps vinyl-source-stream vinyl-buffer jquery --save-dev

debowerifyとbower-resolveを落として、jQueryを追加しました。

gulpfile.jsは、もっとすっきりします。
gulpfile.js

var gulp = require("gulp");
var browserify = require("browserify");
var source = require("vinyl-source-stream");
var buffer = require("vinyl-buffer");
var uglify = require("gulp-uglify");
var sourcemaps = require("gulp-sourcemaps");

gulp.task("javascript", function() {
    browserify({
        entries: "./scripts/main.js"  // ビルド対象のファイル
        // debug: true  // trueにすると、//# sourceMappingURL=data:application/json;charset:utf-8;base64,〜でsourcemapが出力される
    })
        .bundle()
        .pipe(source("app.js"))  // ビルド後のファイル名
        .pipe(buffer())
        .pipe(sourcemaps.init({loadMaps: true}))  // sourcemap作成
        .pipe(uglify())  // minify
        .pipe(sourcemaps.write("./"))
        .pipe(gulp.dest("./dist/scripts/"));  // 生成先の指定
});

gulp.task("default", ["javascript"]);

webpackとwebpack-dev-server、html-loaderと合わせて、JsRenderを試す

webpackと組み合わせて、簡単なJavaScriptでのテンプレートエンジンを使いたいと思いまして。

で、ちょっと事情からjQueryが視野に入らざるをえない感じなんですけど、jQueryでテンプレートといえばjQuery Templates plugin…

jQuery Templates plugin

だと思っていたのですが、今は名を変えてJsRender/JsViews/JsObservableとなっているようです。

JsRender/JsViews

JsRender: best-of-breed templating

このあたりの事情、ホントに知らないなぁ…。

で、これとwebpackを合わせて使うことを考えまして、以下のような感じでやってみようかなと。

  • 依存関係はBowerで解決(npmはちょっとムリでした)
  • HTMLテンプレートは、webpackのhtml-loaderを使ってrequireするようにする
  • webpack-dev-serverを利用して、コードに変更があったらオートリロード

ここまでの内容をやってみます。

追記
Bowerなしで、チャレンジする方法も試してみました。

webpackとwebpack-dev-server、html-loaderと合わせて、JsRenderを試す(再) - CLOVER

準備

まずは、下準備から。以下のコマンドを実行します。

$ npm init
$ npm install -g gulp webpack bower
$ npm install --save-dev gulp gulp-util bower webpack webpack-dev-server html-loader
$ bower init
$ bower install --save jquery jsrender

html-loaderというのが、webpackのHTML Loaderです。

先ほどこういうエントリを書きまして

webpackを使ってJavaScriptのビルド/minify(+Bower) - CLOVER

ここでBower要らんのでは?というツッコミをいただいたのですが、JsRenderの場合「npm install」ではちょっと違う物が入ってしまうので、いろいろ試した挙句やめました。

GitHubのREADME.mdでは、以下のコマンドで良いことになっているのですが、

$ npm install jsrender --save

実際に実行すると、すでにメンテナンスが終了している以下のリポジトリのモジュールが引き込まれます。

https://github.com/shtylman/node-jsrender

関連っぽいもの…。

common js compliant lib #15

あまりハマり続けても仕方がないので、Bowerにしました。

コードの用意

動作確認用のコードはこちら。

まずは、HTML。
index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>webpack/webpack-dev-server &amp; JsRender example</title>
</head>
<body>
  <h1>webpack/webpack-dev-serverとJsRenderのサンプル</h1>
  <span>プログラミング言語</span>
  <span id="languages"></span>
  <script type="text/javascript" src="dist/scripts/app.js"></script>
</body>
</html>

エントリポイントとなるJavaScript
src/scripts/main.js

var $ = require("jquery");
window.jQuery = $;

var jsrender = require("jsrender");

//var template = require("html!../templates/list.html");
var template = require("html!html-templates/list.html");

var tmpl = $.templates(template);

var scopes = {
    languages: ["Java", "JavaScript", "CoffeeScript"]
};

$("span#languages").html(tmpl.render(scopes));

JsRenderのオブジェクトをそのまま使ってもよかったんですけど、jQueryと統合するには以下の1行を書くことに…。

window.jQuery = $;

これ、requireの追加引数とかにすればいいのかな?とも思いましたが、なんかダメっぽい。

言い換えれば、グローバルにjQueryがあれば、それに追加しにいくということですね…。

HTMLテンプレート。
src/templates/list.html

<ul>
  {{for languages}}
  <li>{{>}}</li>
  {{/for}}
</ul>

JavaScriptは「src/scripts」、HTMLテンプレートは「src/templates」配下に置いています。

$ find index.html src -type f
index.html
src/templates/list.html
src/scripts/main.js

gulpの設定

gulpfile.jsは、以下のようになりました。
gulpfile.js

var gulp = require("gulp");
var gutil = require("gulp-util");
var webpack = require("webpack");
var WebpackDevServer = require("webpack-dev-server");
var webpackConfig = require("./webpack.config.js");

gulp.task("default", ["webpack-dev-server"]);

gulp.task("webpack", function(callback) {
    var config = Object.create(webpackConfig);
    webpack(config, function(err, stats) {
        if(err) throw new gutil.PluginError("webpack", err);
        gutil.log("[webpack]", stats.toString({
            colors: true
        }));
        callback();
    });
});

gulp.task("webpack-dev-server", function(callback) {
    var config = Object.create(webpackConfig);
    var compiler = webpack(config);

    new WebpackDevServer(compiler, {
        publicPath: "/" + config.output.publicPath,
		stats: {
			colors: true
		}
    }).listen(8080, "localhost", function(err) {
        if(err) throw new gutil.PluginError("webpack-dev-server", err);
        gutil.log("[webpack-dev-server]", "http://localhost:8080/webpack-dev-server/index.html");

        // keep the server alive or continue?
        callback();
    });
});

webpack-dev-serverのリッスンポートは、以下のサンプルに習って8080に。

このあたりを参考にしています。

http://webpack.github.io/docs/usage-with-gulp.html

https://github.com/webpack/webpack-with-common-libs/blob/master/gulpfile.js

webpackの設定

webpackの設定は、このように行いました。

webpack.config.js

var path = require("path");
var webpack = require("webpack");

var currentWorkingDirectory = process.cwd();

module.exports = {
    cache: true,
    entry: {
        app: "./src/scripts/main.js"
    },
    output: {
		path: path.join(__dirname, "dist/scripts"),
		publicPath: "dist/scripts/",
		filename: "[name].js",
		chunkFilename: "[chunkhash].js",
        // sourceMapFilename: "[file].map"
    },
    loaders: [
        { test: "/\.html$/", loader: "html?minize" }
    ],
    resolve: {
        root: [path.join(__dirname, "bower_components")],
        alias: {
            "html-templates": currentWorkingDirectory + "/src/templates"
        }
    },
    devtool: "#source-map",
    plugins: [
        new webpack.ResolverPlugin(
            new webpack.ResolverPlugin.DirectoryDescriptionFilePlugin("bower.json", ["main"])
        ),
        new webpack.DefinePlugin({
			"process.env": {
				// This has effect on the react lib size
				"NODE_ENV": JSON.stringify("production")
			}
		}),
        new webpack.optimize.DedupePlugin(),
        new webpack.optimize.UglifyJsPlugin()
    ]
};

html-loaderに関しては、npmでインストールしたうえで、以下を追加。

    loaders: [
        { test: "/\.html$/", loader: "html?minize" }
    ],

これで、このような記述でテンプレートをロードすることが可能になります。

var template = require("html!../templates/list.html");

が、これだと現在のスクリプトからの相対パスになるので、なんか微妙だなぁと思ってこういう記述に変更。

var template = require("html!html-templates/list.html");

これを動作させるために、aliasを貼りました。

        alias: {
            "html-templates": currentWorkingDirectory + "/src/templates"
        }

currentWorkingDirectoryというのは、以下の内容です。

var currentWorkingDirectory = process.cwd();

はい。

このあたりは、こちらを参考に。

https://github.com/webpack/webpack-with-common-libs/blob/master/webpack.config.js

http://webpack.github.io/docs/using-loaders.html

https://webpack.github.io/docs/list-of-loaders.html

http://webpack.github.io/docs/loaders.html

html loader for webpack

webpackを使い倒す - Thujikun blog

確認

gulpでwebpack-dev-serverを起動するようにしているので、以下のコマンドを実行。

$ gulp

こんな内容が出力されまして…

[23:00:04] Using gulpfile ~/xxxxx/gulpfile.js
[23:00:04] Starting 'webpack-dev-server'...
[23:00:04] [webpack-dev-server] http://localhost:8080/webpack-dev-server/index.html
[23:00:04] Finished 'webpack-dev-server' after 57 ms
[23:00:04] Starting 'default'...
[23:00:04] Finished 'default' after 6.16 μs
Hash: a21d5a9e49bf19575d1b
Version: webpack 1.12.2
Time: 4173ms
     Asset    Size  Chunks             Chunk Names
    app.js  103 kB       0  [emitted]  app
app.js.map  894 kB       0  [emitted]  app
chunk    {0} app.js, app.js.map (app) 318 kB [rendered]
    [0] ./src/scripts/main.js 354 bytes {0} [built]
    [1] ./bower_components/jquery/dist/jquery.js 248 kB {0} [built]
    [2] ./bower_components/jsrender/jsrender.js 69.9 kB {0} [built]
    [3] ./~/html-loader!./src/templates/list.html 68 bytes {0} [built]

WARNING in app.js from UglifyJs
Side effects in initialization of unused variable jsrender [./src/scripts/main.js:4,0]
Condition always true [./bower_components/jquery/dist/jquery.js:9170,0]
Condition always true [./bower_components/jsrender/jsrender.js:19,0]
Dropping unreachable code [./bower_components/jsrender/jsrender.js:21,2]
Side effects in initialization of unused variable $ [./bower_components/jsrender/jsrender.js:17,0]
Dropping unused variable tag_ [./bower_components/jsrender/jsrender.js:450,0]
webpack: bundle is now VALID.

URLがログにも出ていますが、以下にアクセスすると

http://localhost:8080/webpack-dev-server/index.html

こんな感じの結果になります。

なお、gulp起動中にファイルを修正すると、webpack-dev-serverが変更を反映してくれます。

以上!