近年来前端MV*的发展壮大,人们越来越少的使用jQuery,我们不可能单独为了使用jQueryAjax 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); // 异常处理
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Fetch API提供了一个全局的fetch()方法,以及几个辅助对象来发起一个网络请求。

image

  • fetch()

fetch()方法用于发起获取资源的请求。它返回一个promise,这个 promise 会在请求响应后被 resolve,并传回 Response 对象。

  • Headers

可以通过Headers()构造函数来创建一个你自己的headers对象,相当于 response/request 的头信息,可以使你查询到这些头信息,或者针对不同的结果做不同的操作。

var myHeaders = new Headers();
myHeaders.append("Content-Type", "text/plain");
1
2
  • Request

通过Request()构造函数可以创建一个Request对象,这个对象可以作为fetch函数的第二个参数。

  • Response

fetch()处理完promises之后返回一个Response实例,也可以手动创建一个Response实例。

# 九、fetch polyfill 源码分析

由于fetch是一个非常底层的API,所以我们无法进一步的探究它的底层,但是我们可以借助它的polyfill探究它的基本原理,并找出其中的坑点。

# 代码结构

image

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

# fetch 封装

image

代码非常清晰:

  • 构造一个Promise对象并返回
  • 创建一个Request对象
  • 创建一个XMLHttpRequest对象
  • 取出Request对象中的请求url,请求方发,open一个xhr请求,并将Request对象中存储的headers取出赋给 xhr
  • xhr onload后取出responsestatusheadersbody封装Response对象,调用resolve

# 异常处理

image

可以发现,调用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 封装

image

在 header 对象中维护了一个map对象,构造函数中可以传入Header对象、数组、普通对象类型的header,并将所有的值维护到map中。

之前在fetch函数中看到调用了headerforEach方法,下面是它的实现:

image

可见header的遍历即其内部map的遍历。

另外Header还提供了append、delete、get、set等方法,都是对其内部的map对象进行操作。

# Request 对象

image

Request对象接收的两个参数即fetch函数接收的两个参数,第一个参数可以直接传递url,也可以传递一个构造好的request对象。第二个参数即控制不同配置的option对象。

可以传入credentials、headers、method、mode、signal、referrer等属性。

这里注意:

  • 传入的headers被当作Headers构造函数的参数来构造 header 对象。

fetch 函数中还有如下的代码:

if (request.credentials === "include") {
  xhr.withCredentials = true;
} else if (request.credentials === "omit") {
  xhr.withCredentials = false;
}
1
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在浏览器进行请求的情况:

image

然后我发现在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));
};
1
2
3
4
5
6
7
8
9
10
11
12
13

Response构造函数:

image

可见在构造函数中主要对options中的status、statusText、headers、url等分别做了处理并挂载到Response对象上。

构造函数里面并没有对responseText的明确处理,最后交给了_initBody函数处理,而Response并没有主动声明_initBody属性,代码最后使用Response调用了Body函数,实际上_initBody函数是通过Body函数挂载到Response身上的,先来看看_initBody函数:

image

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

image

Body函数中还为Response对象挂载了四个函数,text、json、blob、formData,这些函数中的操作就是将_initBody 中得到的不同类型的返回值返回。

这也说明了,在fetch执行完毕后,不能直接在response中获取到返回值而必须调用text()、json()等函数才能获取到返回值。

这里还有一点需要说明:几个函数中都有类似下面的逻辑:

var rejected = consumed(this);
if (rejected) {
  return rejected;
}
1
2
3
4

consumed 函数:

function consumed(body) {
  if (body.bodyUsed) {
    return Promise.reject(new TypeError("Already read"));
  }
  body.bodyUsed = true;
}
1
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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

fetch在新版浏览器已经开始默认携带同源cookie,但在老版浏览器中不会默认携带,我们需要对他进行统一设置:

request.credentials = "same-origin"; // 同源携带
request.credentials = "include"; // 可跨域携带
1
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);
  }
});
1
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();
  }
});
1
2
3
4
5
6
7
8

# jsonp

fetch本身没有提供对jsonp的支持,jsonp本身也不属于一种非常好的解决跨域的方式,推荐使用cors或者nginx解决跨域,具体请看下面的章节。

fetch 封装好了,可以愉快的使用了。

嗯,axios 真好用...

文中如有错误,欢迎在评论区指正,谢谢阅读。