Fetch

image-20221203230444744

Promise<Response> fetch(input[, init])

fetch是浏览器提供的一个符合Promise规范的请求方法,在fetch出现之前最常用的请求方式是XHR(XMLHttpRequest),通过回调和事件来编写异步请求流程,如果请求之间存在强关联,很容易写出”回调地域“形式的代码,如下:

function requestData(url, data, callback) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', url, true);
  xhr.onreadystatechange = function () {
    if (xhr.readyState === 4) {
      if (xhr.status === 200) {
        var data = JSON.parse(xhr.responseText);
        callback(null, data);
      } else {
        callback(new Error('Error loading data'));
      }
    }
  };
  xhr.send(data);
}

// 请求第一层数据
requestData('https://api.example.com/data1', {}, function (error, data1) {
  if (error) {
    console.error('Failed to load data1:', error);
  } else {
    // 请求第二层数据
    requestData(
      'https://api.example.com/data2',
      { id: data1[0].id },
      function (error, data2) {
        if (error) {
          console.error('Failed to load data2:', error);
        } else {
          // 请求第三层数据
          requestData(
            'https://api.example.com/data3',
            { id: data2[0].id, type: data[1].type },
            function (error, data3) {
              if (error) {
                console.error('Failed to load data3:', error);
              } else {
                // 继续后续操作
                console.log('Data loaded:', data1, data2, data3);
                // 可能还有更多嵌套的回调...
              }
            },
          );
        }
      },
    );
  }
});

fetch 出现之后,我们可以通过更优雅的基于Promise的异步方式进行请求,如下

async function requestData(url: string, data?: any) {
  const response = await fetch(url, {
    method: 'POST',
    body: JSON.stringify(data),
  });
  if (!response.ok) {
    throw new Error(`Error loading data from ${url}: ${response.statusText}`);
  }
  return await response.json();
}

async function loadData() {
  try {
    // 请求第一层数据
    const data1 = await requestData('https://api.example.com/data1');
    console.log('First data loaded:', data1);

    // 使用第一层数据作为参数请求第二层数据
    const data2 = await requestData('https://api.example.com/data2', {
      id: data1[0].id,
    });
    console.log('Second data loaded:', data2);

    // 使用第二层数据作为参数请求第三层数据
    const data3 = await requestData('https://api.example.com/data3', {
      id: data2[0].id,
      type: data[1].type,
    });
    console.log('Third data loaded:', data3);

    // 处理所有数据
    console.log('All data processed:', data1, data2, data3);
  } catch (error) {
    console.error('Failed to load data:', error);
  }
}

fetch 的第二个参数是一个配置对象,可以用来控制请求的一些行为

参数 描述
method 请求方法,如 GET、POST、PUT 等
headers 请求头信息
body 请求体内容,可以是字符串、FormData 对象或者 Blob 对象
mode 请求模式,如 cors、no-cors 或 same-origin
credentials 包含 cookies 在内的凭据模式,如 omit、same-origin 或 include
cache 缓存模式,如 default、no-store、reload 或 force-cache
redirect 跟随重定向方式, 如 follow, error 或 manual
referrerPolicy 控制请求的 referrer 头部发送情况

我们可以基于 fetch 来封装一个比较通用的请求方法

const initialHeaders = {
  'Content-Type': 'application/json',
}

/**
 * 
 * @param {string} method 请求方法
 * @param {string} url 请求地址
 * @param {string} body  请求数据
 * @Param {Headers} headers 请求头
 */
async function request(method, url, body, headers = initialHeaders) {
  // 请求拦截
  const response = await fetch(url, {
    method,
    headers,
    body
  })

  // 响应拦截
  if (response.status > 299) {
    // error fix
  }

  return response.json()
}

function get(url, params = {}) {
  return request('GET', `${url}?${getQueryString(params)}`)
}

function post(url, body = {}) {
  return request('POST', url, JSON.stringify(body))
}


function getQueryString(queryObj) {
  return Object.keys(queryObj)
    .map((key) => {
      const value = queryObj[key];
      return `${key}=${value}`;
    })
    .join('&');
}

Promise规范是fetch比起XHR最优秀的改变,当然这不是全部,fetch实现了一个更为完善的异步请求架构,包括Headers、Request、Response,通过这三部分,可以对请求进行更加精细的控制。

📢 虽然 fetch 返回值是 Promise,但是 HTTP 响应的异常状态(404, 502……非 200 状态段)并不会标记为 reject,而是 resolve 一个 false 值,只有在网络请求失败时才会标记为 reject。

中断请求

fetch 中支持中断请求,我们可以使用AbortController来中断 Web 请求。

fetch 的参数中支持传入一个 single,只需要要将 AbortController 对象的single 属性传入,即可使用该AbortController 的 abort 方法来中断这个请求。

let controller;

function fetchFile(url) {
  controller = new AbortController()
  const single = controller.signal

  fetch(url, {
    single
  }).then(res => {
    console.log('下载成功')
    return res
  })
}

function stop() {
  if (controller) {
    controller.abort()
    console.log('下载中止')
  }
}

Headers

常用的 headers 直接在 fetch 参数中用对象的形式写入的,其实 Fetch API 还提供了Header 构造器,很不方便但是有些功能限制在 ServiceWorker 中使用。

const initialHeaders = Headers.Headers({
  'Content-Type': 'application/json',
})
initialHeaders.set('Authorization', 'xxxx')
initialHeaders.append('Content-Type', 'charset=utf-8')
initialHeaders.get('Accept')
initialHeaders.has('Accept')
  • append:给 header 添加一个值或者添加一个不存在的 header 并赋值;
  • delete:从 Headers 对象中删除指定 header;
  • entries:返回 Headers 对象中所有的键值对;
  • get:从 Headers 对象中返回指定 header 的全部值;
  • has:从 Headers 对象中返回是否存在指定的 header;
  • keys:返回 Headers 对象中所有存在的 header 名;
  • set:替换现有的 header 的值,或者添加一个未存在的 header 并赋值;
  • values:返回 Headers 对象中所有存在的 header 的值。

set 和 append 的区别在于,set 会覆盖之前的值,append 会在原有的值后追加;

例如

content-type: application/javascript; charset=utf-8

可以通过 append 添加两段或者直接用 set 添加一段

headers.set(‘content-type’, ‘application/javascript; charset=utf-8’)

headers.append(‘Content-Type’, ‘application/javascript’)

headers.append(‘Content-Type’, ‘charset=utf-8’)

Request 和 Response

一个网络请求分为”请求“和”响应“两个过程,这两个过程分别对应Requesth和Response两个模块,通常我们是不需要手动构造的,fetch函数会帮我们构造,一般我们使用的fetch常用API都是基于Response对象的,Request在fetch的使用过程中一般用不到(虽然不怎么用到,还是要了解一下)。

构造一个Request实例所需要的参数和fetch请求的参数是一样的,因为fetch的输入操作就是Request承接的,fetch的参数也可以为一个Request实例。

const request = new Request("https://example.com/api");

fetch(request)
  .then((response) => response.json());

fetch的返回值就是Response实例,Response有很多方便开发者处理响应的实例方法

实例方法 作用
arrayBuffer 将响应体解析为二进制数组
blob 将响应体解析为Blob对象
clone 克隆响应实例对象,因为响应体只能使用一次,克隆之后可以分别进行不同处理
formData 将响应体解析为FormData对象
json 将响应体解析为json对象
text 将响应体解析为纯文本

这些方法都是对response.body进行处理,响应体body其实是一个可读流(流的相关知识可以参考“流”),如果不想使用Response提供的实例方法,需要自行实现处理方法,可以通过response.body来进行解析。例如我们可以通过流式加载文本信息到页面上以实现打字机效果

fetch(url)
  .then(response => {
    const reader = response.body.getReader();

    return new ReadableStream({
      start(controller) {
        const decoder = new TextDecoder();

        function read() {
          return reader.read().then(({ done, value }) => {
            if (done) {
              controller.close();
              return;
            }

            controller.enqueue(decoder.decode(value));
            setTimeout(read, 50); // 控制展示速度,单位为毫秒
          });
        }

        read();
      },
    });
  })
  .then(stream =>
    stream.pipeThrough(
      new WritableStream({
        write(chunk) {
          document.getElementById('content').innerText += chunk; // 将内容显示到页面上的某个元素中
        },
      })
    )
  );

上面表格列举的Response实例方法,在Request中也有相同的实例方法,区别就是前者用于解析响应体,后者用于解析请求体。

补充:Cache

Cache API 是用于缓存的 API,和Fetch API联系比较紧密,目前的浏览器支持也算是比较完全

image-20221203213013346

Cache 为 Request 和 Response 对象提供了存储机制,最常用的场景是在 ServiceWorker 中使用,虽然Cache 是 ServiceWorker 标准中的,但是 Cache 并不是限制在 ServiceWorker 中使用。

但是,Cache 只限制在 HTTPS 环境下使用。

在使用缓存之前需要使用 CacheStorage(使用 caches 访问) 的 open 方法打开一个命名空间,返回值是一个 Promise 对象,resolve的结果是 cache 缓存对象,同一个域名下可以有多个缓存对象。

caches 有以下方法:

  • match:【request, options】检查指定的 request 是否是 cache 对象的键,返回一个resolve 为该匹配的Promise对象;
  • has:【cacheName】如果存在 cacheName 的缓存对象则返回一个 Promise对象,resolve 的值为 true,否则为 false;
  • open:【cacheName】返回与 cacheName 匹配的 cache 对象(Promise),如果不存在则创建一个缓存对象并返回;
  • delete:【cacheName】删除与指定cacheName 匹配的 cache 对象,删除成功返回一个 Promise对象,resolve 值为 true,否则为 false;
  • keys:返回caches 所有命名组成的数组。

cache 实例上有以下方法:

  • match:【request, options】返回一个 Promise对象,resolve 的结果是跟 Cache 对象匹配的第一个缓存请求;
  • matchAll:【request, options】返回一个 Promise对象,resolve 的结果是跟 Cache 对象匹配的缓存请求数组;
  • add:【request】抓取一个 url,然后将相应进行缓存
  • addAll:【requests】抓取一个url 数组,然后将返回的 response 缓存
  • put:【request, response】同时抓取一个 request 和 response 进行缓存
  • delete:【request, options】删除 key 为 request 的缓存,返回结果是一个 Promise 对象,删除成功 resolve 的值为 true,如果没有找到 resolve 的值为 false;
  • keys:【request, options】返回一个 Promise对象,resolve 的值为 Cache key组成的数组,如果指定了 request则返回对应的Request

具体的使用案例之前在 ServiceWorker 中已经写过了。


前端小白