JavaScript – Fetch


前言

我几乎没有用过 Fetch. 第一次接触 Fetch 是 AngularJS (那年 es6 还没有普及). 但是 AngularJS 已经封装它了. 后来只是阅读了一些教程.

发现它有一个致命缺点, 不支持 upload 进度. 于是就再也没有关注它了. 再后来用了 Angular 2 的 Http 到 Angular 4 的 HttpClient 都是上层封装.

一直到最近看了一篇文章说 Fetch 比以前好一点了, 好像是支持了 abort, 但依然没有支持 upload 进度. 趁着我在复习 JavaScript 也一起整理它吧.

参考

阮一峰 – Fetch API 教程

Fetch 的好处

Fetch 是 es6 的产物, 它简化了以前 XMLHttpRequest 的使用方式. 可以算是 XMLHttpRequest 的替代品 (唯一无法替代的地方是, Fetch 不支持 upload 进度, 真的是唯一败笔啊)

Fetch 有 3 个好处:

1. 它基于 Promise. Promise 比 XMLHttpRequest.onreadystatechange 好太多了. 搭配 await 更理想.

2 Request, Header, Response class, Fetch 用了 3 个主要的 class 来完成工作, 对比 XMLHttpRequest 就一个 class 太臃肿了

3. 支持下载分段, 通过 body.getReader 可以一部分一部分的提前使用 response data. 而像 XMLHttpRequest 必须等所有 data 下载完了才可以开始用. 

1 和 2 主要是在代码优化上面加分. 第 3 点则是具体的功能 (虽然很少项目需要).

吐槽 XMLHttpRequest 

下面这个是 XMLHttpRequest 的写法

const xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = () => {
  if (xhttp.readyState === 4) {
    console.log(xhttp.status);
  }
};
xhttp.open('GET', 'https://localhost:7063/WeatherForecast', true);
xhttp.send();

下面这个是 Fetch 的写法

const response = await fetch('https://localhost:7063/WeatherForecast');
console.log(response.status);

没有对比没有伤害, XMLHttpRequest 的调用接口设计糟糕透了...

Send Request (GET)

simplest

先从最简单的 GET 说起

fetch('https://localhost:7063/WeatherForecast')

因为 fetch 默认的 method 是 GET, 所以只要给一个地址, 它就会发 GET 请求了.

with query params

query params 没有被 Fetch 封装, 直接用 URLSearchParams build 好后拼接到 URL 即可

const queryParams = new URLSearchParams({
  param1: 'param1',
  param2: 'param2',
});
fetch(`https://localhost:7063/WeatherForecast?${queryParams.toString()}`);

with headers

fetch('https://localhost:7063/WeatherForecast', {
  headers: {
    Custom: 'custom header value',
    Accept: 'application/json; charset=utf-8',
  },
});

Fetch 的第二个参数可以配置所以的 options

headers 属性的 interface 是

下面这 2 种写法都是 ok 的

with credentials

允许同域发送 cookie

fetch('https://localhost:7063/WeatherForecast', {
  credentials: 'same-origin',
});

with Request

fetch('https://localhost:7063/WeatherForecast', {
  headers: {
    Accept: 'application/json; charset=utf-8',
  },
});

也可以先创建 Request 才传入 fetch

const request = new Request('https://localhost:7063/WeatherForecast', {
  headers: {
    Accept: 'application/json; charset=utf-8',
  },
});
const response = await fetch(request);

new Request 的参数和 fetch 是同样的. Request 的好处是可以 clone, 修改, 容易复用.

Read Response

response 分 2 part. 一个是 header info, status 这类. 它们是马上可以读取的 (同步)

另一 part 是 body, 它是异步读取的. 而且可以分段式读.

read status

const response = await fetch('https://localhost:7063/WeatherForecast');
const status = response.status; // 200;
const succeed = response.ok; // true

response.ok 是一个方便. 只要 status 是 200 – 299 都算 ok.

read header

使用 header 对象. 它提供了一些方便的接口, 比如 get, has, keys, values, entries 等等

const response = await fetch('https://localhost:7063/WeatherForecast');
const contentType = response.headers.get('Content-Type'); // application/json; charset=utf-8
const customHeaderValue = response.headers.get('custom'); // 'a, b' 如果有 2 个会 combine by common + space

重复 header?

如果有重复的 header 那么它会合并起来

结果是 'a, b'

跨域 custom header

服务端记得返回 expose header, 不然跨域情况下是拿不到 custom header 的哦

read body text

所以 body 都是异步读取的.

const textContent = await response.text();

如果返回的是 plain text 或者你想拿到 json string 就可以使用 .text()

read body json

const parsedJsonValue = await response.json();

.json() 还会帮你把 json string 拿去 parse 哦.

blob, arrayBuffer, formData

还有 3 种 read from body, 比较少用到.

blob 用在拿 image, files

arrayBuffer 用在拿 audio, video

formData 用在 Service Worker 拦截 formData post request

multiple read body

const response = await fetch('https://localhost:7063/WeatherForecast');
const jsonText = await response.text();
const jsonValue = await response.json();

一个 response 不能被读取超过 1 次. 上面这样 .text 又 .json 是不 ok 的. 会报错

解决方法是先 clone 一个 response 就可以了.

const jsonText = await response.clone().text(); // 加了 clone
const jsonValue = await response.json();

body reader 分段式读取

const response = await fetch('https://localhost:7063/WeatherForecast');
const reader = response.body!.getReader();
while (true) {
  const { done, value } = await reader.read();
  if (done) {
    break;
  }
  console.log(`Received ${value.length} bytes`);
}

下面是我 request 一个 mp4 的效果

配上 header 的 Content-Length 配上 data.length 就可以显示下载进度了, 这种分段式读取是 XMLHttpRequest 做不到的.

Send Request (POST)

之前写过一篇 , 里面的 Ajax Form 有介绍了如何用 XMLHttpRequest send post request.

Fetch 的逻辑也差不多

发 JSON

fetch('https://localhost:7063/WeatherForecast', {
  headers: {
    'Content-Type': 'application/json; charset=utf-8',
  },
  method: 'POST',
  body: JSON.stringify({ username: 'Derrick' }),
});

记得要添加 header Content-Type 哦.

发 application/x-www-form-urlencoded 和 multipart/form-data

fetch('https://localhost:7063/WeatherForecast', {
  method: 'POST',
  body: new URLSearchParams({ username: 'Derrick' }), // application/x-www-form-urlencoded
  body: new FormData(document.querySelector('form')!), // multipart/form-data
});

记得, 不要添加 header Content-Type, 游览器会自己添加.

Abort Request

abort 是用来中断请求的, 可能 user 按错, 或者后悔了. 

通常只有那些很耗时的操作才需要设计 abort. 比如批量修改.

当前端 abort 以后, 后端就好也是处理一下. 避免不必要的消耗.

创建 abort controller

const abortCtrl = new AbortController();

把 signal 放入 fetch

const request = new Request('https://localhost:7063/WeatherForecast', {
  signal: abortCtrl.signal
});

发送, 一秒后 abort

await fetch(request);

setTimeout(() => {
  abortCtrl.abort();
}, 1000);

前端监听 abort

可以通过 signal 监听和查看当前 abort status

abortCtrl.signal.addEventListener('abort', () => {
  console.log('abort done');
});

后端判断 abort

ASP.NET Core 有一个 cancellationToken, 可以随时查看是否前端已经 abort 了 request, 如果已经 abort 那么就没有必要继续执行了, 直接返回就是了. 

相关