Mithril 1.1.0

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.dataFormDataの時は恒等写像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に格納されているオリジナルの空の配列を使って表示を行います。サーバーからレスポンスが返ってくると、オブジェクトの配列のitemsData.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.filesFileオブジェクトの配列になっています。

次に、マルチパートリクエストを作成するための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() を使うには、欠けているブラウザサポートをおぎなうために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と較べていくつかの技術的な利点がありますが、用途は限定されています。

しかし、メガバイトの単位のデータをダウンロードしない限りは、一般的な用途ではストリーミングのメリットが目に見えることはほぼありません。また小さいバッファを繰り返し利用することによるメモリの効率的な利用に関しても、再描画が頻繁になると打ち消されてしまいます。これらの理由により、m.requestの代わりにfetch()ストリームが推奨されるのは大量のデータを扱うアプリケーションに限られるでしょう。


アンチパターンを避ける

Promiseはレスポンスデータそのものではありません。

m.requestPromiseを返しますが、これはレスポンスそのものではありません。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.