近年来前端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
调用成功后的返回值:
回顾下f
etch中对
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 真好用...
文中如有错误,欢迎在评论区指正,谢谢阅读。