Mithril + WebPackでページごとの非同期読み込み対応のSPAを作る

PySpaコミュニティで、いいことも悪いこともいろいろ教えてくれるみんなのお兄さんのmopemopeさんから、ES6で全面的に書きなおされたVue.jsが良いよ、という話を聞きました。Vue.jsはMithrilを触る前にちょびっと触ってみたことはあったのですが、良さそうだったものの、当時使っていたJSX(DeNAの方)の静的型付けと相性が良くなくて、あきらめました。まあ静的型付けと相性のいいWebMVCはtypeScriptで作りなおされているというAngular 2の登場までは存在しないかもしれませんが・・・

当時は知らなかったのですが、vueifyというツールを使うと、CSS/テンプレート/ロジック(.js)がセットになった.vueファイルというのがあって、大規模での開発でも取り回しがしやすいとのこと。

**mopemope**

```
ES6ならvuejsがいい
ほぼ全面的に書き直しされた
```

**aodag**

```
,(´_・ω・)_ vueファイル使ってる？
```

**mopemope**

```
つこてるよ
```

**shibukawa**

```
vueファイルとは
```

**wozozo**

```
https://github.com/vuejs/vue-hackernews/blob/gh-pages/src/components/NewsView.vue
.vue
```

**mopemope**

```
componentだね
テンプレート、CSS、ロジック(js) のワンセット
これをローダーがよきにはからってくれるテンプレートとJSを綺麗にワンセットで独立化できるのでそこそこでかい規模でも使える
vuejsのコンポーネントはそのままwebpackの非同期ローディングとして使える
コンポーネントは必要になった際はじめてロードされる
なのででかいSPAでも初期表示のコストを抑えれる
全部読み込まないので
画面をコンポーネント化してSPAを組んだ際に再度コンポーネントをロードしなおすかどうかはvue-routerの機能で実現できる
SPAならkeep-aliveで状態保持でもいい
そうするとDOMの状態をずっと保持してるので遷移しても本当に描画だけの速度ですむので切り替えが高速
vuejsの致命的なとこは大量のリストのレンダリングが遅い
```

**aodag**

```
,(´_・ω・)_使い込んでんだな
vueファイルのcssって
,(´_・ω・)_ あれやっぱグローバルだよね > vueファイル内のcss
```

**mopemope**

``` 
1枚になるね、css
WebPackの限界のはず
```

**shibukawa**

```
ページごとの非同期ロードを実現するような仮想DOMのフレームワーク、作りたいなぁとぼんやりと思ってたけどすでに先に実現しているやつがいたか。まぁ誰でも思いつきそうではあるけど。
```

**aodag**

```
vueは仮想DOMじゃないでしょ
```

**shibukawa**

```
vueは違う
ページごとの非同期ロードの部分
```

**aodag**

``` 
,(´_・ω・)_mithrilをベースに作ればええんじゃないのん
```

Mithrilでも非同期ロードしてみる
=================================

aodagに煽られたので、Mithrilでもやってみました。全ファイルはgistにおいてあります。フォルダ構成はこんな感じ。jsはwebpackした成果物置き場です。

* つくってみた: https://gist.github.com/shibukawa/edb4c0895c2a5de64e9587d255015fc4

```
+ index.html
+ js
+ src
|  + app.js
|  + module_a.js
|  + module_b.js
|  + module_c.js
+ webpack.config.js
```

まずはトップページのindex.html。ページのコンテンツと、ナビゲーションの２つを置くタグを設置してます。

```html:index.html
<!DOCTYPE html>
<html>
    <meta charset="utf-8">
    <body>
        <div id="menu"></div>
        <div id="content"></div>
        <script src="js/app.js"></script>
    </body>
</html>
```

app.jsです。loadedというのが読み込み済みのモジュール(≒Mithrilコンポーネント)を入れておくキャッシュです。LazyModuleA-Cが、遅延ロードだけを行うコンポーネントです。3つあって冗長になっちゃっていますが、やっていることはそれぞれ変わりません。

``m.mount`` でページ遷移のナビゲーションのリストを表示して、末尾の ``m.route`` で、各ページの情報を設定しています。

```js:src/app.js
var m = require("mithril");

m.mount(document.querySelector("#menu"), {
    view: function() {
        return m("ul", [
            m("li", m('a[href="/a"]', {config: m.route}, "module A")),
            m("li", m('a[href="/b"]', {config: m.route}, "module B")),
            m("li", m('a[href="/c"]', {config: m.route}, "module C"))
        ]);
    }
});

var loaded = {};

var lazyModuleA = {
    view: function (ctrl) {
        if (loaded.module_a) {
            return loaded.module_a;
        } else {
            m.startComputation();
            require(["./module_a.js"], function(module_a) {
                loaded.module_a = module_a;
                m.endComputation();
            });
        }
    }
};

// LazyModuleB, LazyModuleCは省略

m.route(document.querySelector("#content"), "/a", {
    "/a": lazyModuleA,
    "/b": lazyModuleB,
    "/c": lazyModuleC,
});
```

各ページのソースはこんな感じです。ロードされたらコンソールにログを出しています。A-Cは中身はほぼ一緒なので、1つだけ紹介します。

```js:src/module_a.js
var m = require("mithril");

console.log("module A is loaded");

module.exports = {
   view: function () {
       return m("h1", "module A");
   }
};
```

最後にWebPackの設定です。

```js:webpack.config.js
module.exports = {
  entry: {
    app: "./src/app.js"
  },
  output: {
    path: "./js/",
    publicPath: "/js/",
    filename: "[name].js"
  },
  resolve: {
    extensions: ["", ".src", ".css"]
  }
};
```

できたのはこんな感じ。トップ(デフォルトのラウトは``/a``なので、Aが自動でロードされる)を表示したのち、module Bに遷移してみたところです。モジュールは3つありますが、まだ遷移してないmodule Cはロードされていないので、module Cロードのログは出ていません。めでたしめでたし。

![スクリーンショット 2016-04-13 12.44.12.png](https://qiita-image-store.s3.amazonaws.com/0/6105/42547bcc-1e1d-c2ef-b44a-26abed197cbd.png)

コードの解説
================

Mithrilの描画の流れについては下記のエントリーで解説しています。今回のコードは別にすごい大変なことをしているわけじゃなくて、この仕組みに乗っかっているだけです。

* [最速MVCフレームワークMithril.jsの速度の秘密](http://qiita.com/shibukawa/items/890d24874655439932ec)

```js:src/app.js
var lazyModuleA = {
    view: function (ctrl) {
        if (loaded.module_a) {
            return loaded.module_a;
        } else {
            m.startComputation();
            require(["./module_a.js"], function(module_a) {
                loaded.module_a = module_a;
                m.endComputation();
            });
        }
    }
};
```

キャッシュされていたらそれをそのまま返します。Mithrilのビューは状態を持たないJSの関数でしかないため、簡単に他のコンポーネントに処理を委譲できます。

キャッシュされていない場合はロードを行います。ロードは[WebPackの非同期ロード](http://webpack.github.io/docs/code-splitting.html)を使っています。ロードの完了の前後で、画面更新を停止させています。これで中途半端な状態で表示されることはなくなります。[本来はサーバからAjaxでデータを取ってくる](http://qiita.com/shibukawa/items/890d24874655439932ec#%E3%83%9E%E3%82%AF%E3%83%AD%E3%81%AA%E9%AB%98%E9%80%9F%E5%8C%96)目的の仕組みですが、これをコードのロードに使ってみた、ってだけです。

実用的に使うには
===================

どのURLでアクセスしても、メインのラウター定義の.jsと、各ページの.jsしか読み込まないので、ページ数の増加に対してもパフォーマンスはO(1)ですよね。超大規模になっても初期ロード時間は一定です。Mithrilの仮想DOMもあいまって、ユーザビリティを損ねない仕組みにはなっていると思います。

実際にアプリケーションのロジックやビューを記述するコード以外に非同期処理の定形処理がページ数分発生してしまいます。当初はモジュール名を受け取る ``LazyComponent`` というのを作ってみたのですが、WebPackが ``require(静的文字列)``というASTノードを探して処理しているため、そのままでは ``require(変数)`` 形式ではうまく処理できません。偉大なダイクストラ先生の構造化がうまくいかないという、ちょっと悲しい状態です。

このムダを減らすにはいくつか方法があると思います。WebPackは昨夜初めて使ったので想像も入っています。

* ``LazyComponent``でも何でもいいが、特定の形式で書いたソースコードのトークンを特別扱いして、requireしたのと同様の処理になるようなWebPackプラグインを作る。
* .vueみたいなファイルを作って、事前に.jsやらに変換するプリプロセッサを作る(というWebPackプラグインを作る？)
* 特にツールとかは作らず、非同期処理のコードだけ一箇所にまとめる。

ツールを作ったりするとメンテが発生するし、あらたなカオスを生むことになるので、多少雑でも最後の方法がいいのかなぁと思ってるところです。

JavaScriptフレームワークが大変話しがでていますが
===============================================

いろんなフレームワークがいろいろな工夫をして生産性を改善しようとしているのは健全なことですよね。単一のツールやフレームワークしかなくて、それの「良い使い方」の部分最適化しか業界全体で行われないとなると、新しい方向性の進化が生まれてくることもない。多様性は善です。あと、革袋が新しくても、中身はあんまり変わらないですしね。僕はMVC的なやつは最初はKnockout.jsを使いましたが、１つ使ってみたら、vue.jsだってMithrilだってキャッチアップは簡単でした。

１人で全部追い掛ける必要はないと思います。今回もVue.jsやら他のフレームワークに詳しい人から「こんなのあるよ」「じゃあこっちでもやってみよう」という流れなわけですし、一緒に情報交換できる仲間がいればいい。職場にいていつでも話ができればそれに越したことはないですが、ニュースグループ、フォーラム、メーリングリスト、掲示板、汎用のSNS(mixi、Twitter、Facebook)など、20世紀後半から場所に限らず情報交換できるようにはなっていますし。

個人的にはMithrilでいいやと思っています。仮想DOMで状態管理を手動でしなくてもよくて、遅くなくて、シングルページアプリケーションも実現できて、今回非同期ロードもできるようになりました。他の仕組みを使わないとユーザに提供できない価値があるならもちろん他のフレームワークを使うことも検討しますが、今のところ僕のユースケースではそのようなものは見当たりませんし。

次は、テンプレート周りでハックしたいところですね。Isomorphicというと、サーバもクライアントと同じ言語(つまりはnode.js)を使うのが当たり前になっていますが、1つのテンプレートから、JavaScript用と他の言語用のテンプレートを両方出す、というのは難しくないでしょうし、テンプレートのレンダリングに使うコンテキストも、スキーマを持ったデータのモデルを元にある程度共通化ってできると思うんですよね。