10
头图

1. 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的易用性和灵活性。

在下一节中,我们将介绍如何做到这一点。

2. 准备Fetcher接口

装饰器模式很有用,因为它允许以灵活和松散耦合的方式在基本逻辑之上添加功能(换句话说,装饰)。

如果你对装饰器模式不熟悉,建议阅读一下它的工作原理。

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

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

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

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

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

第二步是实现基本的fetcher类:

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' }]

Try the demo

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

然后你可以使用 decoratedFetch('/movies.json’) 来获取电影JSON,就像使用常规的 fetch() 一样。

在这一步,BasicFetcher 类并没有带来好处,而且,因为有了新的接口和新的类,事情变得更加复杂。稍等...将装饰器引入动作后,你将看到巨大的好处。

3. JSON提取装饰器

装饰器模式的主力军是装饰器类。

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

让我们实现一个装饰器,该装饰器从响应对象中提取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, init) 可以实际获取数据。

然后,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' }]

Try the demo

现在,不用再从响应中手动提取JSON数据,你可以从响应对象的 data 属性中访问提取的数据。

通过将JSON提取器移至装饰器,现在在每个使用 const { data } = decoratedFetch(URL) 的地方,你都不必手动提取JSON对象。

4. 请求超时装饰器

fetch() API默认在浏览器指定的时间超时,在Chrome浏览器中,网络请求的超时时间为300秒,而在Firefox浏览器中则为90秒。

用户最多可以等待8秒的时间来完成简单的请求,所以需要对网络请求设置超时,8秒后告知用户网络问题。

装饰器模式的好处是,你可以用任意多的装饰器来装饰你的基本实现。所以,让我们为fetch请求创建一个超时装饰器。

const TIMEOUT = 8000; // 8 seconds

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 接口的装饰器。

TimeoutFetcherDecoratorrun() 方法里面:如果在8秒内还没有完成请求,就用AbortController来中止请求。

现在,让这个装饰器开始工作:

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"

Try the demo

在demo中,对 /movies.json 的请求耗时超过8秒。

由于 TimeoutFetcherDecorator 的缘故,decoratedFetch('/ movies.json’) 引发超时错误。

现在,基本的fetcher被包裹在2个装饰器中:一个提取JSON对象,另一个在8秒内超时结束请求,这极大地简化了 decoratedFetch() 的使用。

5. 总结

fetch() API提供了执行获取请求的基本功能,但你需要的不止这些,使用 fetch() 将强制你手动从请求中提取JSON数据、配置超时等等。

为了避免模板,你可以使用一个更友好的库,比如axios。然而,使用像axios这样的第三方库会增加应用程序的捆绑大小,以及你与它的紧密耦合。

另一种解决方案是在 fetch() 之上应用装饰器模式。你可以制作装饰器,从请求中提取JSON,超时请求等等。你可以随时组合、添加或删除装饰器,而不影响使用装饰的fetch的代码。

你想把 fetch() 和最常见的装饰器一起使用吗?我为你创建了要点!可以在你的应用程序中随意使用它,甚至可以根据自己的需要添加更多装饰器!

还有什么其他的 fetch() 装饰器可能有用?请在下面的评论中写下你的实现 (请使用TypeScript)


杭州程序员张张
11.8k 声望6.7k 粉丝

Web/Flutter/独立开发者/铲屎官