# 网络请求-Fetch

# Fetch

JavaScript 可以将网络请求发送到服务器,并在需要时加载新信息。

例如,我们可以使用网络请求来:

  • 提交订单,
  • 加载用户信息,
  • 从服务器接收最新的更新, ……等。 ……所有这些都没有重新加载页面!

对于来自 JavaScript 的网络请求,有一个总称术语 “AJAX”(Asynchronous JavaScript And XML 的简称)。虽然现在我们使用 JSON 多于使用 XML。由于这个术语诞生于很久以前,所以这个词一直在那儿。

基本语法:

let promise = fetch(url, [options]);

第一阶段,当服务器发送了响应头(response header),fetch 返回的 promise 就使用内建的 Response class 对象来对响应头进行解析。

第二阶段,为了获取 response body,我们需要使用一个其他的方法调用。

方法 作用
res.text() 读取 response,并以文本形式返回
res.json() 将 response 解析为 JSON
res.formData() 读取 response,并以文本形式返回
res.blob()
res.arrayBuffer()

TIP

注意 fetch 返回的是一个 promise,response 的方法返回的也是一个 promise

# 示例

blob 示例

let response = await fetch("/article/fetch/logo-fetch.svg");

let blob = await response.blob(); // 下载为 Blob 对象

// 为其创建一个 <img>
let img = document.createElement("img");
img.style = "position:fixed;top:10px;left:10px;width:100px";
document.body.append(img);

// 显示它
img.src = URL.createObjectURL(blob);

setTimeout(() => {
  // 3 秒后将其隐藏
  img.remove();
  URL.revokeObjectURL(img.src);
}, 3000);

上面我们发送了 GET 请求,现在我们来发送 POST 请求

let user = {
  name: "John",
  surname: "Smith"
};

let response = await fetch("/article/fetch/post/user", {
  method: "POST",
  headers: {
    "Content-Type": "application/json;charset=utf-8" // 注意发送JSON的时候设置"Content-Type"
  },
  body: JSON.stringify(user)
});

let result = await response.json();
alert(result.message);

发送图片可以直接使用 blob,而不需要专门设置"Content-Type",浏览器会自动处理

let blob = await new Promise((resolve) => canvasElem.toBlob(resolve, "image/png"));
let response = await fetch("/article/fetch/post/image", {
  method: "POST",
  body: blob
});

# Request header

要在 fetch 中设置 request header,我们可以使用 headers 选项。它有一个带有输出 header 的对象,如下所示:

let response = fetch(protectedUrl, {
  headers: {
    Authentication: "secret"
  }
});

# Response header

Response header 位于 response.headers 中的一个类似于 Map 的 header 对象。

它不是真正的 Map,但是它具有类似的方法,我们可以按名称(name)获取各个 header,或迭代它们。

TIP

HTTP 报文的 header 是纯文本的形式,这不利于我们直接处理。因而浏览器的封装是有必要的额

# 总结

典型的 fetch 请求由两个 await 调用组成:

let response = await fetch(url, options); // 解析 response header
let result = await response.json(); // 将 body 读取为 json

或者以 promise 形式:

fetch(url, options)
  .then(response => response.json())
  .then(result => /* process result */)

# Fetch: progress

fetch 方法允许去跟踪 下载 进度。

要跟踪下载进度,我们可以使用 response.body 属性,response.body 给予了对进度读取的完全控制,我们可以随时计算下载了多少。

下面有一个比较脏的实现

// 代替 response.json() 以及其他方法
const reader = response.body.getReader();

// 在 body 下载时,一直为无限循环
while (true) {
  // 当最后一块下载完成时,done 值为 true
  // value 是块字节的 Uint8Array
  const { done, value } = await reader.read();

  if (done) {
    break;
  }

  console.log(`Received ${value.length} bytes`);
}

这个实现我们可以改造成定时器或者 RAF 版本,不过暂时还是不折腾了。

# Fetch: abort

正如我们所知道的,fetch 返回一个 promise。JavaScript 通常并没有“中止” promise 的概念。那么我们怎样才能取消一个正在执行的 fetch 呢?例如,如果用户在我们网站上的操作表明不再需要 fetch。

为此有一个特殊的内建对象:AbortController。它不仅可以中止 fetch,还可以中止其他异步任务。

用法非常简单。

# AbortController 对象

创建一个控制器(controller):

let controller = new AbortController();

控制器是一个极其简单的对象。

它具有单个方法 abort(), 和单个属性 signal,我们可以在这个属性上设置事件监听器。

let controller = new AbortController();
let signal = controller.signal;

signal.addEventListener("abort", () => alert("abort!"));

controller.abort(); // 中止!

alert(signal.aborted); // true

# 与 fetch 一起使用

为了能够取消 fetch,请将 AbortController 的 signal 属性作为 fetch 的一个可选参数(option)进行传递

let controller = new AbortController();
fetch(url, {
  signal: controller.signal
});
controller.abort();

cancelToken

我们常用的请求库也具备这项功能,比如 axios 的cancelToken,可以类比上下两段代码,我们可以发现其用法惊人的一致。

const source = axios.CancelToken.source();
axios.get("/user/12345", {
  cancelToken: source.token
});
source.cancel("Operation canceled by the user.");