request(options)
Description
XHR (AJAX) リクエストを実行し、Promise オブジェクトを返します。
m.request({
method: 'PUT',
url: '/api/v1/users/:id',
params: { id: 1 },
body: { name: 'test' },
}).then(function (result) {
console.log(result);
});
Signature
promise = m.request(options)
Argument | Type | Required | Description |
---|---|---|---|
options | Object | Yes | リクエストに渡すオプション。 |
options.method | String | No | 使用する HTTP メソッド。GET 、POST 、PUT 、PATCH 、DELETE 、HEAD 、または OPTIONS のいずれかを指定します。デフォルトは GET です。 |
options.url | String | Yes | リクエストを送信する パス名。options.params の値を使用して補間できます。 |
options.params | Object | No | URL に埋め込んだり、クエリ文字列にシリアライズしたりするデータ。 |
options.body | Object | No | (POST、PUT などのリクエストの場合)リクエストボディにシリアライズするデータ。 |
options.async | Boolean | No | リクエストを非同期にするかどうか。デフォルトは true です。 |
options.user | String | No | HTTP 認証で使用するユーザー名。デフォルトは undefined です。 |
options.password | String | No | HTTP 認証用のパスワード。デフォルトは undefined です。このオプションは XMLHttpRequest との互換性のために提供されていますが、パスワードを平文でネットワーク送信するため、使用は推奨されません。 |
options.withCredentials | Boolean | No | サードパーティのドメインに Cookie を送信するかどうか。デフォルトは false です。 |
options.timeout | Number | No | リクエストがタイムアウトするまでのミリ秒数。デフォルトは undefined です (XMLHttpRequest.timeout を参照)。 |
options.responseType | String | No | レスポンスの期待される タイプ。extract が定義されている場合はデフォルトで "" 、ない場合は "json" 。responseType: "json" の場合、内部的に JSON.parse(responseText) を実行します。 |
options.config | xhr = Function(xhr) | No | XMLHttpRequest オブジェクトを操作するための設定関数。XMLHttpRequest オブジェクトを置き換えることも可能です (新しい XHR オブジェクトを返す場合)。 |
options.headers | Object | No | リクエストを送信する前にリクエストに追加するヘッダー (options.config の直前に適用されます)。 |
options.type | any = Function(any) | No | レスポンス内の各オブジェクトに適用されるコンストラクタ。デフォルトは恒等関数(恒等関数)です。 |
options.serialize | string = Function(any) | No | body に適用されるシリアライズ関数。デフォルトは JSON.stringify です。options.body が FormData または URLSearchParams のインスタンスの場合、デフォルトは恒等関数(function(value) {return value} )になります。 |
options.deserialize | any = Function(any) | No | xhr.response または正規化された xhr.responseText に適用されるデシリアライズ関数。デフォルトは 恒等関数 です。extract が定義されている場合、deserialize は無視されます。 |
options.extract | any = Function(xhr, options) | No | XMLHttpRequest レスポンスの読み取り方法をカスタマイズする関数。レスポンスデータの処理やヘッダー、Cookie の読み取りに役立ちます。デフォルトでは、options.deserialize(parsedResponse) を返す関数であり、サーバーのレスポンスステータスコードがエラーを示す場合、もしくはレスポンスが構文的に無効な場合、例外をスローします。カスタム extract 関数が指定された場合、xhr パラメータはリクエストに使用される XMLHttpRequest インスタンス、options は m.request に渡されたオブジェクトです。この場合、deserialize は無視され、extract 関数が返した値が Promise の結果としてそのまま使用されます。 |
options.background | Boolean | No | false の場合、リクエストの完了時にマウントされたコンポーネントを再描画します。true の場合、再描画しません。デフォルトは false です。 |
returns | Promise | extract 、deserialize 、type 関数を通して処理されたレスポンスデータで解決される Promise。レスポンスステータスコードがエラーを示す場合、Promise は reject されますが、extract オプションを使用することで、この拒否を回避できます。 |
promise = m.request(url, options)
Argument | Type | Required | Description |
---|---|---|---|
url | String | Yes | リクエストを送信する パス名。options.url が存在する場合、これがオーバーライドされます。 |
options | Object | No | 渡すリクエストオプション。 |
returns | Promise | extract 、deserialize 、および type メソッドを介して処理された後のレスポンスデータで解決される Promise。 |
この 2 番目の形式は、ほとんど m.request(Object.assign({url: url}, options))
と同等ですが、内部的に ES6 のグローバル Object.assign
に依存しません。
How it works
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 のオブジェクト(または配列)に解析します。
HTTP レスポンスステータスコードがエラーを示す場合、返された Promise は reject されます。extract コールバックを提供すると、Promise の拒否を防ぐことができます。
Typical usage
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>
のリストが生成されます。
Error handling
file:
以外のリクエストが 2xx または 304 以外のステータスで返された場合、エラーで拒否されます。このエラーは通常の Error インスタンスですが、いくつかの特別なプロパティがあります。
error.message
は、生のレスポンステキストに設定されます。error.code
は、ステータスコード自体に設定されます。error.response
は、通常のレスポンスと同様に、options.extract
およびoptions.deserialize
を使用して、解析されたレスポンスに設定されます。
エラー自体を処理できる場合に便利です。例えば、セッションの有効期限切れを検知するには、if (error.code === 401) return promptForAuth().then(retry)
を実行します。API のスロットリング制限に引っかかり、"timeout": 1000
というエラーが返された場合は、setTimeout(retry, error.response.timeout)
を実行できます。
Loading icons and error messages
上記の例を拡張し、ローディングインジケーターとエラーメッセージを表示するようにした例を以下に示します。
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
があり、Todos
コンポーネントのビューは、エラーメッセージが存在する場合はエラーメッセージを表示し、Data.todos.list
が配列でない場合はローディングアイコンを表示するように変更されました。
Dynamic URLs
リクエスト URL には、補間を含めることができます。
m.request({
method: 'GET',
url: '/api/v1/users/:id',
params: { id: 123 },
}).then(function (user) {
console.log(user.id); // logs 123
});
上記のコードでは、:id
は params
オブジェクトの値で置換され、リクエストは GET /api/v1/users/123
となります。
params
プロパティに一致するデータが存在しない場合、補間は無視されます。
m.request({
method: 'GET',
url: '/api/v1/users/foo:bar',
params: { id: 123 },
});
上記のコードでは、リクエストは GET /api/v1/users/foo:bar?id=123
になります。
Aborting requests
リクエストを中止することが望ましい場合があります。例えば、オートコンプリート/タイプアヘッドウィジェットでは、最後の入力に対するリクエストのみを有効にする必要があります。通常、オートコンプリートはユーザーの入力に応じて複数のリクエストを発行しますが、HTTP リクエストはネットワーク状況によって順不同で完了する可能性があります。最後の入力に対するリクエストよりも古いリクエストが完了した場合、ウィジェットには不適切なデータが表示される可能性があります。
m.request()
は、基になる XMLHttpRequest
オブジェクトを options.config
パラメータを介して公開します。これにより、そのオブジェクトへの参照を保存し、必要に応じてその abort
メソッドを呼び出すことができます。
var searchXHR = null;
function search() {
abortPreviousSearch();
m.request({
method: 'GET',
url: '/api/v1/users',
params: { search: query },
config: function (xhr) {
searchXHR = xhr;
},
});
}
function abortPreviousSearch() {
if (searchXHR !== null) searchXHR.abort();
searchXHR = null;
}
File uploads
ファイルをアップロードするには、まず File
オブジェクトへの参照が必要です。最も簡単な方法としては、<input type="file">
要素を使用する方法があります。
m.render(document.body, [m('input[type=file]', { onchange: upload })]);
function upload(e) {
var file = e.target.files[0];
}
上記のコードスニペットは、ファイル入力をレンダリングします。ユーザーがファイルを選択すると、onchange
イベントがトリガーされ、upload
関数が呼び出されます。e.target.files
は File
オブジェクトのリストです。
次に、FormData
オブジェクトを作成して、multipart request を作成する必要があります。これは、リクエスト body でファイルデータを送信できる特別な形式の HTTP リクエストです。
function upload(e) {
var file = e.target.files[0];
var body = new FormData();
body.append('myfile', file);
}
次に、m.request
を呼び出し、options.method
を body を使用する HTTP メソッド (例: POST
、PUT
、PATCH
) に設定し、FormData
オブジェクトを options.body
として使用する必要があります。
function upload(e) {
var file = e.target.files[0];
var body = new FormData();
body.append('myfile', file);
m.request({
method: 'POST',
url: '/api/v1/upload',
body: body,
});
}
サーバーがマルチパートリクエストを受け入れるように構成されていると仮定すると、ファイル情報は myfile
key に関連付けられます。
Multiple file uploads
1 つのリクエストで複数のファイルをアップロードできます。これにより、バッチアップロードはアトミックに行われます。つまり、アップロード中にエラーが発生した場合、すべてのファイルが処理されません。一部のファイルのみを保存することはできません。ネットワークエラーが発生した場合でも、できるだけ多くのファイルを保存したい場合は、個別のリクエストで各ファイルをアップロードすることを検討してください。
複数のファイルをアップロードするには、それらをすべて FormData
オブジェクトに追加するだけです。ファイル入力を使用する場合、入力を multiple
属性に追加することで、ファイルのリストを取得できます。
m.render(document.body, [
m('input[type=file][multiple]', { onchange: upload }),
]);
function upload(e) {
var files = e.target.files;
var body = new FormData();
for (var i = 0; i < files.length; i++) {
body.append('file' + i, files[i]);
}
m.request({
method: 'POST',
url: '/api/v1/upload',
body: body,
});
}
Monitoring progress
リクエストが本質的に遅い場合 (たとえば、大きなファイルのアップロード)、アプリケーションがまだ動作していることを示すために、進行状況インジケーターをユーザーに表示することが望ましい場合があります。
m.request()
は、基になる XMLHttpRequest
オブジェクトを options.config
パラメータを介して公開します。これにより、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 body = new FormData();
body.append('myfile', file);
m.request({
method: 'POST',
url: '/api/v1/upload',
body: body,
config: function (xhr) {
xhr.upload.addEventListener('progress', function (e) {
progress = e.loaded / e.total;
m.redraw(); // tell Mithril.js that data changed and a re-render is needed
});
},
});
}
上記の例では、ファイル入力がレンダリングされます。ユーザーがファイルを選択すると、アップロードが開始され、config
コールバックで progress
イベントハンドラーが登録されます。このイベントハンドラーは、XMLHttpRequest で進行状況の更新があるたびに発生します。XMLHttpRequest のプログレスイベントは、Mithril.js の仮想 DOM エンジンによって自動的に処理されないため、m.redraw()
を呼び出して、データの変更と再描画の必要性を Mithril.js に明示的に通知する必要があります。
Casting response to a type
全体的なアプリケーションアーキテクチャによっては、リクエストのレスポンスデータを特定のクラスまたはタイプに変換することが望ましい場合があります (たとえば、日付フィールドを均一に解析したり、ヘルパーメソッドを使用したりするため)。
コンストラクタ関数を options.type
パラメータとして渡すと、Mithril.js は 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); // logs a name
});
上記の例では、/api/v1/users
がオブジェクトの配列を返すと仮定すると、User
コンストラクタは、配列内の各オブジェクトに対してインスタンス化されます (つまり、new User(data)
として呼び出されます)。レスポンスが単一のオブジェクトを返した場合、そのオブジェクトは body
引数として使用されます。
Non-JSON responses
サーバーエンドポイントが JSON レスポンスを返さない場合があります。たとえば、HTML ファイル、SVG ファイル、または CSV ファイルをリクエストしている場合があります。デフォルトでは、Mithril.js はレスポンスを 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) {
// naive implementation for the sake of keeping example simple
return data.split('\n').map(function (row) {
return row.split(',');
});
}
上記の parseCSV 関数が適切な CSV パーサーが処理する多くのケースを処理しないという事実を無視すると、上記のコードは配列の配列をログに記録します。
カスタムヘッダーもこの点で役立つ場合があります。たとえば、SVG をリクエストしている場合は、コンテンツタイプをそれに応じて設定することをお勧めします。デフォルトの JSON リクエストタイプをオーバーライドするには、options.headers
をリクエストヘッダー名と値に対応する key-value ペアのオブジェクトに設定します。
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;
},
});
Retrieving response details
デフォルトでは、Mithril.js は 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 チェーンに渡される前の状態です。したがって、extract
関数内で例外が発生した場合、Promise は reject される可能性があります。
Issuing fetches to IP addresses
URL からパラメータを抽出する処理が単純なため、IPv6 アドレスのセグメントがパスパラメータとして誤認識され、エラーが発生します。IPv6 アドレスを正しく処理するには、パラメータとして渡す必要があります。
// This doesn't work
m.request('http://[2001:db8::990a:cd27:4d9e:79]:8080/some/path', {
// ...
});
これを回避するには、代わりに IPv6 アドレス + ポートペアをパラメータとして渡す必要があります。
m.request('http://:host/some/path', {
params: { host: '[2001:db8::990a:cd27:4d9e:79]:8080' },
// ...
});
これは IPv4 アドレスの問題ではなく、通常どおり使用できます。
// This will work as you expect
m.request('http://192.0.2.15:8080/some/path', {
// ...
});
Why JSON instead of HTML
対照的に、Mithril.js はシンクライアントアプリケーション向けのフレームワークであり、テンプレートとデータを個別にダウンロードし、ブラウザ上で JavaScript を使って組み合わせることを前提としています。ブラウザ側でテンプレート処理を行うことで、サーバーリソースを節約し、運用コストを削減できます。また、テンプレートとデータを分離することで、テンプレートコードのキャッシュ効率が向上し、デスクトップやモバイルなど、様々なクライアントでコードを再利用しやすくなります。さらに、Mithril.js は retained mode UI 開発を可能にし、複雑なユーザーインタラクションの開発とメンテナンスを大幅に簡略化します。
デフォルトでは、m.request
はレスポンスデータが JSON 形式であると想定します。一般的な Mithril.js アプリケーションでは、その JSON データは通常、ビューによって消費されます。
Mithril でサーバーで生成された動的 HTML をレンダリングしようとすることは避ける必要があります。サーバーサイドのテンプレートシステムを使用する既存のアプリケーションがあり、それを再構築したい場合は、まずその作業がそもそも実現可能かどうかを判断します。シンサーバーアーキテクチャからシンクライアントアーキテクチャへの移行は、通常、やや大規模な作業であり、テンプレートから論理データサービスへのロジックのリファクタリング (およびそれに伴うテスト) が必要になります。
データサービスは、アプリケーションの性質に応じて、さまざまな方法で編成できます。RESTful アーキテクチャは API プロバイダーで一般的であり、サービス指向アーキテクチャ は、トランザクションワークフローが多数ある場合に必要になることがよくあります。
Why XHR instead of fetch
fetch()
は、XMLHttpRequest
と同様に、サーバーからリソースをフェッチするための新しい Web API です。
Mithril.js の m.request
は、いくつかの理由で fetch()
の代わりに XMLHttpRequest
を使用します。
fetch
はまだ完全に標準化されておらず、仕様の変更を受ける可能性があります。XMLHttpRequest
呼び出しは、解決する前に中止できます (たとえば、インスタント検索 UI での競合状態を回避するため)。XMLHttpRequest
は、長時間実行されるリクエスト (たとえば、ファイルのアップロード) の進行状況リスナーのフックを提供します。XMLHttpRequest
はすべてのブラウザでサポートされていますが、fetch()
は Internet Explorer および古い Android (5.0 Lollipop より前) ではサポートされていません。
Mithril.js の XHR モジュールは非常に軽量でありながら、URL 補間やクエリ文字列のシリアライズなど、実装が容易ではない多くの重要な機能をサポートしています。また、Mithril.js の自動再描画システムとの統合もスムーズに行えます。一方、fetch
の polyfill にはこれらの機能が含まれておらず、同等の機能を実現するには追加のライブラリやコードが必要になります。
さらに、Mithril.js の XHR モジュールは JSON ベースのエンドポイント用に最適化されており、最も一般的なケースを適切に簡潔にします。つまり、m.request(url)
です。一方、fetch
では、レスポンスデータを JSON として解析するために、追加の明示的な手順が必要です: fetch(url).then(function(response) {return response.json()})
fetch()
API には、いくつかのまれなケースで XMLHttpRequest
よりもいくつかの技術的な利点があります。
- ストリーミング API (リアクティブプログラミングの意味ではなく、「ビデオストリーミング」の意味) を提供し、(コードの複雑さを犠牲にして) 非常に大きなレスポンスに対してより優れたレイテンシとメモリ消費を実現します。
- Service Worker API に統合されており、ネットワークリクエストがいつどのように発生するかをより詳細に制御できます。この API では、プッシュ通知とバックグラウンド同期機能にもアクセスできます。
一般的なシナリオでは、ストリーミングは目立ったパフォーマンス上の利点を提供しません。そもそもメガバイト単位のデータをダウンロードすることはお勧めできないためです。また、小さなバッファを繰り返し再利用することによるメモリの利点は、過度のブラウザの再描画が発生した場合に相殺または無効になる可能性があります。これらの理由から、m.request
の代わりに fetch()
ストリーミングを選択することはお勧めできません。
Avoid anti-patterns
Promises are not the response data
m.request
メソッドは、レスポンスデータ自体ではなく、Promise
を返します。HTTP リクエストはネットワーク遅延の影響を受けるため、完了までに時間がかかることがあります。JavaScript がリクエストの完了を待機すると、データが利用可能になるまでアプリケーションがフリーズしてしまうため、m.request
はレスポンスデータを直接返すことができません。
// AVOID
var users = m.request('/api/v1/users');
console.log('list of users:', users);
// `users` is NOT a list of users, it's a promise
// PREFER
m.request('/api/v1/users').then(function (users) {
console.log('list of users:', users);
});