m.request
- 基本的な使い方
- ウェブサービスのデータの処理
- リダイレクトするコードのバインド
- エラーのバインド
- 操作のキューイング
- ウェブサービスから帰ってきたデータをクラスにキャスト。
- レスポンスデータの展開
- 異なるデータ転送フォーマットを使用する
- FormDataによるファイルのアップロード
- 可変データ・フォーマットの使用
- レスポンス中のメタデータの分解
- カスタムリクエストのリジェクト
- バックエンドのXMLHttpRequestの設定
- リクエストの中断
- JSONPの使用
- ウェブサービスのリクエスト完了前のレンダリング
- シグニチャ
この関数はウェブサービスと協調するための高度なユーティリティです。この機能を使うと、非同期で動作するコードを、比較的手続き的に記述することができます。
デフォルトではサーバのレスポンスは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はunwrapSuccess
、unwrapError
という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)
open
とsend
の間に呼ばれる初期化関数です。この関数を使うと、リクエストヘッダを追加したり、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
によって、適切な方法でシリアライズされて、適切なペイロードに格納されます。