使用装饰器模式强大你的 fetch

1,035 阅读6分钟

搜索框传播样式-白色版.png

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应用程序的bundle大小。其次,您的应用程序与第三方库相结合:您获得了该库的所有好处,但也得到了它的所有bug

我的目的是采用一种不同的方法,从这两个方面都得到了最好的结果——使用装饰器模式来增加fetch() API的易用性和灵活性

其思想是将一个基fetch类(我将展示如何定义它)包装为您需要的任何其他功能:提取JSON、超时、在糟糕的HTTP状态下抛出错误、处理auth头,等等。让我们在下一节中看看如何做到这一点。

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

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数据。最后,响应。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' }]

现在,您可以从响应对象的data属性访问所提取的数据,而不是从响应中手动提取JSON数据。

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

4. 创建请求超时装饰器

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

用户可以等待8秒来完成简单的请求。这就是为什么需要为网络请求设置一个超时,并在8秒后通知用户网络问题的原因。

装饰器模式的伟大之处在于,可以使用任意多的装饰器装饰你的基本实现!那么,让我们为取回请求创建一个超时装饰器:

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接口的decorator

TimeoutFetcherDecoratorrun()方法内部:如果请求在8秒内没有完成,则使用中止控制器中止请求。

现在让我们来使用这个装饰器:

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) {
    // Timeouts if the request takes
    // longer than 8 seconds
    console.log(error.name);
  }
}

executeRequest(); 
// if the request takes more than 8 seconds
// logs "AbortError"

在这个示例中,对/movies.json的请求需要超过8秒。

decoratedFetch('/movies.json'),由于TimeoutFetcherDecorator,抛出超时错误。

现在基本的获取器被封装在2个装饰器中:一个提取JSON对象,另一个在8秒内超时请求。这极大地简化了decoratedFetch()的使用:当调用decoratedFetch()时,decorator逻辑将为你工作。

5. 总结

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

为了避免样板文件,你可以使用更友好的库,如axios。然而,使用像axios这样的第三方库会增加应用包的大小,同时你也会与之紧密结合。

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