m.request



この関数はウェブサービスと協調するための高度なユーティリティです。この機能を使うと、非同期で動作するコードを、比較的手続き的に記述することができます。

デフォルトではサーバのレスポンスはJSONとみなして解釈します。また、オプションでレスポンスデータを元にクラスのインスタンスを作ることができます。

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

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

基本的な使い方

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のインタフェース(thennableとも呼ばれる)を持っています。この機能は、ウェブサービスからデータが帰ってきた後の操作をキューイングするのに使います。

この機能のもっとも簡単な使い方は、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)

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

thennableの仕組みは主に以下の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のthennableは、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レスポンスが実際に処理される前に、デバッグ処理を差し込んでいます:

//関数プログラミングと相性の良いconsole.log
var log = function(value) {
    console.log(value)
    return value
}

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

//レスポンスには`[{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;}
});

FormDataによるファイルのアップロード

HTML5 FormDataをリクエストのペイロードとして使うには、serializeをオーバーライドする必要があります。デフォルトではserializeはオブジェクトをJSONに変換しますが、FormDataをペイロードとして使う場合には、オブジェクトをそのまま流します。

//ファイルはHTML5のドラッグアンドドロップイベントで渡されたとする
var file = e.dataTransfer.files[0]

var data = new FormData();
data.append("file", file)

m.request({
    method: "POST",
    url: "/upload",
    data: data,
    serialize: function(data) {return data}
})

可変データ・フォーマットの使用

デフォルトでは、Mithrilは成功時も失敗時もレスポンスはJSONフォーマットで帰ってくるものとみなしてパースしようとしますが、サーバの中には404のようなエラーコードを返す時にJSONを返さないものもあります。

この問題を回避するには、extractを使用します。

var nonJsonErrors = function(xhr) {
  return xhr.status > 200 ?JSON.stringify(xhr.responseText) : xhr.responseText
}

m.request({method: "GET", url: "/foo/bar.x", extract: nonJsonErrors})
  .then(function(data) {}, function(error) {console.log(error)})

レスポンス中のメタデータの分解

extractメソッドはHTTPレスポンスヘッダのメタデータや、XMLHttpRequestのステータスフィールドを読み込むのに使用します。

var extract = function(xhr, xhrOptions) {
    if (xhrOptions.method == "HEAD") return xhr.getResponseHeader("x-item-count")
    else return xhr.responseText
}

m.request({method: "POST", url: "/foo", extract: extract});

カスタムリクエストのリジェクト

もし、promiseのリジェクトハンドラ内でエラー条件のハンドリングを行いたい場合は、extractの中から、promiseのリジェクトに向けてErrorを投げることができます。

もしJSONとしては不正なレスポンスをプロダクション環境のサーバから受け取ったとして、ユーザに対してサーバがオフラインであるというメッセージを表示したいとします。

var extract = function(xhr, xhrOptions) {
    try {
        return JSON.stringify(xhr.responseText)
    }
    catch (e) {
        //e instanceof SyntaxError == true
        //デフォルトでは`e`はMithrilのpromiseの例外モニタがキャッチして、コンソールに向けて再度投げる
        //このエラーはPromises/A+の仕様にしたがって下流のpromiseのリジェクトに渡される
        throw new Error("サーバがオフラインです")
    }
}

m.request({method: "POST", url: "/foo", extract: extract});

詳細については、こちらのpromiseの例外モニタを参照してください。


バックエンドのXMLHttpRequestの設定

configオプションを使って、ネイティブのXMLHttpRequestインスタンスの設定を任意に設定したり、他の方法ではアクセスできないプロパティにアクセスすることができます。

下記の例は、サーバがリクエストとしてContent-Type: application/jsonヘッダを期待しているときに、それを行うための方法を示したサンプルです。

var xhrConfig = function(xhr) {
    xhr.setRequestHeader("Content-Type", "application/json");
}

m.request({method: "POST", url: "/foo", config: xhrConfig});

リクエストの中断

configを通じて、XMLHttpRequestのインスタンスに対して、リクエストの中断を行わせることもできます。この方法は、onprogressを付与するのにも使えます。

var transport = m.prop();

m.request({method: "POST", url: "/foo", config: transport});

//`transport` getter-setterにはXMLHttpRequestのインスタンスが含まれる。
transport().abort();

JSONPの使用

JSONPリクエストを行うには、methodではなく、dataTypeを設定します。通常のJSONPリクエストで必要になるcallbackクエリー文字列パラメータを設定する必要はありません。Mithrilが内部で行います。

m.request({dataType: "jsonp", url: "/api/User"});

Flickrなどの一部のサービスは、callbackパラメータで指定されたcallbackを呼び出すというJSONPの規約に従っていません。コールバック関数を表すクエリー文字列を指定するには、callbackKeyを指定します。

m.request({
    dataType: "jsonp",
    callbackKey: "jsoncallback",
    url: "http://api.flickr.com/services/feeds/photos_public.gne?tags=monkey&tagmode=any&format=json"
});

ウェブサービスのリクエスト完了前のレンダリング

デフォルトでは、Mithrilはウェブサービスへのリクエストが完了するまでは再描画を待ちます。この仕組により、ビューが非同期データのデータにアクセスするときも、データがまだ利用できないために表示がおかしくなる、ということがありません。

しかし、時々ウェブサービスのリクエストが完了する前に再描画を行いたいと思うことがあります。多くのウェブサービスに並行でアクセスして、アクセスが遅いのがその中の1つだけだったり、もしくは再描画するのにレスポンスがいらない、といったこともありえます。

backgroundオプションをtrueに設定すると、リクエストが再描画のプロセスに干渉することはなくなります。このオプションを有効にすると、データが利用可能になる前にビューがデータにアクセスしようとします。m.requestに渡すgetter-setterに初期値を渡すという防衛的なコーディングを行えば、null参照例外が発生する可能性を抑えられます:

var demo = {}

demo.controller = function() {
    return {
        users: m.request({method: "GET", url: "/api/user", background: true, initialValue: []})
    }
}

//ビュー内
demo.view = function(ctrl) {
    //このビューはデータが利用可能になる前にレンダリングされる
    //初期値としてundefinedに代わりに空の配列を定義したため、この.mapはエラーにならない
    return ctrl.users().map(function(user) {
        return m("div", user.name)
    })
}

シグニチャ

シグニチャの読み方

Promise request(Options options)

where:
    Promise :: GetterSetter { Promise then(any successCallback(any value), any errorCallback(any value)) }
    GetterSetter :: any getterSetter([any value])
    Options :: XHROptions | JSONPOptions
    XHROptions :: Object {
        String method,
        String url,
        [String user,]
        [String password,]
        [Object<any> data,]
        [Boolean background,]
        [any initialValue,]
        [any unwrapSuccess(any data, XMLHttpRequest xhr),]
        [any unwrapError(any data, XMLHttpRequest xhr),]
        [String serialize(any dataToSerialize),]
        [any deserialize(String dataToDeserialize),]
        [any extract(XMLHttpRequest xhr, XHROptions options),]
        [void type(Object<any> data),]
        [XMLHttpRequest?config(XMLHttpRequest xhr, XHROptions options)]
    }
    JSONPOptions :: Object {
        String dataType,
        String url,
        String callbackKey,
        String callbackName,
        Object<any> data
    }
  • XHROptions options

    XMLHttpRequestに渡すオプションのマップ。

    • String method

      HTTPのメソッド。このオプションは、"GET""POST""PUT""DELETE""HEAD""OPTIONS"のどれかを設定します。

    • String url

      リクエストを送るURL。もしURLが違うドメインであれば、対象となるサーバは Access-Control-Allow-Origin: *ヘッダを付与するなど、クロスドメインのリクエストを許可する設定になっている必要があります。

    • String user (optional)

      HTTP認証のユーザ名。デフォルトはundefined

    • String password (optional)

      HTTP認証のパスワード。デフォルトはundefined

    • Object data (optional)

      送信するデータ。設定されているmethodによって、適切な方法でシリアライズされて、適切なペイロードに格納されます。

    • Boolean background (optional)

      m.request中にテンプレートのレンダリング行うかどうかの設定。デフォルトはfalseです。

      このオプションがtrueであれば、requestメソッドはm.startComputation / m.endComputationを呼びません。そのため、リクエストの完了がビュー更新のトリガーにはならず、データ更新があってもビューが更新されません。この操作は、ユーザの操作によらず行われるバックグラウンドの操作を実行する場合に便利です。

      バックグラウンドのリクエスト後に再描画を行わせたい場合は、m.redrawもしくはm.startComputation / m.endComputationを使用してください。

      var demo = {}
      
      demo.controller = function() {
        var users = m.request({method: "GET", url: "/api/users", background: true, initialValue: []})
        users.then(m.redraw)
        return {users: users}
      }
      
      demo.view = function(ctrl) {
        //このビューは初回と上記のリクエストの完了後の2回再描画が行われる
        return m("div", [
            ctrl.users().map(function(user) {
                return m("div", user.name)
            })
        ])
      }
      

      backgroundをtrueに設定するリクエストの「全ての」リクエストに対してinitialValueオプションを設定するのを強く推奨します。

      複数のバックグラウンドのAJAXリクエストを行った場合は、個別のリクエストの完了後に再描画を行うのではなく、m.syncを使って待ち合わせを行い、一度だけ再描画を行うのが推奨です:

      var demo = {}
      
      demo.controller = function() {
        var users = m.request({method: "GET", url: "/api/users", background: true, initialValue: []})
        var projects = m.request({method: "GET", url: "/api/projects", background: true, initialValue: []})
      
        m.sync([users, projects]).then(m.redraw)
      
        return {users: users, projects: projects}
      }
      

      リクエスト値がnullになる可能性がある場合は、nullチェックを行ってください。

      var demo = {}
      
      demo.controller = function() {
        var user = m.request({method: "GET", url: "/api/users/1", background: true, initialValue: null})
        user.then(m.redraw)
        return {user: user}
      }
      
      demo.view = function(ctrl) {
        return m("div", [
            //最初の描画時はユーザがいないが、エラーにはならない
            ctrl.user ?ctrl.user.name : "no user"
        ])
      }
      
    • any initialValue (optional)

      リクエスト完了前に、この関数が返すgetter-setterが内部で保持する初期値。backgroundオプションを使用するときにこのオプションを使用すると、リクエスト完了前にビューがgetter-setterにアクセスするときにnullチェックを行う手間が減ります。

      このオプションは将来落とし穴に落ちないように設定することを強く勧めます。

    • any unwrapSuccess(any data, XMLHttpRequest xhr) (optional)

      レスポンス成功時に、レスポンスに含まれるメタデータなどからデータを取り出す時に使用するプリプロセッサです。

      この設定がfalsyな値だったときのデフォルトの動作は、function(value) {return value}という関数と等価です。

      もし返り値が{data: [{name: "John"}, {name: "Mary"}]}というJSONで、unwrap関数としてfunction(response) {return response.data}という関数が渡された場合には、typeパラメータの評価時に[{name: "John"}, {name: "Mary"}]というデータを返します。

      • Object | Array data

        unwrapするデータ

      • returns Object | Array unwrappedData

        unwrapされたデータ

    • any unwrapError(any data, XMLHttpRequest xhr) (optional)

      レスポンス失敗時に、レスポンスに含まれるメタデータなどからデータを取り出す時に使用するプリプロセッサです。

      この設定がfalsyな値だったときのデフォルトの動作は、function(value) {return value}という関数と等価です。

      • Object | Array data

        unwrapするデータ

      • returns Object | Array unwrappedData

        unwrapされたデータ

    • String serialize(any dataToSerialize) (optional)

      リクエストデータをシリアライズするときに使用するメソッド。

      この設定がfalsyな値だったときのデフォルト動作は、JSON.stringifyです。

      • any dataToSerialize

        シリアライズするデータ。

      • returns String serializedData

    • any deserialize(String dataToDeserialize) (optional)

      レスポンスデータをデシリアライズするメソッド。

      この設定がfalsyな値だったときのデフォルト動作は、JSON.parseです。

      • String dataToDeserialize

        デシリアライズするデータ。

      • returns any deserializedData

    • any extract(XMLHttpRequest xhr, XHROptions options) (optional)

      XMLHttpRequest.からデータを取り出す時に使うメソッドです。このオプションはレスポンスヘッダやステータスフィールドに必要なデータがある場合に便利です。

      この設定がfalsyな値だったときのデフォルトの動作は、xhr.responseTextを返す関数です。

    • void type(Object data) (optional)

      レスポンスオブジェクト、もしくはレスポンスが配列の場合はその子供は、typeで定義されているクラスコンストラクタに渡されます。

      もしこのパラメータがfalxyな値だった時は、デシリアライズされたデータはそのまま渡されます。

      もしtypeとして次のクラスが渡されたとします。

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

      そして、サーバから[{name: "John"}, {name: "Mary"}]というデータが帰ってくると、レスポンスはUserクラスのインスタンスを2つ含む配列になります。

    • XMLHttpRequest?config(XMLHttpRequest xhr, XHROptions options) (optional)

      opensendの間に呼ばれる初期化関数です。この関数を使うと、リクエストヘッダを追加したり、XMLHttpRequestのuploadプロパティなどXHR2の機能を呼び出すことができます。

      • XMLHttpRequest xhr

        XMLHttpRequestのインスタンス。

      • XHROptions options

        optionsパラメータは、m.request呼び出し時に渡された引数。

      • returns XMLHttpRequest?xhr

        この関数を使うと、Mithrilが生成したXHRインスタンスの代わりに、XDomainRequestインスタンスなど、XHRのように動作するまったく別のオブジェクトに置き換えることもできます。

  • returns Promise promise

    AJAXリクエストが完了したときに呼ばれるコールバックをバインドすることが可能なpromise。


  • JSONPOptions options

    JSONPリクエストのためのオプションのマップ。

    • String dataType

      "jsonp"でなければなりません。

    • String url

      リクエストを送るURL。もしURLが違うドメインであれば、対象となるサーバは Access-Control-Allow-Origin: *ヘッダを付与するなど、クロスドメインのリクエストを許可する設定になっている必要があります。

    • String callbackKey

      レスポンス受け取るコールバック関数を定義する、クエリー文字列の名前。デフォルト値は"callback"。

      foo.com/?jsonpCallback=doSomethingなど、一般的な規約とはことなるjsonpコールバック定義をさせるウェブサービスを章するときに使います。

    • String callbackName

      The name of callback function to be called by the response. Defaults to a unique auto-generated name

      This option is useful for web services serving static files and to prevent cache busting.

    • Object data (optional)

      送信するデータ。設定されているmethodによって、適切な方法でシリアライズされて、適切なペイロードに格納されます。