Grunt + Webpack + Bowerを使ってCoffeeScriptで作ったBackboneアプリケーションをビルドする



# まえがき

* nodeとかbowerとかgruntとかwebpackは適当に入れてください
* JSビギナーなのでおかしなことが書いてあるかもしれません（ツッコミ歓迎）

元々CoffeeScriptで書いたものをGruntでコンパイルしつつ開発していましたが、ファイルを分割する必要があり、分割するにあたってそれぞれの依存関係を解決する必要がありました（速度出てたのでブラウザコンパイラは使わず）。当初、[RequireJS](http://requirejs.org/)を使って分割作業を始めたのですが、`r.js`を使ったコンパイルにハマって作業が滞り、「最近はwebpackなんてのもあるらしいよ」という[元同僚](https://twitter.com/maru_cc)の一声に乗っかりwebpack始めてみました。

システム全体は、Djangoで書かれたウェブアプリケーション、Ajax APIを使用するクライアントサイドアプリケーションという構成になります。

記述言語、コンパイル、結合、コピー、変更監視は次のようなツール構成で実現しています。

| タスク | ツール | 備考 |
|:----:|:----:|:-:|
| 言語        | CoffeeScript / JS | |
| JSの結合     | grunt-webpack (webpack) | |
| 変更監視      | grunt-contrib-watch | webpackにも変更監視あるけど使いません |
| ファイルコピー | grunt-contrib-copy | |



# ディレクトリレイアウト

クライアントサイドアプリケーションのソースコードは`www/src`以下に、bowerでインストールするライブラリ群は`www/components`以下に配置しています。`www/resources`以下が実際の配布物という想定。

```text
.
├── .bowerrc
├── Gruntfile.coffee
├── bower.json
├── package.json
└── www
    ├── components         # bower
    ├── resources
    │   ├── css
    │   └── js
    ├── src
    │   ├── loader.coffee  # アプリケーション実行開始時に読み込む
    │   ├── router.coffee  # Backbone.Router
    │   ├── models
    │   ├── utils
    │   └── views
    └── webpack.config.js  # 今回使わないwebpack
```

# webpackの環境整備
## node modules

必要なnodeモジュールをインストールします。

```bash
npm install coffee-script -g
npm install coffee-loader
npm install grunt -g
npm install webpack -g
npm install bower -g
```

以下は参考のpackage.jsonです。


```json:package.json
{
  "name": "testapp",
  "version": "0.0.1",
  "engines": [
    "~0.11.14"
  ],
  "repository": {
    "type": "git",
    "url": "git://example.com"
  },
  "author": "Mitsukuni Sato",
  "devDependencies": {
    "coffee-loader": "^0.7.2",
    "coffee-script": "~1.8.0",
    "grunt": "^0.4.5",
    "grunt-contrib-clean": "^0.6.0",
    "grunt-contrib-coffee": "^0.12.0",
    "grunt-contrib-cssmin": "^0.10.0",
    "grunt-contrib-uglify": "^0.6.0",
    "grunt-contrib-watch": "^0.6.1",
    "grunt-legacy-util": "^0.2.0",
    "grunt-notify": "^0.4.1",
    "grunt-webpack": "^1.0.8",
    "underscore.string": "^2.3.3",
    "webpack-dev-server": "^1.6.6"
  },
  "dependencies": {
    "bower": "^1.3.12"
  }
}
```

## bower

Backboneに必要なモジュールはbowerからインストールします。`.bowerrc`, `bower.json`を書いてbowerからインストールし。

```json:.bowerrc
{
  "directory": "www/components/",
  "analytics": false,
  "timeout": 30000,
  "registry": {
    "search": [
      "https://bower.herokuapp.com"
    ]
  }
}
```

```json:bower.json
{
  "name": "testapp",
  "version": "0.0.1",
  "homepage": "https://example.com",
  "authors": [
    "Mitsukuni Sato <mitsukuni.sato@example.com>"
  ],
  "license": "MIT",
  "private": true,
  "ignore": [
    "**/.*",
    "node_modules",
    "www/components/",
    "test",
    "tests"
  ],
  "dependencies": {
    "jquery": "~2.1.1",
    "underscore": "~1.7.0",
    "backbone": "~1.1.2"
  }
}
```

```bash
bower install
```


## Grunt

Gruntfileを書いてwebpackをビルドできるようにします。

```coffeescript:Gruntfile
"use strict"

# webpackのモジュールを呼び出しておく
path = require("path")
webpack = require('webpack')

tasks = ["webpack"]


module.exports = (grunt) ->
  pkg = grunt.file.readJSON("package.json")

  grunt.initConfig

    pkg: pkg

    # webpackのオプションをここに書く。
    # webpack.config.jsと併用する場合、どちらを優先しておくか決めておくのが良さそう。
    webpack:
      app:
        entry: "./www/src/loader.coffee"  # コンパイル開始時に読み込むエントリファイル
        output:
          filename: "./www/resources/js/bundle.js"  # 出力先ファイルパス
        module:
          loaders: [
            { test: /\.coffee$/, loader: 'coffee-loader' }  # 拡張子coffeeはcoffee-loaderを使う
          ]
        resolve:
          root: [path.join(__dirname, "www/components")]  # bowerのディレクトリを定義
        plugins: [
          new webpack.ResolverPlugin(
            new webpack.ResolverPlugin.DirectoryDescriptionFilePlugin("bower.json", ["main"])
          ),
          new webpack.ProvidePlugin({  # グローバル空間に読み込むモジュール？よく分かってません
            $: "jquery",
            jQuery: "jquery",
            _: "underscore",
            Backbone: "backbone"
          })
        ]
        stats:
          colors: true
          modules: true
          reasons: true
        progress: false
        failOnError: false
        watch: false  # webpackで変更監視しません
        keepalive: false

    watch:
      files: [
        "www/src/**/*.coffee",
        "www/components/**/*.js",
      ]
      tasks: tasks

  taskName = undefined

  for taskName of pkg.devDependencies
    grunt.loadNpmTasks taskName  if taskName.substring(0, 6) is "grunt-"

  grunt.registerTask "default", tasks
```

# Backboneアプリケーション

## loader.coffee

webpackのコンパイル時に初めに呼び出されるファイル。

```coffeescript:loader.coffee
require './router.coffee'

window.App =
  Models: {}
  Views: {}
  Utils: {}
```


## router.coffee

Backbone.Routerの定義。実質このファイルの処理からアプリケーションが開始される。

```coffeescript:router.coffee
require './views/top.coffee'

jQuery ->
  class App.Router extends Backbone.Router
    routes:
      '': 'top'
      'pictures/:id': 'pictures'

    top: ->
      console.log('top', arguments)
      new App.Views.TopView()

    pictures: ->
      console.log('pictures', arguments)

  new App.Router()
  Backbone.history.start({pushState: true})
```

## views/top.coffee

サイト直下にアクセスした場合のビューを定義します。`Apps.Models.PhotoModel`に依存するものとします（てきとうです）。

```coffeescript:views/top.coffee
# 必要なモデルを呼び出し
require '../models/top.coffee'


jQuery ->
  # Backboneオブジェクトは定義済みなのでrequire必要なし
  class App.Views.TopView extends Backbone.View
    initialize: ->
      @
```


## models/top.coffee

`Apps.Models.PhotoModel`の定義（てきとうです）。

```coffeescript:models/top.coffee
jQuery ->
  class App.Models.PhotoModel extends Backbone.Model
    idAttribute: 'id'
    urlRoot: '/ajax/photos/'

    parse: (response)->
      return response.photo
```


# コンパイル

## 手動でコンパイル
`webpack.config.js`を用意していないのでgruntからコンパイルします。

```text
grunt webpack

Running "webpack:app" (webpack) task
Version: webpack 1.4.13
                       Asset    Size  Chunks             Chunk Names
./www/resources/js/bundle.js  460308       0  [emitted]  main
   [0] ./www/src/loader.coffee 86 {0} [built]
   [1] ./www/src/router.coffee 2093 {0} [built]
(省略)

Done, without errors.
```

## 変更を監視してコンパイル

`grunt-contrib-watch`をインストールしてCoffeeScriptの変更を監視し、変更時に自動的にコンパイルします。前述の`Gruntfile.coffee`にwatchタスクが定義されているので、`grunt watch`を実行するだけで監視状態になります。

```text
grunt watch
```


# 参考
* [全部のせRequireJSっぽいWebpackを使ってみた。](http://qiita.com/shn/items/7bd544a39ee7ac858669)