request(options)
描述
发送 XHR (AJAX) 请求,并返回一个 Promise。
m.request({
method: 'PUT',
url: '/api/v1/users/:id',
params: { id: 1 },
body: { name: 'test' },
}).then(function (result) {
console.log(result);
});
签名
promise = m.request(options)
参数名 | 类型 | 必需 | 描述 |
---|---|---|---|
options | Object | 是 | 请求选项。 |
options.method | String | 否 | 使用的 HTTP 方法。可选值为:GET 、POST 、PUT 、PATCH 、DELETE 、HEAD 或 OPTIONS 。默认为 GET 。 |
options.url | String | 是 | 请求发送到的 路径名,可以选择使用 options.params 中的值进行替换。 |
options.params | Object | 否 | 要插入到 URL 中和/或序列化为查询字符串的数据。 |
options.body | Object | 否 | 要序列化到请求体中的数据。 |
options.async | Boolean | 否 | 请求是否为异步?默认为 true 。 |
options.user | String | 否 | HTTP 认证的用户名。默认为 undefined 。 |
options.password | String | 否 | HTTP 认证的密码。默认为 undefined 。此选项为保持与 XMLHttpRequest 兼容而提供,但应避免使用,因为密码会以明文形式通过网络传输。 |
options.withCredentials | Boolean | 否 | 是否向第三方域发送 Cookie。默认为 false 。 |
options.timeout | Number | 否 | 请求自动 终止 前可持续的毫秒数。默认为 undefined 。 |
options.responseType | String | 否 | 响应的预期 类型。若定义了 extract ,则默认为 "" ,否则默认为 "json" 。如果 responseType: "json" ,它会在内部执行 JSON.parse(responseText) 。 |
options.config | xhr = Function(xhr) | 否 | 暴露底层的 XMLHttpRequest 对象,用于底层配置和可选替换(通过返回新的 XMLHttpRequest)。 |
options.headers | Object | 否 | 在发送请求前添加到请求头中(在 options.config 之前应用)。 |
options.type | any = Function(any) | 否 | 用于处理响应中每个对象的构造函数。默认为 恒等函数。 |
options.serialize | string = Function(any) | 否 | 用于序列化 body 的方法。默认为 JSON.stringify ,或者如果 options.body 是 FormData 或 URLSearchParams 的实例,则默认为 恒等函数(即 function(value) {return value} )。 |
options.deserialize | any = Function(any) | 否 | 用于反序列化 xhr.response 或规范化的 xhr.responseText 的方法。默认为 恒等函数。如果定义了 extract ,则将跳过 deserialize 。 |
options.extract | any = Function(xhr, options) | 否 | 用于指定如何读取 XMLHttpRequest 响应的回调函数。可用于处理响应数据、读取请求头和 Cookie。默认情况下,该函数返回 options.deserialize(parsedResponse) 。当服务器响应状态码指示错误或响应的语法无效时,会抛出异常。如果提供了自定义 extract 回调,则 xhr 参数是用于请求的 XMLHttpRequest 实例,options 是传递给 m.request 调用的对象。此外,将跳过 deserialize ,并且从 extract 回调函数返回的值将在 Promise resolve 时保持不变。 |
options.background | Boolean | 否 | 若为 false ,请求完成后会重绘已挂载的组件;若为 true ,则不重新渲染。默认为 false 。 |
返回 | Promise | 一个 Promise,它解析为响应数据,在通过 extract 、deserialize 和 type 方法处理后得到。如果响应状态代码指示错误,则 Promise 将拒绝,但可以通过设置 extract 选项来防止这种情况。 |
promise = m.request(url, options)
参数 | 类型 | 必需 | 描述 |
---|---|---|---|
url | String | 是 | 要将请求发送到的路径名。如果存在 options.url ,则会覆盖此项。 |
options | Object | 否 | 要传递的请求选项。 |
返回 | Promise | 一个 Promise,它解析为响应数据,在通过 extract 、deserialize 和 type 方法处理后得到。 |
第二种形式主要等同于 m.request(Object.assign({url: url}, options))
,只是它在内部不依赖于 ES6 全局 Object.assign
。
工作原理
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 将被拒绝。提供 extract 回调可以阻止 Promise 拒绝。
典型用法
以下示例展示了一个使用 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>
列表。
错误处理
当非 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)
。
加载图标和错误消息
这是上面示例的扩展版本,它实现了加载指示器和错误消息:
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',
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
中止请求
有时,需要中止请求。例如,在自动完成器/预输入小部件中,您希望确保只有最后一个请求完成,因为通常自动完成器会在用户键入时触发多个请求,并且由于网络不可预测的性质,HTTP 请求可能会乱序完成。如果之前的请求在最后一次请求之后完成,小部件可能会显示相关性较低(甚至错误)的数据。
m.request()
通过 options.config
参数公开其底层的 XMLHttpRequest
对象,这允许您保存对该对象的引用并在需要时调用其 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
对象的引用。最简单的方法是从 <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
对象来创建一个 multipart request,这是一种特殊格式的 HTTP 请求,允许在请求体中发送文件数据。
function upload(e) {
var file = e.target.files[0];
var body = new FormData();
body.append('myfile', file);
}
接下来,您需要调用 m.request
并将 options.method
设置为使用正文的 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,
});
}
假设服务器已配置为接受 multipart 请求,文件信息将会关联到 myfile
键。
多文件上传
可以在一个请求中上传多个文件。这样做会使批量上传具有原子性,也就是说,如果在上传过程中发生错误,所有文件都不会被处理,因此无法只保存部分文件。如果希望在网络故障时尽可能多地保存文件,应该考虑在单独的请求中上传每个文件。
要上传多个文件,只需将它们全部附加到 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,
});
}
监控进度
有时,如果请求本身比较耗时(例如上传大文件),需要向用户展示进度指示器,表明应用程序仍在运行。
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 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(); // 告诉 Mithril.js 数据已更改,需要重新渲染
});
},
});
}
在上面的示例中,渲染了一个文件输入。如果用户选择一个文件,则会启动上传,并且在 config
回调中,注册一个 progress
事件处理程序。每当 XMLHttpRequest 对象的进度发生更新时,该事件处理程序就会被触发。由于 XMLHttpRequest 对象的进度事件不由 Mithril.js 的虚拟 DOM 引擎直接管理,因此必须调用 m.redraw()
来通知 Mithril.js 数据已经发生变化,需要重新渲染。
将响应强制转换为类型
根据整体应用程序架构,可能需要将请求的响应数据转换为特定的类或类型(例如,统一解析日期字段或具有辅助方法)。
可以将构造函数作为 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
参数。
非 JSON 响应
有时,服务端接口不会返回 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
设置为键值对的对象,该对象对应于请求标头名称和值。
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.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 链之前,因此如果处理抛出异常,Promise 仍可能最终处于拒绝状态。
向 IP 地址发出请求
由于 URL 中检测参数的方式过于简单,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', {
// ...
});
为什么选择 JSON 而不是 HTML
许多服务器端框架提供了一个视图引擎,该引擎在提供 HTML 之前(在页面加载时或通过 AJAX)将数据库数据插入到模板中,然后使用 jQuery 来处理用户交互。
与此相反,Mithril.js 是一个为富客户端应用设计的框架,它通常会分别下载模板和数据,然后在浏览器中使用 JavaScript 将它们组合在一起。在浏览器中进行大量的模板渲染工作可以带来一些好处,例如通过释放服务器资源来降低运营成本。将模板和数据分离,可以更有效地缓存模板代码,并支持跨不同类型的客户端(例如桌面端、移动端)更好地复用代码。另一个好处是 Mithril.js 实现了保留模式的 UI 开发模式,极大地简化了复杂用户交互的开发和维护工作。
默认情况下,m.request
期望响应数据为 JSON 格式。在典型的 Mithril.js 应用程序中,该 JSON 数据通常由视图使用。
您应该避免尝试使用 Mithril 渲染服务器生成的动态 HTML。如果你的现有应用使用了服务端模板系统,并且你希望对其进行重构,首先要评估一下从一开始就进行重构是否可行。从服务端渲染架构迁移到客户端渲染架构通常是一项非常大的工程,需要将模板中的逻辑重构到数据服务层(以及相应的测试)。
数据服务可以以许多不同的方式组织,具体取决于应用程序的性质。RESTful 架构在 API 提供商中很受欢迎,并且在存在大量高度事务性工作流程的情况下通常需要面向服务的架构。
为什么选择 XHR 而不是 fetch
fetch()
是一种较新的 Web API,用于从服务器获取资源,类似于 XMLHttpRequest
。
Mithril.js 的 m.request
使用 XMLHttpRequest
而不是 fetch()
是出于以下几个原因:
fetch
尚未完全标准化,并且可能会受到规范更改的影响。- 可以在
XMLHttpRequest
调用 resolve 之前将其中止(例如,为了避免在即时搜索 UI 中出现竞态条件)。 XMLHttpRequest
为长时间运行的请求(例如,文件上传)提供进度侦听器的钩子。- 所有浏览器都支持
XMLHttpRequest
,而 Internet Explorer 和较旧的 Android(5.0 Lollipop 之前)不支持fetch()
。
尽管 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 提供了对网络请求发生方式和时间的额外控制层。此 API 还允许访问推送通知和后台同步功能。
因此,只有在资源消耗非常高的应用中,才建议使用 fetch()
的流式传输代替 m.request
。
避免反模式
Promise 不是响应数据
m.request
方法返回的是一个 Promise
对象,而不是响应数据本身。它无法直接返回数据,因为 HTTP 请求可能需要很长时间才能完成(由于网络延迟),如果 JavaScript 阻塞等待,会导致应用程序卡死,直到数据返回。
// 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 // `users` 不是用户列表,它是一个 promise
// PREFER // 推荐
m.request('/api/v1/users').then(function (users) {
console.log('list of users:', users);
});