request(options)
- コア
- オプショナル
- ツール
説明
XHR (別名 AJAX) リクエストを行い、Promiseを返します。
m.request({
method: "PUT",
url: "/api/v1/users/:id",
data: {id: 1, name: "test"}
})
.then(function(result) {
console.log(result)
})
シグニチャ
promise = m.request([url,] options)
引数 | 型 | 必須 | 説明 |
---|---|---|---|
url |
String |
No | これが存在していたら、{method: "GET", url: url} というオプションと等価です。options 引数に渡されたオプションは、この短縮形式のデフォルトオプションを上書きします。 |
options.method |
String |
No | 使用するHTTPメソッドです。このオプションは、GET , POST , PUT , PATCH , DELETE , HEAD , OPTIONS のどれかでなければなりません。デフォルトはGET です。 |
options.url |
String |
Yes | リクエストを送る先のURLです。。URLは絶対パスでも相対パスでも使用できますし、 変数(interpolation)を含むことも可能です。 |
options.data |
any |
No | URLの変数に挿入されたり、クエリー文字列(GETリクエスト)やボディー(他のメソッドのリクエスト)として文字列化されるデータです。 |
options.async |
Boolean |
No | リクエストが非同期であるべきかどうか。デフォルト値はtrue です。 |
options.user |
String |
No | HTTP認証のユーザー名。デフォルトはundefined です。 |
options.password |
String |
No | HTTP認証のパスワード。デフォルトはundefined です。このオプションはXMLHttpRequest との互換性のために用意されていますが、このメソッドはパスワードをプレーンテキスト形式で送付してしまうため、避けるべきです。 |
options.withCredentials |
Boolean |
No | サードパーティードメインに対してクッキーを送付するかどうかを決定します。デフォルト値はfalse です。 |
options.config |
xhr = Function(xhr) |
No | 低レベルの設定を行うために、APIの内部に隠れているXMLHttpRequestオブジェクトに触れるようにします。デフォルトは恒等写像(入力をそのまま返す関数)です。 |
options.headers |
Object |
No | 送信前に追加されるヘッダーです。options.config の直前に付与が行われます。 |
options.type |
any = Function(any) |
No | レスポンスの中でオブジェクトごとに適用されるコンストラクタです。デフォルトは恒等写像(入力をそのまま返す関数)です。 |
options.serialize |
string = Function(any) |
No | data に適用されるシリアライズメソッドです。デフォルトはJSON.stringify 、あるいは、options.data がFormData の時は恒等写像(function(value) {return value} )となります。 |
options.deserialize |
any = Function(string) |
No | xhr.responseText に適用する、デシリアライズ用のメソッドです。デフォルトは、空のレスポンスに対してnull を返すようにした、JSON.parse のラッパー関数です。もしextract が定義されていると、deserialize はスキップされます。 |
options.extract |
any = Function(xhr, options) |
No | XMLHttpRequestのレスポンスをどのように読み込むかを指定するフック関数です。レスポンスデータを処理したり、ヘッダー、クッキーを読み込むのに便利なメソッドです。デフォルトでは、この関数はxhr.responseText を返します。これはその後deserialize に渡されます。もしカスタムのextract コールバックが提供されると、リクエストに使ったXMLHttpRequestインスタンスが xhr パラメータとして、m.request 呼び出し時に渡したオブジェクトが options として渡されます。さらに、deserialize がスキップされ、extractコールバックから返された値は自動ではJSONでパースされなくなります。 |
options.useBody |
Boolean |
No | true をセットすると、GET リクエスト時にHTTPのボディセクションをdata に使うようになります。 false を設定すると他のHTTPメソッドのときにクエリー文字列を使うようになります。GET リクエスト時のデフォルトはfalse で、他のメソッドのリクエスト時のデフォルトはtrue です。 |
options.background |
Boolean |
No | このパラメータをfalse にすると、リクエスト完了時にマウントされているコンポーネントを再描画します。true の場合は再描画を行いません。デフォルトはfalse です。 |
返り値 | Promise |
このPromiseが解決されるとextract , deserialize , type メソッドで処理されたレスポンスデータが渡されます。 |
どのように動作するのか
m.request
ユーティリティはXMLHttpRequest
の薄いラッパーです。これを使うとHTTPリクエストを通じて、リモートサーバーにデータを保存したり、データを取得してくることができます。
m.request({
method: "GET",
url: "/api/v1/users",
})
.then(function(users) {
console.log(users)
})
m.request
を呼ぶとPromiseが返ってきます。また、そのPromiseのチェーンが完了すると再描画が行われます。
デフォルトではm.request
はレスポンスがJSONフォーマットであると想定してパースし、JavaScriptのオブジェクトや配列に変換します。
一般的な使用法
次のコードはm.request
を使ってサーバーからデータを取得してくるコンポーネントのサンプルです。
var Data = {
todos: {
list: [],
fetch: function() {
m.request({
method: "GET",
url: "/api/v1/todos",
})
.then(function(items) {
Data.todos.list = items
})
}
}
}
var Todos = {
oninit: Data.todos.fetch,
view: function(vnode) {
return Data.todos.list.map(function(item) {
return m("div", item.title)
})
}
}
m.route(document.body, "/", {
"/": Todos
})
サーバーURL/api/items
がJSONオブジェクトの配列を返すものとします。
最後の行でm.route
が呼ばれると、Todos
コンポーネントが初期化されます。初期化されるとoninit
が呼び出され、この中でm.request
が実行されます。この関数は非同期でサーバーからオブジェクトの配列を取得します。「非同期」とは、サーバーからレスポンスが返ってくるのを待つ間に、JavaScriptの他のコードの実行が継続されることを意味します。このサンプルの場合、fetch
が完了したときにはまだサーバーからレスポンスが返ってきていないので、Data.todos.list
に格納されているオリジナルの空の配列を使って表示を行います。サーバーからレスポンスが返ってくると、オブジェクトの配列のitems
がData.todos.list
に割り当てられ、再描画が行われ、ToDoのタイトルが格納されている<div>
タグが挿入されます。
ローディングアイコンとエラーメッセージ
次のコードは前述のサンプルに、ローディングのインジケーターと、エラーメッセージを追加したサンプルです。
var Data = {
todos: {
list: null,
error: "",
fetch: function() {
m.request({
method: "GET",
url: "/api/v1/todos",
})
.then(function(items) {
Data.todos.list = items
})
.catch(function(e) {
Data.todos.error = e.message
})
}
}
}
var Todos = {
oninit: Data.todos.fetch,
view: function(vnode) {
return Data.todos.error ?[
m(".error", Data.todos.error)
] : Data.todos.list ?[
Data.todos.list.map(function(item) {
return m("div", item.title)
})
] : m(".loading-icon")
}
}
m.route(document.body, "/", {
"/": Todos
})
最初のサンプルに対して、いくつか違いがあります。まず、Data.todos.list
の初期値がnull
になっています。また、エラーメッセージを保持する追加フィールドのerror
が追加されており、エラーがあったらエラーメッセージを、Data.todos.list
が配列でなかったらローディングアイコンを表示するようにTodos
コンポーネントが書き換えられています。
動的URL
リクエストするURLには変数(interpolation)を含めることができます。
m.request({
method: "GET",
url: "/api/v1/users/:id",
data: {id: 123},
}).then(function(user) {
console.log(user.id) // logs 123
})
上記の例では:id
の項目はオブジェクト{id: 123}
を使って置き換えられ、リクエスト先はGET /api/v1/users/123
となります。
Idata
プロパティにマッチするデータがなければ、変数は無視されます。
m.request({
method: "GET",
url: "/api/v1/users/foo:bar",
data: {id: 123},
})
上記のコードではリクエスト先はGET /api/v1/users/foo:bar
となります。
リクエストの中断
リクエストを中断したくなるケースがあります。例えば、自動補完やサジェストウィジェットの場合、最後にリクエストした補完だけを利用したいはずです。よくある自動補完ではユーザーが入力するたびに何度もリクエストが発生しますが、ネットワークの性質上、結果の順序が正しく返ってくるとは限りません。もし、最後に実行したリクエストよりも後に他のリクエストが完了すると、本来よりも精度が低い(場合によっては間違った)データが表示されることになります。
options.config
パラメータを使うと、m.request()
の内部のXMLHttpRequest
オブジェクトに触れる事ができます。これを使ってオブジェクトの参照を保存しておくことで、必要に応じてabort
メソッドを呼ぶこともができます。
var searchXHR = null
function search() {
abortPreviousSearch()
m.request({
method: "GET",
url: "/api/v1/users",
data: {search: query},
config: function(xhr) {searchXHR = xhr}
})
}
function abortPreviousSearch() {
if (searchXHR !== null) searchXHR.abort()
searchXHR = null
}
ファイルアップロード
ファイルをアップロードする時はまず、File
オブジェクトの参照を取得します。もっとも簡単な方法は<input type="file">
を使うことです。
m.render(document.body, [
m("input[type=file]", {onchange: upload})
])
function upload(e) {
var file = e.target.files[0]
}
上記のスニペットはファイルのinputタグをレンダリングします。ユーザーがファイルを選択すると、onchange
イベントが起動し、upload
関数が呼ばれます。e.target.files
がFile
オブジェクトの配列になっています。
次に、マルチパートリクエストを作成するためのFormData
オブジェクトを作ります。これを使うと、ファイルデータを含められるように特別にフォーマットされたHTTPリクエストが行えるようになります。
function upload(e) {
var file = e.target.files[0]
var data = new FormData()
data.append("myfile", file)
}
次にm.request
を呼びます。options.method
にはボディが指定できるHTTPメソッド(POST
, PUT
, PATCH
)を設定し、FormData
オブジェクトをoptions.data
に設定します。
function upload(e) {
var file = e.target.files[0]
var data = new FormData()
data.append("myfile", file)
m.request({
method: "POST",
url: "/api/v1/upload",
data: data,
})
}
マルチパートフォームが受け取れるサーバーであれば、myfile
キーに格納されたファイル情報が取得できます。
複数ファイルのアップロード
ひとつのリクエストで複数ファイルの同時アップロードも行えます。複数ファイルを同時にアップロードすると、その通信はアトミックになります。そのため、アップロード中にエラーがひとつでもあると、すべてのファイルのアップロードが無効になります。一部だけを保存することはできません。ネットワークエラーが発生したときに、ひとつでも多くのファイルを保存したいのであれば、それぞれのファイルを個別音リクエストでアップロードすべきです。
複数ファイルをアップロードするのは、FormData
オブジェクトに送信したいファイルをすべて登録するだけで行なえます。fileのinputタグを使うのであれば、multiple
属性を付与すると、ファイルのリストが得られるようになります。
m.render(document.body, [
m("input[type=file][multiple]", {onchange: upload})
])
function upload(e) {
var files = e.target.files
var data = new FormData()
for (var i = 0; i < files.length; i++) {
data.append("file" + i, file)
}
m.request({
method: "POST",
url: "/api/v1/upload",
data: data,
})
}
進捗のモニタリング
巨大なファイルのアップロードなど、リクエストが遅い場合にはプログレスのインジケーターを表示して、ユーザーにアプリケーションが正常に稼働していることを表示したくなるでしょう。
options.config
パラメータを使うと、m.request()
の内部のXMLHttpRequest
オブジェクトに触れる事ができます。これを使うと、XMLHttpRequestオブジェクトにイベントリスナーを付与するできます。
var progress = 0
m.mount(document.body, {
view: function() {
return [
m("input[type=file]", {onchange: upload}),
progress + "% completed"
]
}
})
function upload(e) {
var file = e.target.files[0]
var data = new FormData()
data.append("myfile", file)
m.request({
method: "POST",
url: "/api/v1/upload",
data: data,
config: function(xhr) {
xhr.addEventListener("progress", function(e) {
progress = e.loaded / e.total
m.redraw() // Mithrilに再描画が必要なことを伝える
})
}
})
}
上記のサンプルではファイルのinputタグを表示しています。ユーザーがファイルを選択すると、config
コールバック内でアップロードが初期化されます。progress
イベントハンドラが登録されます。このイベントハンドラは、XMLHttpRequestの進捗が更新されると呼び出されます。XMLHttpRequestのプログレスイベントはMithrilの仮想DOMエンジンでは直接扱う方法は提供していません。そのため、Mithrilにデータが変更されて再描画が必要なことを伝えるにはm.redraw()
を呼び出す必要があります。
レスポンスを指定の型にキャスト
アプリケーションのアーキテクチャによっては、レスポンスデータを特定のクラスや型に変換し、データの正規化を行ったり、ヘルパーメソッドを持たせたいことがあります。
options.type
パラメータにコンストラクタを渡すと、MithrilがHTTPレスポンスの各オブジェクトからインスタンス化を行います。
function User(data) {
this.name = data.firstName + " " + data.lastName
}
m.request({
method: "GET",
url: "/api/v1/users",
type: User
})
.then(function(users) {
console.log(users[0].name) // 名前をログに出力
})
この例では、/api/v1/users
はオブジェクトの配列を返すことを想定しています。その配列の各要素のオブジェクトごとにUser
コンストラクタが呼ばれます。内部では要素ごとにnew User(data)
というコンストラクタ呼び出しが行われます。もしレスポンスが配列ではなくてオブジェクトだった時も、このオブジェクトをdata
引数に渡してインスタンスが1つ作られます。
JSON以外のレスポンス
JSON以外を返すサーバーのエンドポイントもあります。例えば、HTMLファイル、SVG fileファイル、CSVファイル形式でレスポンスを返すようにリクエストを送ることがありえます。MithrilはデフォルトではレスポンスがJSONであるとみなしてパースしようとします。この動作を変更するには、カスタムのoptions.deserialize
関数を定義します。
m.request({
method: "GET",
url: "/files/icon.svg",
deserialize: function(value) {return value}
})
.then(function(svg) {
m.render(document.body, m.trust(svg))
})
この上記のサンプルのリクエストはSVGファイルを取得しています。このサンプルのdeserialize
関数は入力値をそのまま返しているため、パースは何も行われません。SVG文字列をそのまま信用できるHTMLとしてレンダリングしています。
もちろん、deserialize
関数により複雑な処理をさせることも可能です。
m.request({
method: "GET",
url: "/files/data.csv",
deserialize: parseCSV
})
.then(function(data) {
console.log(data)
})
function parseCSV(data) {
// このコードはサンプルを短くするための簡易実装です
return data.split("\n").map(function(row) {
return row.split(",")
})
}
このparseCSV関数が、適切に実装されたCSVパーサーが行っていることをほぼ行っていない簡易実装であることに目をつぶれば、このを実行すると文字列の配列の配列がログ出力されます。
このサンプルでは、カスタムヘッダーも役に立ちます。たとえば、SVGをリクエストする時は、適切なContent-Typeをセットしたいと考えるでしょう。デフォルトではJSONをリクエストしますが、これを上書きするにはoptions.headers
にリクエストヘッダーの名前と値が含まれているオブジェクトを渡します。
m.request({
method: "GET",
url: "/files/image.svg",
headers: {
"Content-Type": "image/svg+xml; charset=utf-8",
"Accept": "image/svg, text/*"
},
deserialize: function(value) {return value}
})
レスポンスの詳細の取得
Mithrilはデフォルトではxhr.responseText
からレスポンスのデータを取得し、JSONとしてパースして返そうとします。カスタムのoptions.extract
関数をオプションで渡すことで、サーバーのレスポンスの詳細を知ることができるます。
m.request({
method: "GET",
url: "/api/v1/users",
extract: function(xhr) {return {status: xhr.status, body: xhr.responseText}}
})
.then(function(response) {
console.log(response.status, response.body)
})
options.extract
関数のパラメータは、送受信が完了した後のXMLHttpRequestオブジェクトです。この段階はまだPromiseチェーンに渡される前の状態なので、例外を投げるとPromiseは「リジェクト状態」で終了します。
なぜHTMLではなくJSONなのか
多くのサーバーサイドフレームワークは、テンプレートエンジンを使って、データベースの値をテンプレートに挿入してHTMLを作成します。例えそのHTMLがページロードによるものでも、AJAXのためのものでも基本は同じです。その後jQueryを使ってユーザのインタラクションを取り扱います。
これと比較すると、Mithrilは厚いクライアントアプリケーションレイヤーのためにデザインされているフレームワークです。テンプレートとデータは別々にダウンロードされ、JavaScriptを使ってブラウザ上で結合されます。テンプレートの実行をブラウザ側に寄せると、サーバーリソースが解放されるため、サーバーのオペレーションコストが下がるメリットがあります。また、テンプレートとデータを分離すると、テンプレートコードをより効率的にキャッシュしたり、デスクトップやモバイルなどの異なる種類のクライアント間でコードの再利用性が上がります。他のメリットとしては、Mithrilを使うとretained modeと呼ばれるUI開発のパラダイムが利用できるようになります。このパラダイムを使うと複雑なユーザーとのインタラクションの開発をシンプルにして、メンテナンス性も向上します。
デフォルトではm.request
はレスポンスのデータはJSONであると想定しています。一般的なMithrilアプリケーションでは、このJSONデータはビューから利用されます。
サーバーで動的に生成したHTMLをMithrilでレンダリングしようとするのは避けるべきです。サーバーサイドのテンプレートシステムを使う既存のアプリケーションを作り直す場合は、アーキテクチャを再構築してそのような作業が可能かどうか判断します。シック・サーバー・アーキテクチャから、シック・クライアント・アーキテクチャへの移行には多くの労力が必要となります。ロジックをテンプレートから取り出して、ロジックのデータサービスに入れる必要が発生したりします。それにともなってテストの修正も必要でしょう。
データサービスの構造化は、アプリケーションの性質によって、さまざまな手法があります。API提供者の間では、RESTfulアーキテクチャが人気ですし、トランザクションを高度に扱う必要があればサービス指向アーキテクチャが必要となるでしょう。
なぜfetchではなくXHRなのか
fetch()
はサーバーからリソースを取得してくるための、新しいウェブのAPIです。XMLHttpRequest
と似ています。
Mithrilの m.request
はいくつかの理由により、fetch()
の代わりにXMLHttpRequest
を使っています。
fetch
はまだ十分に標準化が行われていません。そのため、仕様の変更の影響を受ける可能性があります。XMLHttpRequest
の呼び出しは、完了前に中断させられることがあります。例えば、簡易検索UIの複数リクエストの競合を避けるために必要となることがあります。XMLHttpRequest
は長時間かかる進捗情報を取得するためのフックを提供しています。XMLHttpRequest
はすべてのブラウザで対応していますが、fetch()
はInternet Explorer, Safari, ChromiumでないAndroidではサポートされていません。
現在fetch()
を使うには、欠けているブラウザサポートをおぎなうためにpolyfillが必要となりますが、これは無圧縮状態で11KBです。これはMithrilのXHRの三倍のサイズです。
ファイルサイズは大きくありませんが、自動再描画システムとの統合に加えて、URLの変数, クエリー文字列のシリアライズ, JSON-Pリクエストなど、MithrilのXHRモジュールは重要かつ、実装が簡単ではないさまざまな機能を提供しています。fetch
のpolyfillはこれらの昨日の一部をサポートしていなかったり、同じレベルの機能を提供しようとすると追加のライブラリやコードが必要になります。
さらに、MithrilのXHRはJSONベースのエンドポイントを利用するのに最適化されており、一般的な用途でシンプルに利用できます。m.request(url)
の代わりにfetch
を使うと、レスポンスのJSONデータを変換するには、fetch(url).then(function(response) {return response.json()})
などの追加の明示的なステップが必要となります。
fetch()
APIはXMLHttpRequest
と較べていくつかの技術的な利点がありますが、用途は限定されています。
- fetch()はストリーミングAPIを提供しています。これは「ビデオストリーミング」と同じ用法であって、リアクティブプログラミング用語とは異なります。この機能を使用すると、少ないコードの行数と複雑さで、よりレイテンシーが優れていて、消費メモリが少ないコードが書けます。
- また、これは Service Worker APIと統合されています。これはネットワークリクエストを制御する追加のレイヤーです。このAPIを使うと、Push通知にアクセスしたり、バックグラウンドのデータの同期などが行えるようになります。
しかし、メガバイトの単位のデータをダウンロードしない限りは、一般的な用途ではストリーミングのメリットが目に見えることはほぼありません。また小さいバッファを繰り返し利用することによるメモリの効率的な利用に関しても、再描画が頻繁になると打ち消されてしまいます。これらの理由により、m.request
の代わりにfetch()
ストリームが推奨されるのは大量のデータを扱うアプリケーションに限られるでしょう。
アンチパターンを避ける
Promiseはレスポンスデータそのものではありません。
m.request
はPromiseを返しますが、これはレスポンスそのものではありません。HTTP要求が完了するまでに長い時間待ちがかかることがあるため(ネットワーク待ち時間のため)、データを直接返すことはできません。また、JavaScript上で同期待ちをおkナウと、データが利用可能になるまでアプリケーションがフリーズします。
// 避けるべき実装
var users = m.request("/api/v1/users")
console.log("ユーザーのリスト:", users)
// `users`はユーザーのリストではなく、Promise
// 望ましい実装
m.request("/api/v1/users").then(function(users) {
console.log("ユーザーのリスト:", users)
})
License: MIT. © Leo Horie.