JavaScript 网络请求 Fetch

JavaScript 可以将网络请求发送到服务器,并在需要时加载新信息。例如在表单提交、从服务器加载数据等都需要使用网络请求。对于来自 JavaScript 的网络请求,有一个总称术语 “AJAX”,有很多方式可以向服务端发送网络请求获取到数据。fetch()是目前比较通用的方法。

基本语法

let promise = fetch(url, [options])
  • url:要访问的 url地址
  • options:可选参数,methodheader等,参数为空则发起 GET请求
  • 返回值为一个 Promise 对象

请求阶段

获取到响应通常要经历两个阶段。

第一阶段:当服务器发送了响应头(response header),fetch 返回的 promise 就使用内建的 Response class 对象来对响应头进行解析。此时,我们可以通过检查响应头来检查 HTTP 状态来确认网络请求是否成功,这是还没有响应体 response body

fetch 无法建立一个 HTTP 请求,如网络问题,或是请求的网址不存在,那么 promise 就会 reject。异常的 HTTP 状态,例如 404 或 500,不会导致出现 error

  • response.status -- HTTP 状态码
  • response.ok -- Boolean,若为 200 - 299 为 true

第二阶段:获取到 response bodyResponse 提供了多种基于 promise 的方法,来以不同的格式访问 body

  • response.text() —— 读取 response,并以文本形式返回 response
  • response.json() —— 将 response 解析为 JSON
  • response.formData() —— 以 FormData 对象的形式返回 response
  • response.blob() —— 以 Blob(具有类型的二进制数据)形式返回 response
  • response.arrayBuffer() —— 以 ArrayBuffer(低级别的二进制数据)形式返回 response
  • 另外,response.bodyReadableStream 对象,它允许你逐块读取 body,我们稍后会用一个例子解释它。

我们只能选择一种读取 body 的方法。

如果我们已经使用了 response.text() 方法来获取 response,那么如果再用 response.json(),则不会生效,因为 body 内容已经被处理过了。

POST 请求

let response = await fetch(url, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  },
  body: requestBody
});
  • body -- 请求体,可以是字符串、FormData对象(以 form/multipart 形式发送)、Blob/BufferSource二进制数据等

如果请求的 body 是字符串,则 Content-Type 会默认设置为 text/plain;charset=UTF-8。但是,当我们要发送 JSON 时,我们会使用 headers 选项来发送 application/json,这是 JSON 编码的数据的正确的 Content-Type

用法小结

典型的 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 => /* 处理结果 */)

实例

从 GitHub 获取用户信息:

  async function getUsers(names) {
    const response = await fetch(`https://api.github.com/users/${names}`);
    if (response.ok) {
    		const result = await response.json();
    		console.log(result);
    } else {
      return null;
    }
  }
  getUsers('qzlu-cyber');

Fetch 跟踪下载进度

fetch() 无法跟踪上传进度,但是可以跟踪下载进度。要跟踪下载进度,可以使用 response.body 属性。它是 ReadableStream —— 一个特殊的对象,它可以逐块(chunk)提供 body

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

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

  if (done) {
    break;
  }

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

fetch 中止

JavaScript 有一个特殊的内建对象 AbortController,既可以中止 fetch 也可以中止其他异步任务。

用法:

let controller = new AbortController();

controller 对象具有 abort() 方法和 signal属性。当 abort() 被调用时,abort 事件会在 controller.signal 上触发,同时 controller.signal.aborted 属性变为 true

let controller = new AbortController();
fetch(url, {
  signal: controller.signal // 将 signal 属性传递给 fetch 参数
}); // fetch 会监听 signal 上的 abort

controller.abort(); // 调用 controller.abort() 来中止

当一个 fetch 被中止,它的 promise 就会以一个 error AbortError 被 reject:

try {
  let response = await fetch('/article/fetch-abort/demo/hang', {
    signal: controller.signal
  });
} catch(err) {
  if (err.name == 'AbortError') { // 处理 abort()
    alert("Aborted!");
  } else {
    throw err;
  }
}

AbortController 是可扩展的,它允许一次取消多个 fetch。也可以等待需要完成的 fetch 异步网络请求后中止其他 fetch

let urls = [...];
let controller = new AbortController();

let ourJob = new Promise((resolve, reject) => { // 需要完成的 fetch
  ...
  controller.signal.addEventListener('abort', reject);
});

let fetchJobs = urls.map(url => fetch(url, { // 所有 fetch
  signal: controller.signal
}));

// 等待完成我们的任务和所有 fetch
let results = await Promise.all([...fetchJobs, ourJob]);

// 如果 controller.abort() 被从其他地方调用,
// 它将中止所有 fetch 和 ourJob