JavaScript
CoffeeScript
stylus
ECMAScript6
webpack

モジュール管理、だけじゃない-Webpack入門 〜 JSおくのほそ道 #029

More than 3 years have passed since last update.

こんにちは、ほそ道です。

前回のRequireJSに続きブラウザ環境におけるモジュール管理について。
今回はWebpackを用いてソリッドなサンプルを作りつつ、本質的な所を抽出できればと思います。

目次はこちら

Webpackはじめの一歩

彼是言う前にまずやってみよう!

下記のようなプロジェクト構成を作ります。

プロジェクト構成
├── index.html
└── src
    ├── Man.js
    └── entry.js

各ファイルの内容は下記のようにしています。

  • htmlファイルのbody部は下記のような感じです。最終的にはapp.jsが生成されるのでこれを参照させます。
index.html
<body>
    <script src="build/app.js"></script>
</body>
  • 次にモジュール管理らしくモジュールを作ります。コンストラクタをexportして外部から参照可能とします。
src/Man.js
module.exports = Man = function(name, age) {
  this.name = name;
  this.age = age;
};
Man.prototype.greet = function () {
  return 'Hello, my name is ' + this.name + ', I\'m ' + this.age + ' years old';
};
  • 最後にモジュールを利用するエントリーポイントJSです。requireメソッドで取り込みます。
src/entry.js
var Man = require('./Man');
var m = new Man('Taro', 30);
console.log(m.greet());

ではプロジェクト化とwebpackをインストールしましょう。
コンソールから下記コマンドを叩きます。

npm init
npm i -D webpack

これで準備が整いました。そのままコンソールからwebpackを実行しましょう。

webpack src/entry.js build/app.js

下記のようなメッセージが表示されbuild/app.jsが生成されたら成功です。

Hash: 12508ed1cbe266e5028b
Version: webpack 1.9.6
Time: 43ms
 Asset     Size  Chunks             Chunk Names
app.js  1.77 kB       0  [emitted]  main
   [0] ./src/entry.js 80 bytes {0} [built]
   [1] ./src/Man.js 196 bytes {0} [built]

それではブラウザからindex.htmlを開いてみましょう。
Manコンストラクタのgreetメソッドは実行できたでしょうか?

スクリーンショット 2015-05-16 15.42.45.png

デビュー戦としてはこんな感じですね。

webpackコマンドで何が起きたのか?

コマンドの実行

さて、webpack src/entry.js build/app.jsコマンドの実行によってbuild/app.jsが生成されました。
第一引数はエントリーポイント、第二引数は出力先です。
この時、エントリーポイントがrequireしているモジュールであるsrc/Man.jsも取り込んでくれたわけです。

生成されたJSファイル

生成されたbuild/app.jsの中も見てみましょう。
ファイル自体は長いので自分で書いたコードが返還された部分だけ抜粋します。

/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {

    var Man = __webpack_require__(1);
    var m = new Man('Taro', 30);
    console.log(m.greet());

/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {

    module.exports = function (name, age) {
      this.name = name;
      this.age = age;
      this.greet = function () {
        return 'Hello, my name is ' + this.name + ', I\'m ' + this.age + ' years old';
      }
    };

/***/ }
/******/ ]);

こんな感じです。複数のファイルに分割した内容はbuild/app.jsにオールインワンで含まれることになります。
前回取り上げたRequireJSは必要となるモジュールはscriptタグ挿入という形で呼び出されていたので、RequireJSとwebpackではモジュールロードのアプローチとしては異なるやり方を採っていることがわかりますね。
しかし!webpackの本領発揮はここからであります!

Loader

AltJSやES6構文をLoad!

次にLoaderを紹介します。Loaderを使用するとJavaScript以外の言語もブラウザで利用可能なJavaScriptにコンパイルし、app.jsに組み込むことができるようになります。
手始めに必要となるLoaderをプロジェクトに取り込んでおきましょう。

npm i -D coffee-loader 
npm i -D babel-loader

さて、プロジェクトにモジュールファイルをふやしてみましょう。
src/Cat.coffeeDog.jsという2つのファイルを作成します。

├── index.html
├── package.json
└── src
    ├── Cat.coffee
    ├── Dog.js
    ├── Man.js
    └── entry.js
  • まずはCoffeeScriptでCatモジュールです
src/Cat.coffee
module.exports = class Cat
  constructor: (@name) ->
  move: (meters) ->
    return @name + " moved #{meters}m."
  • 次にES6構文を使ってDogモジュールです
src/Dog.js
export default class {
  constructor(name) {
    this.name = name;
  }
  eat(food) {
    return `${this.name} eats ${food}.`;
  }
}

エントリーポイントからモジュールを呼び出します。
この時requireメソッドの引数を対応する言語に合わせたローダ名!ファイル名とするのがミソであります。

src/entry.js
var Man = require('./Man'),
  Cat = require('coffee!./Cat.coffee'),
  Dog = require('babel!./Dog');

var m = new Man('Taro', 30),
  c = new Cat('Tom'),
  d = new Dog('John');

console.log(m.greet());
console.log(c.move(10));
console.log(d.eat('beef'));

準備OKです。最初と同様にwebpackコマンドを叩いてみましょう。

webpack src/entry.js build/app.js

build/app.jsが更新されたらブラウザで確認します。
下記のように出ていますか?
スクリーンショット 2015-05-16 16.44.39.png

Styleもロード!

次はスタイルも取り込んでしまいましょう。
なんとこれもbuild/app.jsの中に取り込まれることになります。
それでは使用するLoaderを準備しましょう。

npm i -D style-loader   
npm i -D css-loader  
npm i -D stylus-loader    

次にプロジェクトにstylesディレクトリを作ってファイルを設置します。

プロジェクト構成3
├── index.html
├── package.json
├── src
│   ├── Cat.coffee
│   ├── Dog.js
│   ├── Man.js
│   └── entry.js
└── styles
    ├── style.css
    └── style.styl
  • index.htmlにul要素を追加
index.html
<html>
<head></head>
<body>
<ul id="list">
  <li>one,</li>
  <li>two,</li>
  <li>tree,</li>
  <li>four,</li>
  <li>five!!!</li>
</ul>
<script src="build/app.js"></script>
</body>
</html>
  • ノーマルなCSS
style.css
ul#list {
  background-color: midnightblue;
}
  • Stylus形式
style.styl
ul#list
  for num in 1..5
    if num % 2 == 0
      li:nth-child({num})
        color skyblue
    else
      li:nth-child({num})
        color gold
    li:nth-child({num})
      font-size num * 11px

それでは例によってエントリーポイントにこれらのファイルを取り込ませます。
今度はloaderの記述形式が少し異なります。ローダ名!が連結しますが、これは右から左に順にコンパイルしていくというイメージです。
style-loaderは少し特殊でスタイルをHTMLのheaderに追加する役割を持っています。

src/entry.js
require('style!css!../styles/style.css');  // 追加
require('style!css!stylus!../styles/style.styl');  // 追加

var Man = require('./Man'),
  Cat = require('coffee!./Cat.coffee'),
  Dog = require('babel!./Dog');

var m = new Man('Taro', 30),
  c = new Cat('Tom'),
  d = new Dog('John');

console.log(m.greet());
console.log(c.move(10));
console.log(d.eat('beef'));

もうコマンドは書きませんがwebpackコマンドを叩きます。
うまくHTMLに反映できたでしょうか?
スクリーンショット 2015-05-16 17.44.32.png

SourceMap

さて、一通りのローダも見てきましたがここまでで生成されたapp.jsを見ると500行近くあります。
せっかくモジュールで機能分割したのに「デバッグしづらくね!?」という疑問が湧いてきます。
そこでハイ、SourceMapを使いましょう!

やり方は簡単です。webpackコマンドに下記のオプションをつけましょう

webpack --devtool source-map src/entry.js build/app.js   

build/app.js.mapというファイルが生成されます。このファイルはソースファイルとアウトプットファイルの関連を保持しています。
それではブラウザでHTMLを開いて開発ツールを開いてみましょう。

スクリーンショット 2015-05-16 17.59.48.png

ドメイン一覧にwebpack://という項目が現れ取り込んだファイル群が出てきます。
ES6やCoffee、Stylusコードもコンパイル前の状態を参照できます。

ブレークポイントもバッチリ効きます。
スクリーンショット 2015-05-16 18.01.06.png

Configファイル

最後に、webpackは今回紹介した以外にもたくさんの機能を持っており、それらを駆使していくとコマンドオプションやパラメータがどんどん長くなっていってしまいます。
そこで設定ファイルを作ってここまで出力方法を固定化するためにConfigファイルを使用します。
プロジェクトのルートにwebpack.config.jsを作成しましょう。

webpack.config.js
module.exports = {
  entry: {
    app: ["./src/entry.js"]
  },
  output: {
    filename: "build/[name].js"
  },
  // source-mapを出力
  devtool: "#source-map",
  module: {
    // ローダ設定
    loaders: [
      {test: /\.js$/, loader: "babel"},
      {test: /\.coffee$/, loader: "coffee"},
      {test: /\.css$/, loader: "style!css"},
      {test: /\.styl$/, loader: "style!css!stylus"}
    ]
  },
  resolve: {
    // requireやimport時の拡張子を省略
    extensions: ['', '.js', '.jsx', '.coffee', '.css', '.styl']
  },
};

entryプロパティはエントリーファイルを、outputプロパティは出力ファイルを表すといった感じです。
今回はSourceMapの出力と、ファイル拡張子とLoaderの紐付けを指示しています。
補足ですがLoaderをここで指定できるということはrequireメソッド内でのLoader指定も省略することができます。気が利いておりますね。

これでwebpack実行コマンドは常に同じものを叩くことができます。
下記で実行しましょう。

 webpack --config webpack.config.js

webpackのここが素晴らしい

改めてほそ道的実開発のおいてのメリット/デメリットについてまとめてみます。


  • モジュール化
    • ブラウザアプリにおいて利用モジュール、被利用モジュールの関係、モジュールの責務を明確にしやすい。
    • Loaderにより異なる言語のモジュールをJSコンパイルできるのでメイン言語の異なる過去のプロジェクト資産も使える。

  • コンパクト化
    • HTML内のタグを整理することができ、アプリケーションに必要なリソースはscriptタグ一つに集約できる。
    • 画像や動画などのリソースファイルは一つにまとまっているので圧倒的にファイル取得通信時間が短縮できる。
    • エントリーポイントごとに複数の出力ファイルを吐くこともできるのでページごとに必要最低限のリソースにまとめることができる。

  • 生産性向上
    • 複数の開発者がいてもConfig整備によりビルドの方法を一本化できるので同じ環境を再現しやすい
    • GruntやGulpとの組み合わせもできる。(ほそ道は実開発ではgulp-webpackというライブラリ使ってます。)
    • プラグインが必要なものもあるがWebサーバ起動、ホットロード、ミニファイもでき、開発生産性が向上する。Gulp/Gruntのタスクをかなり巻き取る事ができるのでGrunt/Gulpの設定をシンプルにできる。(どういう役割分担をさせるかも腕の見せ所ですね)

今回は以上です。