5

fetch() 很好用,但还能更好用

fetch() API使你可以在 Web 应用中执行网络请求。

fetch() 的用法非常简单:通过调用 fetch('/movies.json') 启动请求,请求完成后得到一个 Response 对象,然后从中提取数据。

这是一个简单的例子,说明如何从 /movies.json URL获取 JSON 格式的电影数据:

async function executeRequest() {
  const response = await fetch('/movies.json');
  const moviesJson = await response.json();
  console.log(moviesJson);
}

executeRequest(); 
// logs [{ name: 'Heat' }, { name: 'Alien' }]

如上面的代码所示,你必须手动从响应中提取 JSON 对象:moviesJson = await response.json()。这样做一两次没什么问题。但是如果你的程序需要执行许多请求,那么反复用 await response.json() 提取 JSON 对象就显得有些笨拙了。

这时就想到了使用第三方库,例如 axios,这个库大大简化了请求的处理。下面的代码用 axios 封装相同的功能:

async function executeRequest() {
  const moviesJson = await axios('/movies.json');
  console.log(moviesJson);
}

executeRequest(); 
// logs [{ name: 'Heat' }, { name: 'Alien' }]

moviesJson = await axios('/movies.json') 能够返回实际的 JSON 响应,不必像 fetch() 那样手动提取JSON。

但是如果使用 axios 之类的库会带来一系列问题。

首先,它增加了 Web 程序的包大小;其次,你的程序与第三方库结合在了一起,不管是好处还是bug。

所以就想到了第三种方法——用装饰器模式来提高 fetch() API 的易用性和灵活性。

准备Fetcher 接口

装饰器模式非常强大,它能够以灵活和松散耦合的方式在基本逻辑之上添加功能(也就是所谓的“装饰”)。

使用装饰器来增强 fetch() 需要几个简单的步骤。

第一步是声明一个名为 Fetcher 的抽象接口:

type ResponseWithData = Response & { data?: any };

interface Fetcher {
  run(
    input: RequestInfo, 
    init?: RequestInit
  ): Promise<ResponseWithData>;
} 

Fetcher 接口只有一个方法,该方法接受与普通 fetch() 相同的参数并返回相同类型的数据。

第二步是实现基本的访存器类:

class BasicFetcher implements Fetcher {
  run(
    input: RequestInfo, 
    init?: RequestInit
  ): Promise<ResponseWithData> {
    return fetch(input, init);
  }
}

BasicFetcher 实现 Fetcher 接口。它的run() 方法调用 fetch() 函数。就这么简单。

下面是用基本的 fetcher 类来获取数据的代码:

const fetcher = new BasicFetcher();const decoratedFetch = fetcher.run.bind(fetcher);

async function executeRequest() {
  const response = await decoratedFetch('/movies.json');  const moviesJson = await response.json();
  console.log(moviesJson);
}

executeRequest(); 
// logs [{ name: 'Heat' }, { name: 'Alien' }]

const fetcher = new BasicFetcher() 创建 fetcher 类的实例。 decoratedFetch = fetcher.run.bind(fetcher) 创建了一个绑定方法。

然后,就可以用 decoratedFetch('/movies.json') 来获取 JSON 数据了,就像使用普通的 fetch() 一样。

在这个步骤中, BasicFetcher 类不会带来任何好处。而且由于引入了新的接口和类,使代码变得更加复杂了。

JSON 提取装饰器

装饰器模式的核心是装饰器类。

装饰器类必须符合 Fetcher 接口,包装装饰后的实例,并在 run() 方法中引入其他功能。

下面实现一个装饰器,它从 response 对象中提取 JSON 数据:

class JsonFetcherDecorator implements Fetcher {
  private decoratee: Fetcher;

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

  async run(
    input: RequestInfo, 
    init?: RequestInit
  ): Promise<ResponseWithData> {
    const response = await this.decoratee.run(input, init);
    const json = await response.json();
    response.data = json;
    return response;
  }
}

接下来看看 JsonFetcherDecorator 是怎样构造的。

JsonFetcherDecorator 符合 Fetcher 接口。JsonExtractorFetch 中有私有字段 Decoratee,它也符合 Fetcher 接口。在 run() 方法中,this.decoratee.run(input,ini) 获取数据。

然后 json = await response.json() 从响应中提取 JSON 数据。最后,response.data = json 将提取的 JSON 数据分配给响应对象。

下面用 JsonFetcherDecorator 装饰器来装饰 BasicFetcher,并简化 fetch() 的使用:

const fetcher = new JsonFetcherDecorator(  new BasicFetcher());const decoratedFetch = fetcher.run.bind(fetcher);

async function executeRequest() {
  const { data } = await decoratedFetch('/movies.json');  console.log(data);
}

executeRequest(); 
// logs [{ name: 'Heat' }, { name: 'Alien' }]

现在就可以直接从 response 对象的 data 属性访问提取的数据,在每个使用 const { data } = decoratedFetch(URL) 的地方,都不必手动提取 JSON 对象了。

请求超时装饰器

默认情况下,fetch() API 的超时时间受到浏览器的制约。在 Chrome 中,网络请求超时为 300 秒,而在 Firefox 中为 90 秒。

假设我们最多可以等待 8 秒才能完成简单的请求,这时需要设置网络请求超时时间,并在 8。秒后通知用户有关网络问题的信息。

装饰器模式的优点在于:可以根据需要使用任意数量的装饰器来装饰基本实现。接下来再为 fetch 请求创建一个超时装饰器:

const TIMEOUT = 8000; // 8 秒超时

class TimeoutFetcherDecorator implements Fetcher {
  private decoratee: Fetcher;

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

  async run(
    input: RequestInfo, 
    init?: RequestInit
  ): Promise<ResponseWithData> {
    const controller = new AbortController();
    const id = setTimeout(() => controller.abort(), TIMEOUT);
    const response = await this.decoratee.run(input, {
      ...init,
      signal: controller.signal
    });
    clearTimeout(id);
    return response;
  }
}

TimeoutFetcherDecorator 是实现 Fetcher 接口的装饰器。

如果请求没有在 8 秒内完成,那么在 TimeoutFetcherDecorator.run() 方法中的 controller.abort() 会中止请求。

测试一下:

const fetcher = new TimeoutFetcherDecorator(
  new JsonFetcherDecorator(
    new BasicFetcher()
  )
);
const decoratedFetch = fetcher.run.bind(fetcher);

async function executeRequest() {
  try {
    const { data } = await decoratedFetch('/movies.json');
    console.log(data);
  } catch (error) {
    //如果请求超过8秒
    console.log(error.name);
  }
}

executeRequest(); 
//如果请求耗时超过8秒则输出 “AbortError”

由于使用了 TimeoutFetcherDecoratordecoratedFetch('/movies.json') 会引发超时错误。

现在 fetch() 被 2 个装饰器包装:一个装饰器用于提取 JSON 对象,另一个在 8 秒内使请求超时。这就极大地简化了 DecoratedFetch() 的使用。

总结

fetch()API提供了请求的基本功能。但是只用 fetch() 会强制你从请求中手动提取 JSON 数据,自己去配置超时等等。

尽管我们可以使用第三方库,但是使用 axios 之类的库会增加程序包的大小,并增加耦合度。

通过使用装饰器模式,可以使装饰器从请求中提取 JSON,使请求超时等等。另外还可以随时合并、添加或删除装饰器,而不会影响使用了带装饰器的 fetch() 的其他代码。

173382ede7319973.gif


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章:



疯狂的技术宅
44.4k 声望39.2k 粉丝