JavaScript 中如何构建可等待和流畅的接口:Promise与async/await的高级应用

153 阅读3分钟

我编写的大多数 JavaScript 代码都严重依赖于 Promise 和 async/await。我遇到的一个问题是,我希望为我的库的用户提供一个流畅的接口,同时使用 async/await 来实现。

举个例子,考虑下面这个函数:

async function getArticles() {
  const response = await fetch('https://api.example.org');
  const json = await response.json();
  return json;
}

这个函数的用户可能会这样调用它:

const articles = await getArticles();

我想让用户能够为此调用添加自定义的 HTTP 头部。一种简单的方法是在getArticles()函数中添加一个headers参数作为选项。在大多数情况下,这可能是正确的选择。

但在我的情况下,HTTP 头部并不是唯一可能需要的可选参数,而且我不想让接口变得极其复杂。我真正想要的是一个流畅的接口,特别是能够以以下两种方式调用函数:

const articles = await getArticles();
const articles = await getArticles().withHeader('X-Foo', 'Bar');

我需要弄清楚如何从那个函数返回一个既像 Promise 又可以添加自定义函数的东西。事实证明,对象不需要是全局的Promise对象就可以与 Promise 或 async/await 一起使用。它们所需要的只是一个行为与 Promise 的then函数相同的then函数。

我最终得到了这个解决方案:

class HeaderPromise {

  constructor(uri) {
    this.uri = uri;
    this.headers = {};
  }

  withHeader(key, value) {
    this.headers[key] = value;
    return this;
  }

  then(onResolved, onRejected) {
    return this.getInnerPromise().then(onResolved, onRejected);
  }

  getInnerPromise() {
    if (!this.innerPromise) {
      this.innerPromise = (async () => {
        const response = await fetch(this.uri, { headers: this.headers });
        const json = await response.text();
        return json;
      })();
    }
    return this.innerPromise;
  }
}

function getArticles() {
  return new HeaderPromise('https://api.example/');
}

我所做的更改是getArticles不再是async函数,但它返回一个await会视为Promise的对象。

让我们来看看每个类函数:

  constructor(uri) {
    this.uri = uri;
    this.headers = {};
  }

我们需要将所有相关信息传递给构造函数。这意味着如果你想提供这样的 API,对于每个返回流畅 Promise 接口的 API 函数,可能都需要一个自定义类。

  withHeader(key, value) {
    this.headers[key] = value;
    return this;
  }

withHeader更新头部列表,为了确保流畅性,它总是返回自身。这允许这个函数被多次调用,并最终允许等待类似 Promise 的对象。

  then(onResolved, onRejected) {
    return this.getInnerPromise().then(onResolved, onRejected);
  }

then函数完成所有“Promise 相关的工作”。它实际上只是将其功能委托给一个真正的 Promise,而不是实现所有的 Promise 逻辑(这很难!)。

  getInnerPromise() {
    if (!this.innerPromise) {
      this.innerPromise = (async () => {
        const response = await fetch(this.uri, { headers: this.headers });
        const json = await response.text();
        return json;
      })();
    }
    return this.innerPromise;
  }

getInnerPromise是完成所有工作的函数。一个重要的细微差别是,如果它被多次调用,它只会执行一次工作。这很重要,因为如果你对一个 Promise 调用then两次,它不应该执行两次工作。在 Promise 完成工作后,它应该存储结果并在以后调用then时返回它。

总之,就是这样!这很复杂,我的一般建议是除非别无他法,否则应避免使用这种模式。使用这种模式会使你的实现不太易读,但潜在的好处是有一个更好的接口。接口比实现更重要,但要注意不要使维护变得太难。

最后,这是 TypeScript 的等效代码:

class HeaderPromise<T> implements PromiseLike<T> {

  uri: string;
  headers: { [key: string]: string };
  innerPromise: Promise<T>;

  constructor(uri: string) {
    this.uri = uri;
    this.headers = {};
  }

  withHeader(key: string, value: string): this {
    this.headers[key] = value;
    return this;
  }
  
  then<TResult1 = any, TResult2 = never>(onfulfilled?: ((value: any) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined): PromiseLike<TResult1 | TResult2> {
    return this.getInnerPromise().then(onfulfilled, onrejected);
  }

  getInnerPromise() {
    if (!this.innerPromise) {
      this.innerPromise = (async () => {
        const response = await fetch(this.uri, { headers: this.headers });
        const json = await response.json();
        return json;
      })();
    }
    return this.innerPromise;
  }
}

function getArticles(): HeaderPromise<any> {
  return new HeaderPromise('https://api.example/');
}