ここ数年のSPAはExtJSを使っているのですがES6時代は新しいフレームワークに変えようと思っていろいろと試しています。Ember.jsやReact.jsで書いていても何か違う感じがしていたのですが、黒ムツ本ことMithrilを読んで気に入ったので、しばらくはMithrilで書いていこうと思います。特にReact.jsの慌ただしさに疲れた人には向いていると思います。
フロントエンドはトランスパイルするのでimport句を使い、サーバーサイドは最新のNode.js(執筆時点はv5.1.0)でサポートされていないので使っていません。SPAだとサーバーサイドは軽量なREST APIサーバーやWebhookが多くなるので、処理単位の小さい関数で十分なくらいの実装にしたいです。
プロジェクト
Mithrilはまだ新しいフレームワークなのでビルド環境や他のフレームワークとの連携もサンプルが少なく試行錯誤しているところです。今回は開発環境を整理してみようと思います。リポジトリはこちらです。
$ 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です。ONBUILD版でも良いのですが、クラウドの仮想マシンにあるDockerホストからEmacsで開発ができるようにしています。
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で書くことにあまり関係はありませんが。
mithril:
restart: always
build: .
volumes:
- .:/usr/src/app
ports:
- "3030:3000"
ビルド環境
pakage.json
package.jsonのscriptsフィールドはgulpコマンド経由で実行します。Dockerfileに書いたCMD命令から呼ばれるstart
でプロダクションのビルドとExpressが起動するようにしています。
"scripts": {
"start": "gulp webpack && NODE_ENV=production node ./app/server/server",
"nodemon": "gulp nodemon",
"watch": "gulp watch",
"build": "gulp webpack"
}
開発環境のタスクはnodemonが監視とリロードの面倒を見てくれます。
gulp.task('nodemon', () => {
nodemon({
script: './app/server/server.js',
ext: 'js',
watch: ['./app/server'],
env: { 'NODE_ENV': 'development' }
})
});
プロダクション環境はwebpackタスクで別に分けているwebpack.config.production.jsを実行してから通常のExpresサーバーを起動しています。
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を使い、フロントエンドはwebpack-hot-middlewareとwebpack-dev-middlewareが監視します。
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つのファイルにしたいのですが良い方法が見つからないので現状は分けています。
'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を使いコードの最適化とsourceMapを作成しないように変えています。
'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の組み合わせなので、開発する場所も(インターネットとターミナルがあれば)デプロイする場所からも解放されて自由にプログラミングができます。
$ 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
コマンドをデタッチモードで実行します。
$ docker-compose up -d
restart: always
フィールドを書けばサービスが自動再起動するのでシンプルなデモナイズはこれだけで済みます。
mithril:
restart: always
build: .
volumes:
- .:/usr/src/app
ports:
- "3030:3000"