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 方法。值必须是以下之一:GET 、POST 、PUT 、PATCH 、DELETE 、HEAD 、OPTIONS 。默认为 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 | 在 extract 、deserialize 和 type 方法完成之后,返回 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
设置为请求方式(如 POST
、PUT
、PATCH
),以及把 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)
})