Mithril request(options)

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 如果存在该参数,相当于设置了 {method: "GET", url: url},该对象会覆盖 options 中对应的值。
options.method String 使用的 HTTP 方法。值必须是以下之一:GETPOSTPUTPATCHDELETEHEADOPTIONS。默认为 GET
options.url String 请求会发送到该 URL。该 URL 可以是绝对路径,也可以是相对路径,且可以包含 URL 参数
options.data any 添加到请求的数据。对于 GET 请求,会序列化为查询字符串,并添加到 URL 中。对于其他请求,会添加到 body 中。
options.async Boolean 请求是否异步。默认为 true
options.user String 用于 HTTP 验证的 username。默认为 undefined
options.password String 用于 HTTP 验证的 password。默认为 undefined。此参数用于兼容 XMLHttpRequest,但你应该避免使用此参数,因为它会明文发送密码。
options.withCredentials Boolean 是否把 cookie 发送到第三方域名。默认为 false
options.config xhr = Function(xhr) 用于配置 XMLHttpRequest 对象。默认为恒等函数
options.headers Object 用于添加到请求的 Header 中(该参数会在设置 options.config 之前设置)。
options.type any = Function(any) 响应中的每个对象的构造函数,即得到请求结果后,会先使用该函数进行处理。默认为恒等函数
options.serialize string = Function(any) 序列化 data 的方法。默认为 JSON.stringify,或者当 options.data 是一个 FormData 实例时,默认为恒等函数
options.deserialize any = Function(string) 对请求的响应进行反序列化的方式。默认是对 JSON.parse 进行的封装,如果是空响应,会返回 null
options.extract string = Function(xhr, options) 一个钩子,指定如何读取 XMLHttpRequest 的响应。可用于读取响应的 header 和 cookie。默认为返回 xhr.responseText 的函数。如果定义了该参数,则其中的 xhr 表示请求的 XMLHttpRequest 的实例,options 则是传入到 m.request 中的对象。如果设置了自定义的 extract 回调,则会忽略 options.deserialize,且 extract 回调返回的字符串不会被解析为 JSON。
options.useBody Boolean 当设置为 true 时,强制把所有请求的 data 都放在 HTTP 的 body 中;当设置为 false 时,强制把所有请求的 data 都放在请求的查询字符串中。默认 GET 请求为 false,其他请求为 true
options.background Boolean 如果为 false,则在请求完成后重绘已挂载的组件;如果为 true,则不会重绘。默认为 false
返回 Promise extractdeserializetype 方法完成之后,返回 promise 用于处理响应的数据。

工作原理

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
})

我们假设 /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 字段用于显示错误信息,并且 Todos 组件的视图会在存在错误时显示错误信息,或者在 Data.todos.list 为空时显示加载中图标。

带参数的 URL

请求的 URL 可以包含参数:

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

如果 data 属性中没有匹配的数据,则不会对参数进行替换。

m.request({
    method: "GET",
    url: "/api/v1/users/foo:bar",
    data: {id: 123},
})

在上面的代码中,请求为 GET /api/v1/users/foo:bar

取消请求

有时,需要在请求还没完成时取消请求。例如,在自动完成组件中,在用户输入时,会连续发送多次请求,你只需要获取最后一次请求返回的数据,但请求返回的顺序并不一定和请求发送顺序一致。如果有一个请求在最后一个触发的请求之后完成,则组件可能显示和用户输入不相关的数据。

m.request 可以通过 options.config 暴露出其底层的 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]
}

以上代码会渲染一个文件选择器。如果用户选择了一个文件,onchange 事件会触发,调用 upload 函数。e.target.files 则是 File 对象的数组。

然后,你需要创建一个 FormData 对象来创建 Multipart 请求,这是一个指定格式 HTTP 请求,可以在请求的 body 中发送文件数据:

function upload(e) {
    var file = e.target.files[0]

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

然后,你需要调用 m.request,并把 options.method 设置为请求方式(如 POSTPUTPATCH),以及把 options.data 设置为 FormData

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,
    })
}

假设服务器设置为可接受 multipart 请求,则文件信息会和 myfile 相关联。

多个文件上传

可以在一个请求中上传多个文件。但这样会使批量上传具有原子性,例如在上传过程中出现错误,则不会处理任何文件,因此不能只上传部分文件。如果你想在网络不稳定的情况下保存已处理的文件,则应该把每个文件放在单独的请求中上传。

要上传多个文件,只需将其全部添加到 FormData 对象。当使用文件输入框时,可以通过在输入框上添加 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,
    })
}

检测进度

有时,如果一个请求本身就很慢(例如上传大文件),则需要向用户显示一个进度指示符,以表明应用仍在处理请求。

m.request 通过 options.config 向外暴露底层的 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() // tell Mithril that data changed and a re-render is needed
            })
        }
    })
}

在上面的例子中,渲染了一个文件输入框。如果用户选择了一个文件,则会启动上传,并在 config 回调中,注册了一个 progress 事件处理函数。只要 XMLHttpRequest 中有进度更新,就会触发此事件处理函数。因为 XMLHttpRequest 的进度事件不是由 Mithril 的虚拟 DOM 引擎直接处理的,所以必须调用 m.redraw(),以通知 Mithril 数据已更改,需要进行重绘。

对请求结果进行处理

你可能需要将请求返回的数据进行类型转换(例如,统一对日期字段进行格式化)。

你可以传入构造函数作为 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) // logs a name
})

在上面的示例中,假如 /api/v1/users 返回了一个对象数组,User 构造函数会为每个对象进行实例化(如,调用 new User(data))。如果响应返回了单个对象,该对象会被用作 data 参数。

非 JSON 格式的响应

有时服务端返回的不是 JSON 格式的响应:例如你请求的是 HTML 文件、SVG 文件或 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(",")
    })
}

上面的例子会输出一个二维数组。

自定义 header 也是有用的。例如,你请求一个 SVG 文件,你可能需要设置相应的内容类型。要覆盖默认的 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 会以 JSON 格式解析响应,并返回 xhr.responseText。有时需要获取更详细的响应信息,这时可以传入自定义的 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)
})

一旦请求完成,在返回 promise 之前,options.extract 的参数就会被填充为 XMLHttpRequest 对象,所以如果 options.extract 中发生异常,promise 仍然可以处于拒绝状态。

避免反模式

Promise 不是响应的数据

m.request 请求返回 Promise,而不是响应数据本身。因为一个 HTTP 请求可能需要比较长时间来完成(由于网络延迟),如果 JavaScript 等待请求完成,则它会冻结应用,直到得到响应数据。

// 错误用法
var users = m.request("/api/v1/users")
console.log("list of users:", users)
// `users` 不是用户列表哦,而是 promise

// 正确用法
m.request("/api/v1/users").then(function(users) {
    console.log("list of users:", users)
})