JavaScript
SPA
Mithril.js
ssr

Mithril.js で SPA + SSR をはじめる手引き 🔰

More than 1 year has passed since last update.

はじめに

突然ですが、Mithril.js というフレームワークをご存知でしょうか?
ひとことで言うと、めっちゃ軽量、めっちゃ簡単、めっちゃマイナーなJSフレームワークです。(諸説あり)

マイナー好きな私は大好物なのですが、Mithril.js で SPA + SSR する記事があまりなかったので、布教も兼ねて、はじめてnodeを使う方にも極力わかりやすいよう、開発環境の準備も含め記事にしてみました。

(職業柄、nodeは週末にたしなむ程度なので誤りがあるかもしれません、温かい編集リクエストやコメントお待ちしております!! :pray: :bow: )

前提

  • 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のレンダラが対応していないらしく、オブジェクトとして定義します。:cry:

ルーティングを作成

コンポーネントへのルーティング 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 するアプリケーションの完成です!!! :tada: :tada: :tada:

最後に

本来ならテストに関しても触れておきたいところですが、ボリューム的にまた別の記事として書こうと思います。

めっちゃ簡単で、めっちゃ早い Mithril.js (諸説あり)、もっとコミュニティ盛り上がって欲しいな :pray: :pray: :pray:

つづき

参考