如何与 async/await 一起使用 Fetch

如何与 async/await 一起使用 Fetch

Fetch API 已经成为前端应用中获取资源的原生方式。

在这篇文章中,我将展示如何用 async/await 语法使用 Fetch API 的常见场景。目的是让你对如何获取数据、处理获取错误、取消获取请求等有信心。

开始之前,推荐熟悉 async/await 语法。在下面的例子中将广泛使用它。

fetch() 简介

Fetch API 通过网络访问资源。通过创建 HTTP 请求(GET,POST),下载和上传文件。

开始请求时,执行规范函数 fetch()

const response = await fetch(resource[, options]);

该函数接收两个参数:

  • resource:一个 URL 字符串,或者一个 Request 对象;
  • options:一个配置对象,对象包含 method'GET'''POST''),headersbodycredentials 等等属性,更多属性参数

执行 fetch() 启动一个请求并返回一个承诺。当请求完成后,用 Response 解决承诺。如果由于一些网络问题导致请求失败,则拒绝该承诺。

async/await 语法非常适合 fetch(),因为它简化了对承诺的工作。

例如,让我们做一个获取一些电影的请求:

async function fetchMovies() {
  const response = await fetch('/movies');
  // 等待直到请求完成
  console.log(response);
}

fetchMovies() 是一个异步函数,因为它用 async 关键字标记。

await fetch('/movies') 开始向 '/movies' URL 发出 HTTP 请求。由于存在 await 关键字,异步函数被暂停,直到请求完成。

当请求完成后,响应会被分配给请求的响应对象。让我们在下一节看看如何从响应中提取有用的数据,如 JSON 或纯文本。

请求 JSON 数据

fetch() 返回的 Response 对象是多种数据格式的通用占位符。下面是你如何从响应对象中以 JSON 数据的形式获取电影。

async function fetchMoviesJSON() {
  const response = await fetch('/movies');
  const movies = await response.json();  return movies;
}

fetchMoviesJSON().then((movies) => {
  movies; // fetched movies
});

response.json() 是 Response 对象上的一个方法,让你从响应中提取 JSON 对象。该方法返回一个 promise,所以你必须等待 JSON 结果返回:await response.json()

response 对象提供了很多有用的方法:

  • response.json() 返回一个解析为 JSON 对象的 promise;
  • response.text() 返回一个解析为文本内容的 promise;
  • response.formData 返回一个解析为 FormData 对象的 promise;
  • response.blob() 返回一个解析为 Blog 的 promise;
  • response.arrayBuffer() 返回一个解析为 ArrayBuffer 的 promise。

处理 fetch 错误

当我在了解 fetch() 的时候,我很惊讶,当服务器返回一个不好的 HTTP 状态时,比如 404502fetch() 不会抛出一个错误。

让我们尝试访问服务器上一个不存在的页面 '/oops'。正如预期的那样,这种请求以 404 响应状态结束:

async function fetchMovies404() {
  const response = await fetch('/oops');

  response.ok; // => false
  response.status; // => 404

  const text = await response.text();
  return text;
}

fetchMovies404().then((text) => {
  text; // => 'Page not found'
});

当获取 URL '/oops' 时,服务器的响应是状态 404 和文本 'Page not found'。令人惊讶的是,fetch() 并不会因为 URL 缺失而抛出错误,而是将其视为一个已完成的 HTTP 请求。

fetch() 只有在请求无法发出或响应无法获取时才会拒绝。这可能是因为网络问题:没有网络连接,找不到主机,服务器没有响应。

幸运的是,response.ok 属性可以让你区分 HTTP 响应状态的好坏。只有当响应状态在 200299 之间时,该属性才会被设置为 true

在上面的例子中,response.ok 属性为 false,因为响应状态为 404

如果你想对一个坏的 HTTP 状态(在 200-299 范围之外)抛出一个错误,检查 response.ok 属性的值并手动抛出一个错误:

async function fetchMoviesBadStatus() {
  const response = await fetch('/oops');

  if (!response.ok) {    const message = `An error has occured: ${response.status}`;    throw new Error(message);  }
  const movies = await response.json();
  return movies;
}

fetchMoviesBadStatus().catch((error) => {
  error.message; // 'An error has occurred: 404'
});

取消 fetch 请求

为了取消一个 丰田车请求,你需要添加 AbortController 工具。使用 AbortController 需要 3 步:

// Step 1: instantiate the abort controller
const controller = new AbortController();

// Step 2: make the fetch() aware of controller.signal
fetch(..., { signal: controller.signal });

// Step 3: call to cancel the request
controller.abort();

在下面的例子中,但点击按钮 “Cancel” 时,一个 fetch() 请求被取消:

async function fetchMoviesWithCancel(controller) {
  const response = await fetch('/movies', {
    signal: controller.signal,
  });
  const movies = await response.json();
  return movies;
}

const controller = new AbortController();

cancelButton.addEventListener('click', () => {
  controller.abort();
});

fetchMoviesWithCancel(controller).catch((error) => {
  error.name; // => 'AbortError'
});

const controller = new AbortController() 创建一个 AbortController 的实例。然后 controller.signal 属性最为 fetch 请求的参数:fetch(..., { signal: controller.signal })

controller.abort() 在按钮的点击事件函数中被执行时,取消请求。

当 fetch 请求被取消,fetch() 函数失败并返回拒绝错误(AbortError)的 promise。

并发 fetch 请求

执行 Promise.all() 函数发起并发 fetch 请求。让我们并发请求电影和分类的两个接口:

async function fetchMoviesAndCategories() {
  const [moviesResponse, categoriesResponse] = await Promise.all([    fetch('/movies'),    fetch('/categories'),  ]);
  const movies = await moviesResponse.json();
  const categories = await categoriesResponse.json();

  return {
    movies,
    categories,
  };
}

fetchMoviesAndCategories().then(({ movies, categories }) => {
  movies; // fetched movies
  categories; // fetched categories
});

await Promise.all([...]) 并发开始 fetch 请求,并等待直到所有的请求完成。

拦截 fetch 请求

有时候,你可能需要在发送请求之前,或者在收到响应之后进行工作:这被命名为拦截。

拦截的一个例子是处理 fetch 错误:如果响应状态不在 200299 的范围内,抛出一个错误。

fetch() API 并没有提供任何拦截请求的功能。这是可以理解的,因为 fetch() API 设计得很简单。

decorator 模式 是设计拦截 fetch() 请求的一个很好的解决方案。让我们试着使用它。

首先,定义一个有 doFetch() 方法的类 Fetcher()(它只是简单地调用 fetch() 函数):

class Fetcher {
  doFetch(resource, options) {
    return fetch(resource, options);
  }
}

然后让我们创建类 Fetcher 的实例,并使用它请求电影列表:

const fetcher = new Fetcher();

async function fetchMoviesBadStatus() {
  const response = await fetcher.doFetch('/movies');

  if (!response.ok) {
    const message = `An error has occured: ${response.status}`;
    throw new Error(message);
  }

  const movies = await response.json();
  return movies;
}

fetchMoviesBadStatus()
  .then((movies) => {
    // When fetch succeeds
    movies;
  })
  .catch((error) => {
    // When fetch ends with a bad http status
    error.message;
  });

不直接调用 Fetch API,而是实例化 fetcherconst fetcher = new Fetcher(),然后调用 await fetcher.doFetch('/movies') 开始请求。

if 语句 if (!response.ok) { ......} 里面的逻辑是:如果响应状态在 200299 范围之内,就会抛出一个错误。这个逻辑应该重构成一个拦截器,因为它对响应进行修改。让我们把这个逻辑移到一个装饰器中,FetchDecoratorBadStatus

class FetchDecoratorBadStatus {
  decoratee;

  constructor(decoratee) {
    this.decoratee = decoratee;
  }

  async doFetch(resource, options) {
    const response = await this.decoratee.doFetch(resource, options);

    if (!response.ok) {
      const message = `An error has occured: ${response.status}`;
      throw new Error(message);
    }

    return response;
  }
}

FetchDecoratorBadStatus 封装了一个 Fetcher 实例(decoratee 属性)。decorateedoFetch() 方法调用 this.decoratee.doFetch(resource, options),之后做出 if(!response.ok) { .... } 状态检查(拦截)。

由于将错误拦截逻辑移到 FetchDecoratorBadStatusfetchMoviesBadStatus() 可以被简化:

const fetcher = new FetchDecoratorBadStatus(new Fetcher());

async function fetchMoviesBadStatus() {
  const response = await fetcher.doFetch('/movies');
  const movies = await response.json();
  return movies;
}

fetchMoviesBadStatus()
  .then((movies) => {
    // When fetch succeeds
    // 当请求成功
    movies;
  })
  .catch((error) => {
    // When fetch ends with a bad HTTP status, e.g. 404
    // 当请求结果是不好的的 HTTP 状态,例如 404
    error.message;
  });

new FetchDecoratorBadStatus(new Fetcher()) 就是你装饰常规 Fetcher 实例的方式。因为装饰器并没有改变被装饰的 Fetcher 的接口,所以你可以像以前一样取电影列表:await fetcher.doFetch('/movies')

装饰器模式的设计好处是使 FetcherFetchDecoratorBadStatus 松散耦合。你可以轻松地添加或删除装饰器,而不影响使用 fetcher 实例的客户端代码:await fetcher.doFetch('/movies')

你可以用任意多的装饰符来包裹 fetcher

const fetcher = new FetchDecorator1(
  new FetchDecorator2(new FetchDecoratorBadStatus(new Fetcher())),
);

总结

调用 fetch() 启动一个请求并返回一个 promise。当请求完成后,promise 会解析到响应对象。从响应对象中,你可以提取你需要的格式的数据:JSON, text, Blob.

因为 fetch() 返回的是一个 promise,所以你可以通过使用 async/await 语法来简化代码。

你已经知道了如何使用 fetch() 伴随着 async/await 来获取 JSON 数据,处理获取错误,取消请求,执行并发请求,以及如何使用装饰符拦截请求。

本文作者 Dmitri Pavlutin,转载请注明来源链接:

原文链接:https://dmitripavlutin.com/javascript-fetch-async-await/

本文链接:https://tie.pub/2020/10/fetch-with-async-await/