MithrilはAPI数が極めて少なく、プロパティに関連するAPIとして用意されているのはm.prop
とm.request
の2つだけです。フレームワークなのに、モデルに関しては「こうしようぜ」というのがあまりAPIから伝わってこないので、なかなか戸惑うところもあると思います。
モデルを中心としてMithrilの考えを整理します。
モデルを作る際に意識する外部インタフェース
「モデルから見たらデータベースもビューの一つ」という考え方がシンプルで好きです。この言葉を言った時に賛同してくれる人はあんまりいなかったりもするのですが(ORMラブな世界線だと)、ヘキサゴナルアーキテクチャとか、オニオンなんちゃらみたいなのだと、データベースもインフラストラクチャ層の外部部品ということになるので「データベースもビューの一つ」という世界観に近いでしょうね。
Mithrilでモデルを作る時に、ビューやビューモデルから使うために作るので、そちら向けのインタフェースについては問題ないと思いますが、さらに次の2つのインタフェースを意識すると良いです。
- サーバ向けのインタフェース
- テスト用インタフェース
もちろん、Mithril本に書いたように、別途pubsubを使うならpubsub用インタフェースとかも用意しても良いです。今回は割愛します。
テスト向けのインタフェースは明日書きます。
サーバ側から見たモデル
基本的にはJSONと相性が良いようなモデルを作っていきます。
単体のモデルクラスはJSONと可換にする
具体的には、以下のメソッドで一発でJSONの文字列が作れる状態がMithril的には良いモデルです。
JSON.stringify(モデルのオブジェクト)
デバッグで、モデルの状態を見たい時はこうします。
console.log(JSON.strinigify(モデルのオブジェクト));
逆に、ロード時は次のように呼び出すのを基本とします。
new モデルクラス(json);
もちろん、コンポジットされて使う要素の葉のクラスであれば、data以外にも、親の情報などを受取ることもあるかもしれません。
これが基本的な指針です。説明と完了したかどうかを持つモデルは次のようになります。JSONのキーとオブジェクトのフィールド名/メンバー変数名が同じであれば特に悩むことはありません。m.prop
はJSON.stringify
からは空気のように扱われますので、次の読み込み側だけ作ればOKです。
class Todo {
constructor(data) {
this.description = m.prop(data.description);
this.done = m.prop(data.done);
}
}
もちろん、読み書きの際にデータ形式を変換したり、新しい要素を追加したり、JS側で追加した一時的な要素を除外したりもできます。例えば、日付はepoch(秒)でやってくるとします。JS側ではDate
型で持たせるとします。Date
型とJSONの相性が悪いのはご存知の通りです。
こんな感じで読み書きの際の変換を定義して上げることができます。
class Todo {
constructor(data) {
this.description = m.prop(data.description);
// サーバからやってくるデータは未終了のタスクだけなので、doneにはfalseを設定
this.done = m.prop(false);
// JSのepochはミリ秒単位なので1000倍
this.dueDate = m.prop(new Date(date.dueDate * 1000));
}
toJSON() {
if (this.done()) {
null; // 終わった項目は削除(実際の削除はTodoを持つリスト側でフィルタリングする必要があります)
}
return {
description: this.description(),
dueDate: Math.floor(this.dueDate().valueOf() * 0.001)
};
}
}
もちろん、木構造をしているモデルを作ることも可能です。コンストラクタで、JSONの一部を取り出して、他のモデルクラスに渡してnewしていけばOKです。
サーバインタフェース
JSONとの相性が良い(上記のインタフェースを満たしている)場合は、サーバからのレスポンスをクラスにキャストの仕組みを使います。クラスにそれ専用のstaticメソッドを用意すれば良いでしょう。
サーバからのレスポンスが、モデルクラスが期待しているJSONそのものの場合、もしくはそれの配列であれば、m.request
のtype
にクラスを渡せばOKです。
class Todo {
// 単体のインスタンスをサーバから取得
static load(id) {
return m.request({
url: `https://example.com/todo/${id}`
type: Todo
});
}
// インスタンスのリストをサーバから取得
static loadByList() {
return m.request({
url: 'https://example.com/todo/'
type: Todo
});
}
}
JSONじゃなくて他のフォーマットを変換する、あるいはHATEOASなレスポンスで一段上にJSONのレイヤーがあってlinkタグとかもあって、そこを除外する必要がある、みたいなケースでは、変換関数をm.requestのパラメータを追加して事前に変換することもできます。そのあたりはm.requestのドキュメントを参照してください。
ビューやビューモデルから見たモデル
Mithrilのモデルは当然、ビューなどから使われるためにデザインされています。
ビューモデルやビューから見たモデルは、プロパティを保持しているデータ構造になります。
プロパティの紹介でも説明しましたが、プロパティは双方向を実現する上で便利なデータ構造です。プロパティアクセスのシグネチャは単なる関数呼び出しでしかないので、後からメソッドにロジックを足したりもできます。
ビューやビューモデル向けのインタフェース
ビューモデルでロードする時は次のようにします。
this.todo = m.prop(null);
Todo.load(id).then(this.todo);
this.todos = m.prop([]);
Todo.loadByList().then(this.todos);
複数のファイルのロード待ちをする時には次のようにします。
this.todo = m.prop(null);
this.user = m.prop(null);
m.sync([
Todo.load(id).then(this.todo),
User.load(userID).then(this.user)
]).then(() => { /*両方ロードが終わった*/ });
ただし、Mithrilのビューは、サーバリクエストを複数行った場合は、すべてが完了するまでは描画を延期します。そのため、中途半端に1つだけデータがある状態で描画を行うことはありません。複数のデータを使うのがビューだけであれば、待ち合わせ処理などを行う必要はありません。
AとBの両方のデータをロードして、それを元にCのデータのリクエストを行う必要がある、みたいなケースでは必要になるかもしれませんが、そのような場合には、一度のリクエストで必要なデータを取得できるAPIの口を追加する方が良いでしょう。そのため、syncはビュー向けにはほぼ使うことはないと思います。