MithrilとES6のチュートリアル - Part 1: GulpとWebpackのミニマルなビルド環境

　ここ数年のSPAは[ExtJS](https://www.sencha.com/products/extjs/)を使っているのですがES6時代は新しいフレームワークに変えようと思っていろいろと試しています。[Ember.js](http://emberjs.com/)や[React.js](https://facebook.github.io/react/)で書いていても何か違う感じがしていたのですが、黒ムツ本こと[Mithril](https://www.oreilly.co.jp/books/9784873117447/)を読んで気に入ったので、しばらくは[Mithril](http://mithril.js.org/)で書いていこうと思います。特にReact.jsの慌ただしさに疲れた人には向いていると思います。

　フロントエンドはトランスパイルするのでimport句を使い、サーバーサイドは最新のNode.js(執筆時点はv5.1.0)でサポートされていないので使っていません。SPAだとサーバーサイドは軽量なREST APIサーバーやWebhookが多くなるので、処理単位の小さい関数で十分なくらいの実装にしたいです。

## プロジェクト

　Mithrilはまだ新しいフレームワークなのでビルド環境や他のフレームワークとの連携もサンプルが少なく試行錯誤しているところです。今回は開発環境を整理してみようと思います。リポジトリは[こちら](https://github.com/masato/docker-mithril-study)です。

```bash
$ tree .
.
├── Dockerfile
├── app
│   ├── client
│   │   ├── css
│   │   │   └── main.css
│   │   └── js
│   │       ├── home.js
│   │       ├── main.js
│   │       └── navbar.js
│   └── server
│       └── server.js
├── config.js
├── dist
│   ├── images
│   │   └── favicon.ico
│   └── index.html
├── docker-compose.yml
├── gulpfile.js
├── node_modules -> /dist/node_modules
├── package.json
├── webpack.config.js
└── webpack.config.production.js
```

### Docker

　ベースイメージは公式の[Node.js](https://hub.docker.com/_/node/)です。ONBUILD版でも良いのですが、クラウドの仮想マシンにあるDockerホストからEmacsで開発ができるようにしています。

```bash:Dockerfile
FROM node:5.1
MAINTAINER Masato Shimizu <ma6ato@gmail.com>

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
RUN mkdir -p /dist/node_modules &&\
    ln -s /dist/node_modules /usr/src/app/node_modules
COPY package.json /usr/src/app/
RUN npm install
COPY . /usr/src/app/
CMD ["npm", "start"]
```

　docker-compose.ymlでプロジェクトのルートディレクトリをコンテナにマウントします。`node_modules`はシムリンクにして隠れないようにしています。Dockerを使って開発するのは好みなのでMithrilをES6で書くことにあまり関係はありませんが。

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

## ビルド環境

　ビルド環境は[Gulp](http://gulpjs.com/)と[Webpack](https://webpack.github.io/)のを合わせて使います。

### pakage.json

　package.jsonのscriptsフィールドはgulpコマンド経由で実行します。Dockerfileに書いたCMD命令から呼ばれる`start`でプロダクションのビルドとExpressが起動するようにしています。

```js:package.json
    "scripts": {
        "start":   "gulp webpack && NODE_ENV=production node ./app/server/server",
        "nodemon": "gulp nodemon",
        "watch":   "gulp watch",
        "build":   "gulp webpack"
    }
```
開発環境のタスクはnodemonが監視とリロードの面倒を見てくれます。

```js:gulp.js
gulp.task('nodemon', () => {
  nodemon({
      script: './app/server/server.js',
      ext: 'js',
      watch: ['./app/server'],
      env: { 'NODE_ENV': 'development' }
  })
});
```

　プロダクション環境はwebpackタスクで別に分けているwebpack.config.production.jsを実行してから通常のExpresサーバーを起動しています。

```js:gulp.js
gulp.task('webpack', (callback) => {
    let myConfig = Object.create(webpackConfig);
    webpack(myConfig, function(err, stats) {
        if(err) throw new gutil.PluginError('webpack', err);
        gutil.log('[webpack]', stats.toString({
            colors: true
        }));
        callback();
    });
});
```


### webpack.config.js

　開発中のコード編集によるリロードはサーバーサイドは[nodemon](https://github.com/remy/nodemon)を使い、フロントエンドは[webpack-hot-middleware](https://www.npmjs.com/package/webpack-hot-middleware)と[webpack-dev-middleware](https://www.npmjs.com/package/webpack-dev-middleware)が監視します。

```js:server.js
const isProduction = process.env.NODE_ENV === 'production';

if(!isProduction) {
    const webpack = require('webpack');
    const webpackConfig = require('../../webpack.config.js');
    const compiler = webpack(webpackConfig);

    app.use(require('webpack-dev-middleware')(compiler, {
        noInfo: true, publicPath: webpackConfig.output.publicPath
    }));
    app.use(require('webpack-hot-middleware')(compiler));
}
```

　webpack-config.jsは本当は開発用とプロダクション用で1つのファイルにしたいのですが良い方法が見つからないので現状は分けています。

```webpack-config.js
'use strict';
const webpack = require('webpack');
const path = require('path');
module.exports = {
    devtool: 'eval-source-map',
    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/'
    },
    plugins: [
        new webpack.optimize.OccurenceOrderPlugin(),
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoErrorsPlugin()
    ],
```

　プロダクション環境の`plugins`フィールドではDedupePluginとUglifyJsPluginを使い[コードの最適化](https://github.com/webpack/docs/wiki/optimization)とsourceMapを作成しないように変えています。

```webpack-config.production.js
'use strict';
const webpack = require('webpack');
const path = require('path');
module.exports = {
    entry: [
        path.join(__dirname, 'app/client/js/main.js')
    ],
    output: {
        path: path.join(__dirname, 'dist/js'),
        filename: '[name].js',
        publicPath: '/js/'
    },
    plugins: [
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify('production')
            }),
        new webpack.optimize.DedupePlugin(),
        new webpack.optimize.UglifyJsPlugin({ sourceMap: false } )
    ],
```

## サーバーの起動


　開発サーバーはフラグをつけた`docker-compose run`コマンドで使い捨てのコンテナを起動します。サーバーとクライアントのどちらのコードを編集してもビルドとリロードをしてくれるので、IDEを使わなくてもEmacsでテンポ良く開発ができます。クラウドの仮想マシンと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

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

　プロダクションはDocker Composeの`up`コマンドをデタッチモードで実行します。

```bash
$ docker-compose up -d
```

 `restart: always`フィールドを書けばサービスが自動再起動するのでシンプルなデモナイズはこれだけで済みます。

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