Fetch
Fetch
JavaScript 通过 XMLHttpRequest(XHR)来执行异步请求,这个方式已经存在了很长一段时间。虽说它很有用,但它不是最佳 API。它在设计上不符合职责分离原则,将输入、输出和用事件来跟踪的状态混杂在一个对象里。而且,基于事件的模型与最近 JavaScript 流行的 Promise 以及基于生成器的异步编程模型不太搭。
新的 Fetch API 打算修正上面提到的那些缺陷。它向 JS 中引入和 HTTP 协议中同样的原语。具体而言,它引入一个实用的函数 fetch() 用来简洁捕捉从网络上检索一个资源的意图。Fetch 规范 的 API 明确了用户代理获取资源的语义。它结合 ServiceWorkers,尝试达到以下优化:
- 改善离线体验
- 保持可扩展性
而与 jQuery 相比,fetch 方法与 jQuery.ajax()
的主要区别在于:
fetch()
方法返回的 Promise 对象并不会在 HTTP 状态码为 404 或者 500 的时候自动抛出异常,而需要用户进行手动处理- 默认情况下,fetch 并不会发送任何的本地的 cookie 到服务端,注意,如果服务端依靠 Session 进行用户控制的话要默认开启 Cookie
请求构建
假设 fetch 已经被挂载到了全局的 window 目录下。
// Simple response handling
fetch("/some/url")
.then(function (response) {})
.catch(function (err) {
// Error :(
});
// Chaining for more "advanced" handling
fetch("/some/url")
.then(function (response) {
return; //...
})
.then(function (returnedValue) {
// ...
})
.catch(function (err) {
// Error :(
});
Request
Request 对象代表了一次 fetch 请求中的请求体部分,你可以自定义 Request
对象:
method
- 使用的 HTTP 动词,GET
,POST
,PUT
,DELETE
,HEAD
url
- 请求地址,URL of the requestheaders
- 关联的 Header 对象referrer
- referrermode
- 请求的模式,主要用于跨域设置,cors
,no-cors
,same-origin
credentials
- 是否发送 Cookieomit
,same-origin
redirect
- 收到重定向请求之后的操作,follow
,error
,manual
integrity
- 完整性校验cache
- 缓存模式(default
,reload
,no-cache
)
// 构建独立的请求对象
const request = new Request("/users.json", {
method: "POST",
mode: "cors",
redirect: "follow",
headers: new Headers({
"Content-Type": "text/plain",
}),
});
// Now use it!
fetch(request).then(function () {
/* handle response */
});
// 直接作为参数传入到 fetch 函数中
fetch("/users.json", {
method: "POST",
mode: "cors",
redirect: "follow",
headers: new Headers({
"Content-Type": "text/plain",
}),
}).then(function () {
/* handle response */
});
在 POST 请求中,如果我们需要传递参数,则应该将参数值进行序列化处理为字符串然后传递:
fetch("/users", {
method: "post",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Hubot",
login: "hubot",
}),
});
URI Encode
注意,fetch 方法是自动会将 URI 中的双引号进行编码的,如果在 URI 中存入了部分 JSON,有时候会出现意想不到的问题,譬如我们以 GET 方法访问如下的 URI:
[GET] http://api.com?requestData={"p":"q"}
那么 fetch 会自动将双引号编码,变成:
[GET] http://api.com?requestData={%22p%22:%22q%22}
请求在传入某些后端服务器时可能会触发异常,建议对 URI 的 Query Parameter 部分进行统一的 URI 编码:
//将 requestData 序列化为 JSON
const requestDataString = encodeURIComponent(
JSON.stringify(requestData).replace(/%22/g, '"')
);
//将字符串链接
const packagedRequestURL = `${Model.BASE_URL}${path}?requestData=${requestDataString}&action=${action}`;
Headers | 自定义请求头
// Create an empty Headers instance
const headers = new Headers();
// Add a few headers
headers.append("Content-Type", "text/plain");
headers.append("X-My-Custom-Header", "CustomValue");
// Check, get, and set header values
headers.has("Content-Type"); // true
headers.get("Content-Type"); // "text/plain"
headers.set("Content-Type", "application/json");
// Delete a header
headers.delete("X-My-Custom-Header");
// Add initial values
const headers = new Headers({
"Content-Type": "text/plain",
"X-My-Custom-Header": "CustomValue",
});
常见的请求方法有: append
, has
, get
, set
以及 delete
const request = new Request("/some-url", {
headers: new Headers({
"Content-Type": "text/plain",
}),
});
fetch(request).then(function () {
/* handle response */
});
Cookies
如果需要设置 fetch 自动地发送本地的 Cookie,需要将 credentials 设置为same-origin
:
fetch("/users", {
credentials: "same-origin",
});
该选项会以类似于 XMLHttpRequest 的方式来处理 Cookie,否则,可能因为没有发送 Cookie 而导致基于 Session 的认证出错。对于跨域情况下的 Cookie 发送,可以将 credentials
的值设置为include
来在 CORS 情况下发送请求。
fetch("https://example.com:1234/users", {
credentials: "include",
});
另外需要注意的是,当你为了配置在 CORS 请求中附带 Cookie 等信息时,来自于服务器的响应中的 Access-Control-Allow-Origin 不可以再被设置为 *
,必须设置为某个具体的域名,则响应会失败。
Response | 响应处理
在 fetch 的then
函数中提供了一个Response
对象,即代表着对于服务端返回值的封装,你也可以在 Mock 的时候自定义 Response 对象,譬如在你需要使用 Service Workers 的情况下,在Response
中,你可以作如下配置:
type
-basic
,cors
url
useFinalURL
- 是否为最终地址status
- 状态码 (ex:200
,404
, etc.)ok
- 是否成功响应 (status in the range 200-299)statusText
- status code (ex:OK
)headers
- 响应头
// Create your own response for service worker testing
// new Response(BODY, OPTIONS)
const response = new Response(".....", {
ok: false,
status: 404,
url: "/",
});
// The fetch's `then` gets a Response instance back
fetch("/").then(function (responseObj) {
console.log("status: ", responseObj.status);
});
Response
还提供以下方法:
clone()
- 创建一个 Response 对象的克隆。error()
- 返回与网络错误关联的新 Response 对象。redirect()
- 使用不同的 URL 创建一个新的响应。arrayBuffer()
- 返回一个用 ArrayBuffer 解析的 promise。blob()
- 返回一个用 Blob 解析的 promise。formData()
- 返回一个用 FormData 对象解析的 promise。json()
- 返回一个用 JSON 对象解析的 promise。text()
- 返回一个用 USVString(text)解析的 promise。
Handling HTTP error statuses | 处理 HTTP 错误状态
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
} else {
const error = new Error(response.statusText);
error.response = response;
throw error;
}
}
function parseJSON(response) {
return response.json();
}
fetch("/users")
.then(checkStatus)
.then(parseJSON)
.then(function (data) {
console.log("request succeeded with JSON response", data);
})
.catch(function (error) {
console.log("request failed", error);
});
Handling JSON | 处理 JSON 响应
fetch("https://davidwalsh.name/demo/arsenal.json")
.then(function (response) {
// Convert to JSON
return response.json();
})
.then(function (j) {
// Yay, `j` is a JavaScript object
console.log(j);
});
Handling Basic Text/HTML Response | 处理文本响应
fetch("/next/page")
.then(function (response) {
return response.text();
})
.then(function (text) {
// <!DOCTYPE ....
console.log(text);
});
Blob Responses | 二进制数据处理
如果你希望通过 fetch 方法来载入一些类似于图片等资源:
fetch("flowers.jpg")
.then(function (response) {
return response.blob();
})
.then(function (imageBlob) {
document.querySelector("img").src = URL.createObjectURL(imageBlob);
});
blob()
方法会接入一个响应流并且一直读入到结束。
自定义封装
可控的 Promise
代理
在上面的介绍中会发现,fetch 并没有在客户端实现 Cancelable Request 的功能,或者超时自动放弃功能,因此这一步骤往往是需要在代理层完成。笔者在自己的工作中还遇到另一个请求,就是需要在客户端抓取其他没有设置 CORS 响应或者 JSONP 响应的站点,而必须要进行中间代理层抓取。笔者为了尽可能小地影响逻辑层代码,因此在自己的封装中封装了如下方法:
/**
* @function 通过透明路由,利用get方法与封装好的QueryParams形式发起请求
* @param BASE_URL 请求根URL地址,注意,需要添加http://以及末尾的/,譬如`http://api.com/`
* @param path 请求路径,譬如"path1/path2"
* @param queryParams 请求的查询参数
* @param contentType 请求返回的数据格式
* @param proxyUrl 请求的路由地址
*/
getWithQueryParamsByProxy({BASE_URL=Model.BASE_URL, path="/", queryParams={}, contentType="json", proxyUrl="http://api.proxy.com"}) {
//初始化查询字符串,将BASE_URL以及path进行编码
let queryString = `BASE_URL=${encodeURIComponent(BASE_URL)}&path=${encodeURIComponent(path)}&`;
//根据queryParams构造查询字符串
for (let key in queryParams) {
//拼接查询字符串
queryString += `${key}=${encodeURIComponent(queryParams[key])}&`;
}
//将查询字符串进行编码
let encodedQueryString = (queryString);
//封装最终待请求的字符串
const packagedRequestURL = `${proxyUrl}?${encodedQueryString}action=GET`;
//以CORS方式发起请求
return this._fetchWithCORS(packagedRequestURL, contentType);
}