はじめまして。
アメーバ事業本部 クロスイノベーション室のグンタ(@gunta85)と申します。

今回はフロントエンド開発用の、
ありとあらゆる依存関係を簡単に解決してくれる、
WebPackというスグレモノをご紹介します。

導入経緯

大きなアプリを開発するにあたり、依存関係を解決してくれるものを探していました。

その中で、RequireJSBrowserifyComponentなども候補に挙がりましたが、
諸々の理由でWebPackを選びました。

特徴の違いについては「WebPack vs Browserify」等で検索してみると良いかと思います。

WebPackのメリット

・どのモジュールシステムでも使える

  • AMD
  • CommonJS
  • UMD
  • ES6
  • Component
  • ...

・どのパッケージマネジャーでも使える

  • NPM
  • Bower
  • Component
  • ...

・CSS、画像、フォントなどがrequireできる

・CommonJSシンタックスでモジュールをインポート

var _ = require('lodash');

_.forEach([1, 2, 3], function(num){
});

・Loaders(変換する便利な機能が盛りだくさん)

  • CSSプリプロセッサー
  • 画像をbase64にエンコード
  • ES6→ES5にモジュール変換
  • ...

・Loaderの書き方

var $ = require('jquery');

// 画像をbase64にエンコードし、読み込みます
require('url!./image.jpg');

// CSSをテキストとして読み込み、headに入れる
// 複数のLoaderをチェーンすることは可能
require('style!css!./style.css');

// モジュールシステムを使わないJSファイルも
// requireをし、ローカル変数にアサインされます
require('imports?hoge=lib!lib.js');

・Configファイル

インラインのLoaderを毎回書くのは大変なので、
configファイルに書きます。
{
    module: {
        loaders: [
            { test: /\.png$/, loader: 'url' },
            { test: /\.js$/, loader: 'es6-loader' },
            { test: /\.css$/, loader: 'style!css' }
        ]
    }
}

・モジュールのパス解析

resolve: {
    moduleDirectories: ['node_modules', 'bower_components']
}

・長いパスもスッキリ書ける

下記を書くより...

./../../../../library.js
./../../lodash/dist/lodash.js

エイリアスを書くと...

resolve: {
    alias: {
        library: '絶対パス/../../library',
        lodash: 'lodash/dist/lodash.compat.js'
    }
}

...すっきりインポートが出来ます。

var lib = require('library');
var _ = require('lodash');

・好きなビルドシステムとの連携が簡単

  • Grunt
  • Gulp
  • Make
  • ...

・賢いビルド出力

  • チャンクファイルを等分布することが可能
  • 「重いファイルのみ」などのオンデマンド読み込みが可能
  • ルートによってセグメントに分割が可能
  • 共通モジュールの摘出が可能
  • ...

・速いビルド

  • インクレメンタルビルド
  • 変更されたファイルのみが再ビルドされる

・デバッグが簡単

  • ソースマップなどが出力できます

・懸念点

  • 日本語のドキュメントはまだ無い
  • 基本的には何でもできる
ということで...

WebPackの導入

さっそくインストールしてみましょう。
公式のサイトはこちらです。

インストール


次のコマンドをコマンドラインで叩きます。
$ npm install webpack -g


使用方法

WebPackはとても簡単に使用することが出来ます。
$ webpack <input> <output>
  • <input>はメインのJSファイル名。

  • <output>はビルドされるJSのファイル名。

では実際に実用的なコードを例にして説明していきたいと思います。

まずはサンプルコードを落とします。
$ git clone https://github.com/gunta/webpack-samples-1pixel.git

そして依存ライブラリをインストールします。
$ npm install


【第1部】CommonJSの簡単なサンプル


$ cd 01-commonjs

フォルダの中に下記ファイルがあります。
<math.js>
exports.add = function (x, y) {
  return x + y;
};
<main.js>
var math = require('./math');
alert('1 + 2 = ' + math.add(1, 2));

ビルドをすると、出力先にbundle.jsが生成されます。
$ webpack main.js bundle.js


そしてmain.htmlを開くと、
$ open main.html
きちんとアラートが表示されることが確認できます。


【第2部】Bootstrapを使ったサンプル

$ cd 02-bootstrap

毎度コマンドラインから諸々のフラグを付けるのは大変なので、
Configファイルに書きます。

Configファイル名はwebpack.config.jsにしておくと、
下記コマンドを叩くだけで設定が読み込まれビルドが実行されます。
$ webpack


今回のソースコードは非常にシンプルです。
<main.js>
require('bootstrap/dist/css/bootstrap.css');

こちらのbootstrap.cssのファイルは、
node_modulesの中にnpm installでインストールしたものですが、
bowerでも手動でも、何でもOKです。
<webpack.config.js>
module.exports = {
  entry: './main.js',
  output: {
    filename: 'bundle.js'
  },
  module: {
    loaders: [
        
      // 拡張子がCSSの場合はCSSのLoaderを採用
      { test: /\.css$/, loader: 'style!css' },
      
      // bootstrap.cssの中に使うWebFontを(デフォルトで)base64エンコードされます
      { test: /\.svg$/, loader: 'url-loader?mimetype=image/svg+xml' },
      { test: /\.woff$/, loader: 'url-loader?mimetype=application/font-woff' },
      { test: /\.eot$/, loader: 'url-loader?mimetype=application/font-woff' },
      { test: /\.ttf$/, loader: 'url-loader?mimetype=application/font-woff' }
    ]
  }
};

下記コマンドで確認できます。
$ open main.html


【第3部】AngularJSのサンプル

こちらは最新の公式AngularJSフォルダ設計 ベスト・プラクティス
ベースにしたサンプルです。

こういう構成にすると、
簡単にコンポーネント化できます。
$ cd 03-angular

<main.js>
var angular = require('angular');
angular.module('app', [
  require('./tabs').name,
  require('./pane').name
]);
<webpack.config.js>
module.exports = {
  entry: './main.js',
  output: {
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      { test: /\.html$/, loader: 'html' }
    ]
  }
};
<tabs/index.js>
module.exports = angular.module('app.tabs', [])
  .directive('tabs', require('./tabs-directive'));
<tabs/tabs-controller.js>
module.exports = function ($scope) {
  var panes = $scope.panes = [];

  $scope.select = function (pane) {
    angular.forEach(panes, function (pane) {
      pane.selected = false;
    });
    pane.selected = true;
  };

  this.addPane = function (pane) {
    if (panes.length == 0) {
      $scope.select(pane);
    }
    panes.push(pane);
  }
};
<tabs/tabs-directive.js>
module.exports = function () {
  return {
    restrict: 'E',
    transclude: true,
    scope: {},
    controller: require('./tabs-controller'),
    template: require('./tabs-template.html'),
    replace: true
  }
};

このように、.htmlのテンプレートを簡単に埋め込むことができるようになりました。

ビルドをすると、
$ webpack

ブラウザで確認ができます。
$ open main.html


【第4部】CoffeeScriptやLESS、Jadeを使ったサンプル

$ cd 04-coffee-less-jade

<main.coffee>
Colors = require "./colors/colors.coffee"
...
colors = new Colors()
<colors/colors.coffee>
require './colors.less'
template = require './colors.jade'

class @Colors
  getTemplate: -> template

module.exports = @Colors
<colors/colors.less>
@import "base";
LESSファイルのインポートまで解析してくれます。

ビルドをします。
$ webpack

ブラウザで確認できます。
$ open main.html


【第5部】コードスプリット

$ cd 05-code-splitting
こちらのサンプルでは、出力先が2つに分割されます。

1つ目のバンドルは、初期に読み込まれ、
2つ目のバンドルは、require.ensure()を通じて
オンデマンドで必要になった時に読み込まれます。

<main.js>
var a = require('a');
var b = require('b');

require.ensure(['c'], function (require) {
  require('b');
  var d = require('d');
});


ビルドします。
$ webpack

そして開くと、
$ open main.html

`a  b  d`が表示されます。

なぜ、cのモジュールが呼ばれてないのでしょうか?

理由は、require.ensureでは、ファイルの読み込み完了は保証されるのですが、
中身のコードが評価されないからです。

これはCommonJSの仕様で、読み込みと評価のタイミングは自由に選べます。

また、WebPackは、AMDのシンタックスにも対応しています。
require(["a"], function(a) {
    // こちらはAMD仕様なので、aモジュールが読み込みも評価もされます
});

個人的にはCommonJSのシンタックスの方が好きです。


【第6部】production/develが最適化されたサンプル

$ cd 06-production

こちらは一番リアルに近いサンプルなので、
じっくり中身を確認してみてください。

色んな機能がフィッチャーされます。
  • Gruntによるビルド
  • Gulpによるビルド
  • Grunt/Gulp無しによるビルド
  • 複数ページの複数ファイル出力
  • ウォッチもリロードもしてくれるdevサーバー
  • Minifyもモジュールの重複排除もしてくれる設定
  • jQuery/Bootstrapの読み込み
  • グローバル変数にjQuery変数を導入
  • 出力チャンクのネーミング

ビルドの仕方は簡単です。

Grunt

Gruntでdevel版をビルドしたい場合
$ grunt 
全部のモジュールがビルドされ、develサーバーが立ち上がります。

ファイルを変更すると、自動的にページがリロードされます。

ちなみに、物理的にファイルが出力されず
オンメモリでビルドされるので、速いです。

production版をビルドします。
$ grunt build
dist/フォルダにminifyされたファイルが出力されます。

Gulp

こちらはgruntと同様です。

devel版をビルドします。
$ gulp

production版をビルドします。
$ gulp build


【第7部】EcmaScript 6のサンプル

こちらは未来からきたES6の書き方、
ES6モジュールの書き方ができるサンプルです。

ビルドすると現在のES5に変換されますので、
未来の書き方を今すぐ使えます。

これはAngular 2.0でも採用されている方式です。

$ cd 07-es6

まずはBowerのモジュールをインストールします。
$ bower install

そしてビルドします。
$ webpack

中身も未来的です。
<main.js>
// Style modules
require('todomvc-common/base.css')

// ES5 modules
var $ = require('jquery')
var Backbone = require('backbone')

// ES6 modules
import {AppView, Filters} from './todo-app';

// Document ready
$(() => {
  new AppView()
  new Filters()
  Backbone.history.start()
})
<todo-app.js>
// ES5 modules
var $ = require('jquery')
var _ = require('lodash')
var Backbone = require('backbone')
require('backbone.localStorage')

const { Model, View, Collection, Router, LocalStorage } = Backbone
const ENTER_KEY = 13
let TodoFilter = ''

// Todo Model class
class Todo extends Model {

  defaults() {
    return {
      title: '',
      completed: false
    }
  }

  toggle() {
    this.save({
      completed: !this.get('completed')
    })
  }
}

...
export { AppView, Filters }


終わりに

もっとサンプルがほしい!という方は、こちらにたくさんあります。

検索可能な限りでは、今回は日本語初のWebPackの記事になりますが、
今担当しているプロジェクトで6ヶ月前から導入していますし、
Instagram.comでの導入事例もあります。

以上、WebPackのお話をさせて頂きました。
最後までお付き合い頂き、ありがとうございました!