MithrilとES6のチュートリアル - Part 2: BootstrapをnpmとWebpackでインストールする

　[黒ムツ本](http://www.oreilly.co.jp/books/9784873117447/)の第5章「[Bootstrap](http://getbootstrap.com/)を適用する」ではCDNや[Bower](http://bower.io/)からBootstrapをインストールする方法が書かれていますが、今回はフロントエンドのモジュール管理に[Webpack](https://webpack.github.io/)を使ってみます。[こちらの投稿](http://qiita.com/yosisa/items/61cfd3ede598e194813b)にあるようにWebpackは[npm の利用を推奨](http://webpack.github.io/docs/usage-with-bower.html#prefer-modules-from-npm-over-bower)しています。サーバーサイドもNode.jsで書く場合は、フロントエンドも同じにするとnpmだけで両方のパッケージ管理ができます。

## package.json

　リポジトリは[こちら](https://github.com/masato/docker-mithril-study)です。Bootstrap用にnpmでインストールしたパッケージです。

```js:package.json
    "devDependencies": {
...
        "jquery": "~2.1.4",
        "mithril": "~0.2.0",
        "bootstrap": "~3.3.5",
...
        "style-loader": "~0.13.0",
        "css-loader": "~0.21.0",
        "imports-loader": "~0.6.5",
        "file-loader": "~0.8.4",
        "url-loader": "~0.5.6",
...
```


## loaders

　Webpackの[loaders](https://webpack.github.io/docs/loaders.html)タスクを使いURLを検証してファイルをロードするときに必要な前処理をインジェクトします。

### babel-loader 

　[前回](http://qiita.com/masato/items/154bcbf6e1eb1dc676ad)のようにES6でコードを書くためnpmでインストールしたパッケージ以外の`.js`ファイルは[babel-loader](https://github.com/babel/babel-loader)を通してロードします。

```js:webpack.config.js
..
            test: /\.js?$/,
            exclude: /node_modules/,
            loader: 'babel'
...
```

### import-loader

　Bootstrapの中には`jQuery`変数に依存しているコードがあります。WebpackからjQueryをロードする方法はいくつもあり、書き方もいろいろで悩みます。今回は[import-loader](https://github.com/webpack/imports-loader)を使いました。`bootstrap`モジュールの`jQuery`変数に`jquery`モジュールをインジェクトします。

```js:webpack.config.js
...
        loaders: [{
...
        }, {
            test: /bootstrap\/js\//,
            loader: 'imports?jQuery=jquery'
        }, {
...
```

### style-loader, css-loader, less-loader

　CSSは[style-loader](https://github.com/webpack/style-loader)から[css-loader](https://github.com/webpack/css-loader)経由でロードします。[LESS](http://lesscss.org/)をロードする場合は[less-loader](https://github.com/webpack/less-loader)を経由すると自動的に書式を変換してくれます。

```js:webpack.config.js
...
        loaders: [{
...
        }, {
            test   : /\.css$/,
            loader : 'style!css'
        }, {
            test   : /\.less$/,
            loader : 'style!css!less'
        }, {
...
```

### url-loader

　Bootstrapで使う[フォント](https://github.com/twbs/bootstrap/tree/master/fonts)はURLを[test](https://webpack.github.io/docs/configuration.html)の条件にして、100kbより小さいファイルを[url-loader](https://github.com/webpack/url-loader)からロードします。

```js:webpack.cofig.js
...
        }, {
            test: /\.(png|woff|woff2|eot|ttf|svg)$/,
            loader: 'url?limit=100000'
        }],
...
```

### まとめ

まとめると`module`の`loaders`フィールドは以下のようになります。

```webpack.config.js
...
        loaders: [{
            test: /\.js?$/,
            exclude: /node_modules/,
            loader: 'babel'
        }, {
            test: /bootstrap\/js\//,
            loader: 'imports?jQuery=jquery'
        }, {
            test   : /\.css$/,
            loader : 'style!css'
        }, {
            test   : /\.less$/,
            loader : 'style!css!less'
        }, {
            test: /\.(png|woff|woff2|eot|ttf|svg)$/,
            loader: 'url?limit=100000'
        }],
...
```

## ViewでBootstrapをロードする

　フロントエンドのパッケージ管理をWebpackとnpmからインストールしました。Bootstrapの.jsと.cssを`import`句を使ってエントリーポイントの`main.js`にロードします。

```bash:app/client/js/main.js
import m from 'mithril';
import 'bootstrap';
import 'bootstrap/dist/css/bootstrap.css';
...
```

### NavBarコンポーネント

　Bootstrapの[NavBar](http://getbootstrap.com/components/#navbar)をMithrilのコンポーネントにしたES6で書いてみます。MithrilのViewは[m](https://lhorie.github.io/mithril/mithril.html)のDSLを使います。ReactのJSXを移植した[MSX](https://github.com/insin/msx)という名前がノスタルジックなシンタックスもあります。m関数はオブジェクトと配列で仮装DOMを書くことができます。個人的にはJSXスタイルより書きやすいです。

```app/client/js/navbar.js
import m from 'mithril';

const NavBar = {
    view: (ctrl, args) => {
        return m('.container', [
                 m('.nav-header', [
                   m('a.navbar-brand', { href: '#' }, args.title)
            ])
        ]);
    }
}

export default NavBar;
```

NavBarコンポーネントはmain.jsにimportして`m.mount`をコールしてindex.htmlの`navbar`idにマウントします。`m.component`は引数をviewにバインドしたNavBarコンポーネントを返します。ここではタイトルの名称を渡しています。

```app/client/js/main.js
...
import NavBar from './navbar';
m.mount(document.getElementById('navbar'),
                                m.component(NavBar, {
                                    title: 'Mithril Study'
                                }));
...
```

### Homeコンポーネント

　Homeコンポーネントも普通のオブジェクトで書きました。[Glyphアイコン](http://getbootstrap.com/components/#glyphicons-examples)を使ったBootstrapの[アラート](http://getbootstrap.com/components/#alerts)です。

```app/client/js/home.js
import m from 'mithril';

export default {
    controller: () => {},
    view: () => {
        return [
            m('div', {class: 'container'}, [
                m('.page-header', [
                    m('h3', 'Bootstrap アラート')
                ]),
                m('.alert.alert-info[role="alert"]', [
                    m('span.glyphicon.glyphicon-exclamation-sign[aria-hidden="t\
rue"]'),
                    m('span.sr-only', 'Info:'),
                    ' Infoのサンプルです。'
                ])
            ])
        ];
    }
};
```

　仮装DOMのDIV要素は`m('div')`と書いたり、省略してCSSのクラス名をつなげて`m('.alert.alert-info[role="alert"]'`のようにも書けます。以下のようなHTMLを生成します。

```html
<div role="alert" class="alert alert-info">
  <span aria-hidden="true" class="glyphicon glyphicon-exclamation-sign"></span>
  <span class="sr-only">Info:</span>
 Infoのサンプルです。
</div>
```


### index.html

　SPAのHTMLはとてもシンプルでWebpackのjsをバンドルしてサーブするだけです。[html-webpack-plugin](https://github.com/ampedandwired/html-webpack-plugin)を使えばHTMLファイルも生成してくれますが、navbarも別にマウントしたかったのでindex.htmlを用意しました。

```html:dist/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Mithril Study</title>
    <link rel="shortcut icon" href="/images/favicon.ico" type="image/x-icon">
  </head>
  <body>
    <nav id="navbar" class="navbar navbar-default"></nav>
    <div id="root"></div>
    <script src="/js/main.js"></script>
  </body>
</html>
```

　`/js/main.js`はwebpack.config.jsのoutputフィールドに指定したWebpackがバンドルしたjsファイルの出力先になります。filenameを`[name].js`とするとエントリポイントに指定したファイル名が`[name]`で置換されます。

```js:webpack.config.js
...
    entry: [
        'webpack-hot-middleware/client',
        path.join(__dirname, 'app/client/js/main.js')
    ],
    output: {
        path: path.join(__dirname, 'dist/js'),
        filename: '[name].js',
        publicPath: '/js/'
    },
...
```

### ラウター

　エントリポイントの`main.js`を書きます。`m.mount`コンポーネントを使いNavBarを`<nav id="navbar"`マウントします。`m.route`はURLを使って複数のコンポーネントをマウントできるラウターです。今回はURLは`/`の1つしかないので、デフォルトの`/`にHomeコンポーネントをマップします。

`<div id="root">`をラウター

```app/client/js/main.js
import m from 'mithril';
import 'bootstrap';
import Home from './home';
import NavBar from './navbar';
import 'bootstrap/dist/css/bootstrap.css';
import '../css/main.css';

const home = m.component(Home);

m.mount(document.getElementById('navbar'),
                                m.component(NavBar, {
                                    title: 'Mithril Study'
                                }));

m.route(document.getElementById('root'), '/', {
    '/': home
});
```


## テスト

　Expressの開発サーバーはDocker Composeからワンショットのコンテナで起動します。`--service-ports`フラグを指定するとDockerホストにコンテナのポートがマップされます。

```bash
$ docker-compose run --rm \
  --service-ports \
  mithril \
  npm run nodemon
npm info it worked if it ends with ok
npm info using npm@3.3.12
npm info using node@v5.1.0
npm info lifecycle docker-mithril-study@0.0.1~prenodemon: docker-mithril-study@0.0.1
npm info lifecycle docker-mithril-study@0.0.1~nodemon: docker-mithril-study@0.0.1

> docker-mithril-study@0.0.1 nodemon /usr/src/app
> gulp nodemon

[10:00:41] Using gulpfile /usr/src/app/gulpfile.js
[10:00:41] Starting 'nodemon'...
[10:00:41] Finished 'nodemon' after 2.24 ms
[10:00:41] [nodemon] 1.8.1
[10:00:41] [nodemon] to restart at any time, enter `rs`
[10:00:41] [nodemon] watching: /usr/src/app/app/server/**/*
[10:00:41] [nodemon] starting `node ./app/server/server.js`
Example app listening at http://:::3000
webpack built 75d57784c725d9d93cbb in 4404ms
```

　docker-compose.ymlの`mithril`サービスの3000ポートをDockerホストの3030ポートにマップしています。

```yaml:docker-compose.yml
mithril:
  restart: always
  build: .
  volumes:
    - .:/usr/src/app
  ports:
    - "3030:3000"
```

　`docker-compose ps`コマンドからコンテナの起動と開いているポートの状態を確認できます。

```bash
$ docker-compose ps
       Name               Command       State           Ports
----------------------------------------------------------------------
part1_mithril_run_2   npm run nodemon   Up      0.0.0.0:3030->3000/tcp
```

 クラウドの仮想マシンにパブリックIPアドレスの3030ポートをブラウザで開きます。MithrilでBootstrapを使った簡単なアラート画面が表示されました。

<img width="462" alt="mithril-study-alert.png" src="https://qiita-image-store.s3.amazonaws.com/0/43745/db04dc8a-a68e-a839-930d-980d8a940281.png">