ウェブサービス

Mithrilはウェブサービス側と連係をするための高度なユーティリティを提供しており、非同期のコードを手続き的に書くことができます。

提供されるこの便利な機能には次のようなものがあります:

  • 非同期のレスポンスが後で格納されるコンテンナを事前に参照しておく機能
  • 非同期のリクエストが完了した後に実行される操作をキューに貯めておく機能
  • レスポンスを好きなクラスにキャストする機能
  • メタデータのプロパティを含むレスポンスを展開する機能

基本的な使い方

m.requestを通常の使用法で使うと、AJAXのリクエストが完了した後に結果が格納されるm.prop getter-setterを返します。

getter-setterを返すことは参照を安いコストでコード内に渡すことができて、値が必要になったときにデータの実体を取り出すことができることを意味します。

var users = m.request({method: "GET", url: "/user"});

//レスポンスには`[{name: "John"}, {name: "Mary"}]`というデータが格納されると想定
//そのため、ビューなどの中で解決されると、`users` getter-setterはユーザの配列を持ちます
//例: users() //[{name: "John"}, {name: "Mary"}]

getter-setterは、AJAXリクエストが完了するまではundefined値を返すことに注意してください。そのため、データを早期にアンラップしようとするとおそらくエラーになるでしょう。

返されるgetter-setterはpromiseのインタフェース(thenableとも呼ばれる)を持っています。この機能は、ウェブサービスからデータが帰ってきた後の操作をキューイングするのに使います。

この機能のもっとも簡単な使い方は、m.propを使って関数型的な値の割り当てを行うことです(上記のコードと等価です)。あらかじめ作成したgetter-setterを.thenメソッドの引数で渡して束縛することができます:

var users = m.prop([]); //default value

m.request({method: "GET", url: "/user"}).then(users)
//レスポンスには`[{name: "John"}, {name: "Mary"}]`というデータが格納されると想定
//そのため、ビューなどの中で解決されると、`users` getter-setterはユーザの配列を持ちます
//例: users() //[{name: "John"}, {name: "Mary"}]

この文法を使うと、パイプ処理が次の処理を起動する前に中間結果を束縛することができるようになります。

var users = m.prop([]); //デフォルト値
var doSomething = function() { /*...*/ }

m.request({method: "GET", url: "/user"}).then(users).then(doSomething)

代入構文も、thenableを使った構文も同じ結果になりますが、前者の例の方が読みやすいため、何か制約がない限りはこちらを使用する方がおすすめです。

thenableの仕組みは主に以下の3ヶ所で使われることを想定しています:

  • モデルレイヤ内: ウェブサービスから受信したデータを変換処理をする場合。例えばウェブサービス側でサポートしていないフィルタリングをクライアント側で行う場合など。
  • コントローラレイヤ内: 条件によって、リダイレクトするコードをバインドする場合。
  • コントローラレイヤ内: エラーメッセージをバインドする場合。

ウェブサービスのデータの処理

このステップはモデルレイヤ内で完結します。この処理をコントローラのレベルで行うことができますが、Mithrilの哲学としては推奨していません。コントローラと関係ない処理であったとしても、コントローラとロジックが結びついてしまうと再利用が難しくなるからです。

下記のサンプルのlistEvenメソッドは、IDが偶数のユーザを含むリストのみを返すgetter-setterを返しています。

//モデル
var User = {}

User.listEven = function() {
    return m.request({method: "GET", url: "/user"}).then(function(list) {
        return list.filter(function(user) {return user.id % 2 == 0});
    });
}

//コントローラ
var controller = function() {
    return {users: User.listEven()}
}

リダイレクトするコードのバインド

このステップはコントローラレイヤ内で完結します。この処理をモデルレイヤで行うことも出来ますが、Mithrilの哲学としては非推奨です。リダイレクト処理とモデルが結びついてしまうと、再利用が難しくなります。

下記の例では、前の例で定義したモデルのlistEvenメソッドを使用します。ユーザのリストが空の場合に他のページにリダイレクトするというコントローラレベルの機能をモデル完了後の操作としてキューイングしています。

//コントローラ
var controller = function() {
    return {
        users: User.listEven().then(function(users) {
            if (users.length == 0) m.route("/add");
        })
    }
}

エラーのバインド

Mithrilのthenableは、2つのオプションのパラメータを持っています。最初のパラメータはウェブサービスへのリクエストが問題なく完了した時に呼ばれます。2つ目のパラメータはエラーで完了したときに呼ばれます。

Mithrilではエラーのバインディングはコントローラレベルで行われることを想定しています。もちろん、モデルレベルで行うこともできますが、全ての関連する機能を正しく動かすためには、多くのコードを書く必要がああります。

下記のサンプルでは、error getter-setterと、前のサンプルで紹介したコントローラをバインドしています。error変数は、サーバアクセスがうまく行かなかった時に呼び出されます。

//コントローラ
var controller = function() {
    this.error = m.prop("")

    this.users = User.listEven().then(function(users) {
        if (users.length == 0) m.route("/add");
    }, this.error)
}

もしコントローラが、サーバアクセスが成功した時に実行すべき処理がない場合でも、次のように書くことでエラー処理のバインドが行えます:

//コントローラ
var controller = function() {
    this.error = m.prop("")

    this.users = User.listEven().then(null, this.error)
}

操作のキューイング

これまで見てきたとおり、レスポンスで返されたデータに対して処理のオペレーションをいくつもチェーンさせて追加することができます。一般的に、この機能は以下の3つの場面で必要となります:

  • モデルレベルのメソッド内で、コントローラやビューに対して処理しやすい形式のデータへの変換をクライアント側で行わなければならない場合。
  • コントローラ内で、モデルサービスが改良した後にリダイレクトさせたい場合。
  • コントローラ内で、エラーメッセージをバインドする場合。

下記のサンプルはAJAXレスポンスが実際に処理される前に、デバッグ処理を差し込んでいます:

var users = m.request({method: "GET", url: "/user"})
    .then(log)
    .then(function(users) {
        //レスポンスにユーザをもう一人追加
        return users.concat({name: "Jane"})
    })

function log(value) {
    console.log(value)
    return value
}

//レスポンスには`[{name: "John"}, {name: "Mary"}]`というデータが格納されると想定
//そのため、ビューなどの中で解決されると、`users` getter-setterはユーザの配列を持ちます
//例: users() //[{name: "John"}, {name: "Mary"}, {name: "Jane"}]


ウェブサービスから帰ってきたデータをクラスにキャスト。

JSON表現をクラスに自動変換することができます。POJO(plain old JavaScript objects)の場合はすべてのフィールドが公開状態になってしまうため、この機能を使うとオブジェクト内のプロパティへのアクセス方法をコントロールしやすくなります。

次のサンプルでは、User.listメソッドは、Userインスタンスのリストを返します。

var User = function(data) {
    this.name = m.prop(data.name);
}

User.list = function() {
    return m.request({method: "GET", url: "/user", type: User});
}

var users = User.list();
//レスポンスには`[{name: "John"}, {name: "Mary"}]`というデータが格納されると想定
//そのため、ビューなどの中で解決されると、`users` はUserインスタンスのリストを格納します
//例: users()[0].name() == "John"

レスポンスデータの展開

少なくない数のウェブサービスが、それぞれのデータをメタデータ入りのオブジェクトでラップして返してきます。

MithrilはunwrapSuccessunwrapErrorという2つのコールバックを提供しており、これらを使って、それぞれのデータをアンラアップすることが可能になります。

これらのフックを使うと、レスポンスが成功したかどうかによって、レスポンスデータの違う箇所をアンラップできます。

var users = m.request({
    method: "GET",
    url: "/user",
    unwrapSuccess: function(response) {
        return response.data;
    },
    unwrapError: function(response) {
        return response.error;
    }
});

//レスポンスには`{data: [{name: "John"}, {name: "Mary"}], count: 2}`というデータが格納されると想定
//そのため、ビューなどの中で解決されると、`users` getter-setterはユーザの配列を持ちます
//例: users() //[{name: "John"}, {name: "Mary"}]

異なるデータ転送フォーマットを使用する

デフォルトでは、m.requestはウェブサービスとのデータの送受信にJSONを使います。serializeオプションと、deserializeオプションを提供すると、この動作を変更することができます:

var users = m.request({
    method: "GET",
    url: "/user",
    serialize: mySerializer,
    deserialize: myDeserializer
});

よくある変更方法としては、変換せずに帰ってきた入力をそのままアプリケーションに渡す方式です。次のサンプルはテキストファイルをそのままプレーンな文字列として受ける取る方法を紹介しています。

var file = m.request({
    method: "GET",
    url: "myfile.txt",
    deserialize: function(value) {return value;}
});