ES2018. 异步迭代的简单实际使用:用 20 行代码从 REST API 获取分页数据

2025-06-08

ES2018. 异步迭代的简单实际使用:用 20 行代码从 REST API 获取分页数据

下一个 JavaScript 标准 ES2018 已经发布,它带来了一个重大的新特性:异步迭代。这是一个非常有用的特性,我想与大家分享一个非常简单的例子,来说明如何在实际应用中使用它。

这篇文章我不会解释什么是异步迭代器或迭代器。你可以在这里这里找到相关的解释。

问题是:我们想从一个分页返回的 API 中获取数据,并对每个页面进行操作。例如,我们想获取某个 Github 仓库的所有提交,并对这些数据进行一些操作。

我们希望将“获取提交”和“执行操作”的逻辑分开,因此我们将使用两个函数。在实际场景中,这两个函数fetchCommits可能位于不同的模块中,“执行操作”部分会以fetchCommits某种方式调用:

// Imagine that this function is in a different module...
function fetchCommits(repo) {}


function doStuff() {
  const commits = fetchCommits('facebook/react')
  // do something with `commits`
}
Enter fullscreen mode Exit fullscreen mode

现在,Github API 会返回分页的提交(就像大多数 REST API 一样),所以我们将“批量”获取提交。我们希望以某种方式实现这种“分页”逻辑fetchCommits

然而,我们不想将所有提交一起返回fetchCommits,我们希望在每个页面出现时执行一些逻辑,并在“执行操作”部分实现这些逻辑。

无需异步迭代的解决方案

为了做到这一点,我们被迫使用回调:

// Here we "do stuff"
fetchCommits('facebook/react', commits => {
  // do something with `commits`
}
Enter fullscreen mode Exit fullscreen mode

我们可以使用 Promises 吗?嗯,不行,因为我们只能得到一页,或者整个页面:

function doStuff() {
  fetchCommits('facebook/react').then(commits => {
    // do something
  })
}
Enter fullscreen mode Exit fullscreen mode

我们可以使用同步Promise生成器吗?嗯……我们可以在生成器中返回一个,然后在生成器外部解析该承诺。

// fetchCommits is a generator
for (let commitsPromise of fetchCommits('facebook/react')) {
  const commits = await commitsPromise
  // do something
}
Enter fullscreen mode Exit fullscreen mode

这实际上是一个干净的解决方案,但是生成器的实现是怎样的呢fetchCommits

function* fetchCommits(repo) {
  const lastPage = 30 // Must be a known value
  const url = `https://api.github.com/${repo}/commits?per_page=10`

  let currentPage = 1
  while (currentPage <= lastPage) {
    // `fetch` returns a Promise. The generator is just yielding that one.
    yield fetch(url + '&page=' + currentPage)
    currentPage++
  }
}
Enter fullscreen mode Exit fullscreen mode

这个解决方案还不错,但有一个大问题:lastPage必须提前知道值。这通常是不可能的,因为这个值在我们第一次请求时就已经包含在 headers 中了。

如果我们仍然想使用生成器,那么我们可以使用异步函数来获取该值并返回同步生成器......

async function fetchCommits (repo) {
  const url = `https://api.github.com/${repo}/commits?per_page=10`
  const response = await fetch(url)

  // Here we are calculating the last page...
  const last = parseLinkHeader(response.headers.link).last.url
  const lastPage = parseInt(
    last.split('?')[1].split('&').filter(q => q.indexOf('page') === 0)[0].split('=')[1]
  )

  // And this is the actual generator
  return function* () {
    let currentPage = 1
    while (currentPage <= lastPage) {
      // And this looks non dangerous but we are hard coding URLs!!
      yield fetch(url + '&page=' + currentPage)
      currentPage++
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

这不是一个好的解决方案,因为我们实际上是对“下一个”URL 进行硬编码。

另外,它的用法可能会有点令人困惑......

async function doStuff() {
  // Calling a function to get...
  const getIterator = await fetchCommits('facebook/react')

  // ... a function that returns an iterator???
  for (const commitsPromise of getIterator()) {
    const value = await commitsPromise
    // Do stuff...
  }
}
Enter fullscreen mode Exit fullscreen mode

理想情况下,我们希望在每次请求后获取“下一个” URL,这涉及将异步逻辑放入生成器中,但在产生的值之外

异步生成器(async function*)和for await循环

现在,异步生成器和异步迭代允许我们遍历结构,其中除yield 值之外的所有逻辑也都是异步计算的。这意味着,对于每个 API 调用,我们都可以根据标头猜测“下一个 URL”,并检查是否到达末尾。

事实上,这可能是一个真正的实现:

(该示例在节点 >= 10 时有效)

const rp = require('request-promise')
const parseLinkHeader = require('parse-link-header')

async function* fetchCommits (repo) {
  let url = `https://api.github.com/${repo}/commits?per_page=10`

  while (url) {
    const response = await request(url, {
      headers: {'User-Agent': 'example.com'},
      json: true,
      resolveWithFullResponse: true
    })

    // We obtain the "next" url looking at the "link" header
    // And we need an async generator because the header is part of the response.
    const linkHeader = parseLinkHeader(response.headers.link)

    // if the "link header" is not present or doesn't have the "next" value,
    // "url" will be undefined and the loop will finish
    url = linkHeader && linkHeader.next && linkHeader.next.url
    yield response.body
  }
}
Enter fullscreen mode Exit fullscreen mode

调用函数的逻辑也变得非常简单:

async function start () {
  let total = 0
  const iterator = fetchCommits('facebook/react')

  // Here is the "for-await-of"
  for await (const commits of iterator) {
    // Do stuff with "commits" like printing the "total"
    total += commits.length
    console.log(total)

    // Or maybe throwing errors
    if (total > 100) {
      throw new Error('Manual Stop!')
    }
  }
  console.log('End')
}
start()
Enter fullscreen mode Exit fullscreen mode

您还有其他关于如何使用异步生成器的示例吗?

鏂囩珷鏉ユ簮锛�https://dev.to/exacs/es2018-real-life-simple-usage-of-async-iteration-get-pagulated-data-from-rest-apis-3i2e
PREV
AWS 静态网站托管初学者指南(第 1 部分)
NEXT
如何在 Windows 上安装 Ollama