はじめに
突然ですが、Mithril.js というフレームワークをご存知でしょうか?
ひとことで言うと、めっちゃ軽量、めっちゃ簡単、めっちゃマイナーなJSフレームワークです。(諸説あり)
マイナー好きな私は大好物なのですが、Mithril.js で SPA + SSR する記事があまりなかったので、布教も兼ねて、はじめてnodeを使う方にも極力わかりやすいよう、開発環境の準備も含め記事にしてみました。
(職業柄、nodeは週末にたしなむ程度なので誤りがあるかもしれません、温かい編集リクエストやコメントお待ちしております!! )
前提
- node (v8.5.0) をインストール済み
- MVCを少し知っている
完成品
この記事で作成したコードを下記リポジトリにアップロードしました。記事で触れる範囲を事前に確認したい方はどうぞ。
https://github.com/alfa-jpn/mithril-spa-ssr-example/tree/part1
開発環境の準備
ES6でJSを書き、自動ビルド、ブラウザでの動作確認を行うための環境を構築していきます。
この章の内容は このコミット になります。
npmの初期化
$ npm init
babelの準備
ES6でコードを書くために babel をインストールします。
パッケージのインストール
$ npm install --save babel-cli babel-core babel-loader babel-preset-es2015
.babelrcの作成
プロジェクト直下に .babelrc
を作成し、babelでES6を使用する設定を記述します。
{
"presets": ["es2015"]
}
webpackの準備
プロジェクトのビルドを行うために webpack をインストールします。
パッケージのインストール
$ npm install --save webpack
package.json にタスクを追加
ビルドのタスクを package.json
に定義します。scripts
オブジェクトの中にタスクを追加します。
{
"scripts": {
"build": "webpack -p"
}
}
(部分抜粋)
webpack.config.babel.jsの作成
プロジェクト直下に webpack.config.babel.js
を作成し、webpackの設定を記述します。
import path from 'path';
const config = {
context: path.resolve(__dirname, 'app'),
entry: './app.js',
output: {
path: path.resolve(__dirname, 'public', 'assets', 'javascripts'),
publicPath: '/assets/javascripts/',
filename: 'bundle.js'
},
module: {
rules: [{
test: /\.jsx?$/,
exclude: /node_modules/,
use: ['babel-loader']
}]
},
resolve: {
extensions: ['.js', '.jsx']
}
};
export default config;
設定の内容
- コンパイル時のコンテキストを
app/
に設定 - アプリケーションのエントリーポイントを
app.js
に設定 -
public/assets/javascripts
へブラウザ向けのbundle.js
を出力するように設定 - babel-loaderを使用してES6のJSとJSX(後述)をよしなにトランスパイルするよう設定
app.jsの作成
上記設定の動作確認をするために app/
ディレクトリを作成し暫定の app.js
を配置します。
console.log("Hello!")
動作確認用にコンソールログを出力するだけです。
動作確認
$ npm run build
上記コマンドを実行し、次のような出力結果を得られれば正しく動作しています。
Time: 186ms
Asset Size Chunks Chunk Names
bundle.js 534 bytes 0 [emitted] main
[0] ./app.js 37 bytes {0} [built]
また public/assets/javascripts/bundle.js
が出力されていると思います。
webpack-dev-serverの準備
自動ビルド、ブラウザでの動作確認を行うために、webpack-dev-serverをインストールします。
パッケージのインストール
$ npm install --save-dev webpack-dev-server
開発環境でしか使用しないため --save-dev
オプションを指定しています。
package.json にタスクを追加
開発用サーバーの起動タスクを package.json
に定義します。scripts
オブジェクトの中にタスクを追加します。
{
"scripts": {
"server-dev": "webpack-dev-server --inline --hot"
}
}
(部分抜粋)
--hot
オプションを指定することで、ファイルの変更を検知して自動的にページが再読み込みされるようになります。
webpack.config.babel.jsに設定を追加
webpack.config.babel.js
にwebpack-dev-serverの設定を追記します。
const config = {
// * 略 * //
resolve: {
extensions: ['.js', '.jsx']
}
devServer: {
contentBase: path.resolve(__dirname, 'public'),
port: 3000
}
};
設定内容
- 開発用サーバーのルートを
public/
に設定 - 開発用サーバーのポートを
3000
に設定
index.htmlの作成
public/
ディレクトリを作成し index.html
を配置します。
<html lang="ja">
<body>
<script src="assets/javascripts/bundle.js"></script>
</body>
</html>
動作確認
$ npm run server-dev
上記コマンドでサーバーを起動したら http://127.0.0.1:3000
にアクセスしてみてください。
先ほど作成した app.js
が読み込まれ、ブラウザにコンソールログが出力されます。
また、app.js
を変更するとブラウザが自動的に再読み込みされることも確認できると思います。
以上で開発環境の準備は完了しました。
SPAの作成
まずは、トップページとサブページからなる簡単なSPAを作成してみます。
本項目ではMithril.jsの基本的な知識の解説は省いていますので、公式ドキュメントなどと照らし合わせながら進めてください。
この章の内容は このコミット になります。
パッケージのインストール
Mithril.js本体など、SPAに必要なパッケージをインストールします。
$ npm install --save mithril domready babel-plugin-transform-react-jsx
JSXの設定
JSXをMithril.jsで使用する設定を .babelrc
に追記します。
{
"presets": ["es2015"],
"plugins": [
["transform-react-jsx", {
"pragma": "m"
}]
]
}
共通のレイアウトビューを作成
全てのページで使いまわすレイアウト app/views/layouts/application.jsx
を作成します。
'use strict';
import m from 'mithril';
function getTitle(vnode) {
if (vnode.state.getTitle) {
return vnode.state.getTitle();
}
return 'Mithril SSR';
}
export default function(template) {
return function(vnode) {
if (process.browser) {
document.title = getTitle(vnode);
return template(vnode);
} else {
return (
<html lang="ja">
<head>
<title>{ getTitle(vnode) }</title>
</head>
<body>
{ template(vnode) }
<script src='/assets/javascripts/bundle.js'></script>
</body>
</html>
); }}}
さて、JSXはJSなので上記のように関数を定義できますが、このような記述はJSのロジックが増え、デザイナさんが触りづらいですし(ように見えますし)、肥大化して悲惨な事になるのは想像がつきますね。
と言うことで、次のようにJSロジック部はヘルパに分離しましょう!
app/helpers/layouts_helper.js
export function getTitle(vnode) {
if (vnode.state.getTitle) {
return vnode.state.getTitle();
}
return 'Mithril SSR';
}
app/views/layouts/application.jsx
'use strict';
import m from 'mithril';
import { getTitle } from '../../helpers/layouts_helper'
export default function(template) {
return function(vnode) {
if (process.browser) {
document.title = getTitle(vnode);
return template(vnode);
} else {
return (
<html lang="ja">
<head>
<title>{ getTitle(vnode) }</title>
</head>
<body>
{ template(vnode) }
<script src='/assets/javascripts/bundle.js'></script>
</body>
</html>
); }}}
JSロジックがビューから分離されだいぶスッキリしました。
個別のビューテンプレートを作成
トップページとサブページのテンプレートを作成します。
app/views/home/top.jsx
'use strict'
import m from 'mithril';
export default function() {
return (
<main>
<h1>Hello Mithril SPA + SSR!!</h1>
<a href='/sub' oncreate={ m.route.link }>sub</a>
</main>
)}
app/views/home/sub.jsx
'use strict'
import m from 'mithril';
export default function() {
return (
<main>
<h1>This is sub page!!!</h1>
<a href='/' oncreate={ m.route.link }>sub</a>
</main>
)}
コンポーネントを作成
トップページとサブページになるコンポーネントを作成します。
app/components/homes/top.js
'use strict';
import layout from '../../views/layouts/application';
import template from '../../views/homes/top';
export default {
view: layout(template),
oninit: (vnode) => {
console.log("init top")
}
}
app/components/homes/sub.js
'use strict';
import layout from '../../views/layouts/application';
import template from '../../views/homes/sub';
export default {
view: layout(template),
oninit: (vnode) => {
console.log("init sub");
vnode.state.getTitle = (() => "Sub page");
}
}
本来ならば、ES6クラスとして定義したいところですが、今回SSRのレンダラが対応していないらしく、オブジェクトとして定義します。
ルーティングを作成
コンポーネントへのルーティング config/routes.js
を作成します。
'use strict';
import homesTop from '../app/components/homes/top';
import homesSub from '../app/components/homes/sub';
const routes = {
'/': homesTop,
'/sub': homesSub
}
export default routes;
app.jsを修正
SPAとして動作するよう app/app.js
を修正します。
'use strict';
import domready from 'domready';
import m from 'mithril';
import routes from '../config/routes';
m.route.prefix('');
domready(function(){
m.route(document.body, '/', routes);
});
動作確認
$ npm run server-dev
上記コマンドでサーバーを起動したら http://127.0.0.1:3000
にアクセスしてみてください。
SPAとして動作しているしていると思います。
以上でSPAの作成は完了しました。
アプリをSSRする
前章で作ったアプリをSSRしてみます。
SSRするとファーストビューの高速化や、SEOへの効果を期待できます。
この章の内容は このコミット になります。
パッケージのインストール
mithril-node-render
など、SSRに必要なパッケージをインストールします。
$ npm install --save mithril-node-render express w3c-xmlhttprequest
initializer.jsの作成
レンダリングするための環境を初期化するためのモジュール server/initializer.js
を作成します。
'use strict';
import browserMock from 'mithril/test-utils/browserMock';
import xhr from 'w3c-xmlhttprequest';
browserMock(global);
global.window.XMLHttpRequest = xhr;
app-server.jsの作成
アプリケーションのレンダリングサーバー server/app-server.js
を作成します。
'use strict';
import express from 'express';
import m from 'mithril';
import toHtml from 'mithril-node-render';
import routes from '../config/routes';
var appServer = express();
Object.keys(routes).map(function(route) {
const module = routes[route];
const onmatch = module.onmatch || (() => module);
const render = module.render || ((d) => d);
appServer.get(route, function(req, res, next){
res.type('html');
Promise.resolve()
.then(() => m((onmatch(req.params, req.url) || 'div'), { "data-server-params": req.params }))
.then(render)
.then(toHtml)
.then(res.send.bind(res))
.catch(next)
});
});
export default appServer;
server.jsの作成
レンダリングサーバー本体 server/server.js
を作成します。
'use strict';
import express from 'express';
import initializer from './initializer';
import app from './app-server';
var server = express();
server.use(app);
server.use(express.static('public'));
var port = process.env.PORT || 3000;
console.log("Server is now rnning on port:" + port);
server.listen(port);
package.json にタスクを追加
サーバーの起動タスクを package.json
に定義します。scripts
オブジェクトの中にタスクを追加します。
{
"scripts": {
"server": "babel-node ./server/server.js"
}
}
(部分抜粋)
動作確認
下記のコマンドでJSのビルドとサーバーを起動します。
npm run build && npm run server
上記コマンドでサーバーを起動したら http://127.0.0.1:3000
にアクセスしてみてください。初回アクセスではSSRされたDOMが配信され、以降はSPAとして動作していると思います。
以上で Mithril.js を使って SPA + SSR するアプリケーションの完成です!!!
最後に
本来ならテストに関しても触れておきたいところですが、ボリューム的にまた別の記事として書こうと思います。
めっちゃ簡単で、めっちゃ早い Mithril.js (諸説あり)、もっとコミュニティ盛り上がって欲しいな