近年来前端MV*的发展壮大,人们越来越少的使用jQuery,我们不可能单独为了使用jQuery的Ajax api来单独引入他,无可避免的,我们需要寻找新的技术方案。
尤雨溪在他的文档中推荐大家用axios进行网络请求。axios基于Promise对原生的XHR进行了非常全面的封装,使用方式也非常的优雅。另外,axios同样提供了在node环境下的支持,可谓是网络请求的首选方案。
未来必定还会出现更优秀的封装,他们有非常周全的考虑以及详细的文档,这里我们不多做考究,我们把关注的重点放在更底层的 APIfetch。
Fetch API是一个用用于访问和操纵 HTTP 管道的强大的原生 API。
这种功能以前是使用 XMLHttpRequest 实现的。Fetch 提供了一个更好的替代方法,可以很容易地被其他技术使用,例如 Service Workers。Fetch 还提供了单个逻辑位置来定义其他 HTTP 相关概念,例如 CORS 和 HTTP 的扩展。
可见fetch是作为XMLHttpRequest的替代品出现的。
使用fetch,你不需要再额外加载一个外部资源。但它还没有被浏览器完全支持,所以你仍然需要一个polyfill。
# fetch 的使用
一个基本的 fetch 请求:
const options = {
method: "POST", // 请求参数
headers: { "Content-Type": "application/json" }, // 设置请求头
body: JSON.stringify({ name: "123" }), // 请求参数
credentials: "same-origin", // cookie设置
mode: "cors", // 跨域
};
fetch("http://www.xxx.com")
.then(function (response) {
return response.json();
})
.then(function (myJson) {
console.log(myJson); // 响应数据
})
.catch(function (err) {
console.log(err); // 异常处理
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Fetch API提供了一个全局的fetch()方法,以及几个辅助对象来发起一个网络请求。

fetch()
fetch()方法用于发起获取资源的请求。它返回一个promise,这个 promise 会在请求响应后被 resolve,并传回 Response 对象。
Headers
可以通过Headers()构造函数来创建一个你自己的headers对象,相当于 response/request 的头信息,可以使你查询到这些头信息,或者针对不同的结果做不同的操作。
var myHeaders = new Headers();
myHeaders.append("Content-Type", "text/plain");
2
Request
通过Request()构造函数可以创建一个Request对象,这个对象可以作为fetch函数的第二个参数。
Response
在fetch()处理完promises之后返回一个Response实例,也可以手动创建一个Response实例。
# 九、fetch polyfill 源码分析
由于fetch是一个非常底层的API,所以我们无法进一步的探究它的底层,但是我们可以借助它的polyfill探究它的基本原理,并找出其中的坑点。
# 代码结构

由代码可见,polyfill主要对Fetch API 提供的四大对象进行了封装:
# fetch 封装

代码非常清晰:
- 构造一个
Promise对象并返回 - 创建一个
Request对象 - 创建一个
XMLHttpRequest对象 - 取出
Request对象中的请求url,请求方发,open一个xhr请求,并将Request对象中存储的headers取出赋给 xhr xhr onload后取出response的status、headers、body封装Response对象,调用resolve。
# 异常处理

可以发现,调用reject有三种可能:
1.请求超时
2.请求失败
注意:当和服务器建立简介,并收到服务器的异常状态码如404、500等并不能触发onerror。当网络故障时或请求被阻止时,才会标记为 reject,如跨域、url不存在,网络异常等会触发onerror。
所以使用 fetch 当接收到异常状态码都是会进入 then 而不是 catch。这些错误请求往往要手动处理。
- 3.手动终止
可以在request参数中传入signal对象,并对signal对象添加abort事件监听,当xhr.readyState变为4(响应内容解析完成)后将 signal 对象的 abort 事件监听移除掉。
这表示,在一个fetch请求结束之前可以调用signal.abort将其终止。在浏览器中可以使用AbortController()构造函数创建一个控制器,然后使用AbortController.signal属性
这是一个实验中的功能,此功能某些浏览器尚在开发中
# Headers 封装

在 header 对象中维护了一个map对象,构造函数中可以传入Header对象、数组、普通对象类型的header,并将所有的值维护到map中。
之前在fetch函数中看到调用了header的forEach方法,下面是它的实现:

可见header的遍历即其内部map的遍历。
另外Header还提供了append、delete、get、set等方法,都是对其内部的map对象进行操作。
# Request 对象

Request对象接收的两个参数即fetch函数接收的两个参数,第一个参数可以直接传递url,也可以传递一个构造好的request对象。第二个参数即控制不同配置的option对象。
可以传入credentials、headers、method、mode、signal、referrer等属性。
这里注意:
- 传入的
headers被当作Headers构造函数的参数来构造 header 对象。
# cookie 处理
fetch 函数中还有如下的代码:
if (request.credentials === "include") {
xhr.withCredentials = true;
} else if (request.credentials === "omit") {
xhr.withCredentials = false;
}
2
3
4
5
默认的credentials类型为same-origin,即可携带同源请求的 coodkie。
然后我发现这里 polyfill 的实现和MDN-使用 Fetch (opens new window)以及很多资料是不一致的:
mdn: 默认情况下,fetch 不会从服务端发送或接收任何 cookies
于是我分别实验了下使用polyfill和使用原生fetch携带 cookie 的情况,发现在不设置credentials的情况下居然都是默认携带同源cookie的,这和文档的说明说不一致的,查阅了许多资料后都是说fetch默认不会携带 cookie,下面是使用原生fetch在浏览器进行请求的情况:

然后我发现在MDN-Fetch-Request (opens new window)已经指出新版浏览器credentials默认值已更改为same-origin,旧版依然是omit。
确实MDN-使用 Fetch (opens new window)这里的文档更新的有些不及时,误人子弟了...
# Response 对象
Response对象是fetch调用成功后的返回值:
回顾下fetch中对Response`的操作:
xhr.onload = function () {
var options = {
status: xhr.status,
statusText: xhr.statusText,
headers: parseHeaders(xhr.getAllResponseHeaders() || ""),
};
options.url =
"responseURL" in xhr
? xhr.responseURL
: options.headers.get("X-Request-URL");
var body = "response" in xhr ? xhr.response : xhr.responseText;
resolve(new Response(body, options));
};
2
3
4
5
6
7
8
9
10
11
12
13
Response构造函数:

可见在构造函数中主要对options中的status、statusText、headers、url等分别做了处理并挂载到Response对象上。
构造函数里面并没有对responseText的明确处理,最后交给了_initBody函数处理,而Response并没有主动声明_initBody属性,代码最后使用Response调用了Body函数,实际上_initBody函数是通过Body函数挂载到Response身上的,先来看看_initBody函数:

可见,_initBody函数根据xhr.response的类型(Blob、FormData、String...),为不同的参数进行赋值,这些参数在Body方法中得到不同的应用,下面具体看看Body函数还做了哪些其他的操作:

Body函数中还为Response对象挂载了四个函数,text、json、blob、formData,这些函数中的操作就是将_initBody 中得到的不同类型的返回值返回。
这也说明了,在fetch执行完毕后,不能直接在response中获取到返回值而必须调用text()、json()等函数才能获取到返回值。
这里还有一点需要说明:几个函数中都有类似下面的逻辑:
var rejected = consumed(this);
if (rejected) {
return rejected;
}
2
3
4
consumed 函数:
function consumed(body) {
if (body.bodyUsed) {
return Promise.reject(new TypeError("Already read"));
}
body.bodyUsed = true;
}
2
3
4
5
6
每次调用text()、json()等函数后会将bodyUsed变量变为true,用来标识返回值已经读取过了,下一次再读取直接抛出TypeError('Already read')。这也遵循了原生fetch的原则:
因为 Responses 对象被设置为了 stream 的方式,所以它们只能被读取一次
# 十、fetch 的坑点
VUE的文档中对fetch有下面的描述:
使用
fetch还有很多别的注意事项,这也是为什么大家现阶段还是更喜欢axios多一些。当然这个事情在未来可能会发生改变。
由于fetch是一个非常底层的API,它并没有被进行很多封装,还有许多问题需要处理:
- 不能直接传递
JavaScript对象作为参数 - 需要自己判断返回值类型,并执行响应获取返回值的方法
- 获取返回值方法只能调用一次,不能多次调用
- 无法正常的捕获异常
- 老版浏览器不会默认携带
cookie - 不支持
jsonp
# 十一、对 fetch 的封装
# 请求参数处理
支持传入不同的参数类型:
function stringify(url, data) {
var dataString = url.indexOf("?") == -1 ? "?" : "&";
for (var key in data) {
dataString += key + "=" + data[key] + "&";
}
return dataString;
}
if (request.formData) {
request.body = request.data;
} else if (/^get$/i.test(request.method)) {
request.url = `${request.url}${stringify(request.url, request.data)}`;
} else if (request.form) {
request.headers.set(
"Content-Type",
"application/x-www-form-urlencoded;charset=UTF-8"
);
request.body = stringify(request.data);
} else {
request.headers.set("Content-Type", "application/json;charset=UTF-8");
request.body = JSON.stringify(request.data);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# cookie 携带
fetch在新版浏览器已经开始默认携带同源cookie,但在老版浏览器中不会默认携带,我们需要对他进行统一设置:
request.credentials = "same-origin"; // 同源携带
request.credentials = "include"; // 可跨域携带
2
# 异常处理
当接收到一个代表错误的 HTTP 状态码时,从 fetch()返回的 Promise 不会被标记为 reject, 即使该 HTTP 响应的状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (但是会将 resolve 的返回值的 ok 属性设置为 false ),仅当网络故障时或请求被阻止时,才会标记为 reject。
因此我们要对fetch的异常进行统一处理
.then(response => {
if (response.ok) {
return Promise.resolve(response);
}else{
const error = new Error(`请求失败! 状态码: ${response.status}, 失败信息: ${response.statusText}`);
error.response = response;
return Promise.reject(error);
}
});
2
3
4
5
6
7
8
9
# 返回值处理
对不同的返回值类型调用不同的函数接收,这里必须提前判断好类型,不能多次调用获取返回值的方法:
.then(response => {
let contentType = response.headers.get('content-type');
if (contentType.includes('application/json')) {
return response.json();
} else {
return response.text();
}
});
2
3
4
5
6
7
8
# jsonp
fetch本身没有提供对jsonp的支持,jsonp本身也不属于一种非常好的解决跨域的方式,推荐使用cors或者nginx解决跨域,具体请看下面的章节。
fetch 封装好了,可以愉快的使用了。
嗯,axios 真好用...
文中如有错误,欢迎在评论区指正,谢谢阅读。