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`
}
现在,Github API 会返回分页的提交(就像大多数 REST API 一样),所以我们将“批量”获取提交。我们希望以某种方式实现这种“分页”逻辑fetchCommits
。
然而,我们不想将所有提交一起返回fetchCommits
,我们希望在每个页面出现时执行一些逻辑,并在“执行操作”部分实现这些逻辑。
无需异步迭代的解决方案
为了做到这一点,我们被迫使用回调:
// Here we "do stuff"
fetchCommits('facebook/react', commits => {
// do something with `commits`
}
我们可以使用 Promises 吗?嗯,不行,因为我们只能得到一页,或者整个页面:
function doStuff() {
fetchCommits('facebook/react').then(commits => {
// do something
})
}
我们可以使用同步Promise
生成器吗?嗯……我们可以在生成器中返回一个,然后在生成器外部解析该承诺。
// fetchCommits is a generator
for (let commitsPromise of fetchCommits('facebook/react')) {
const commits = await commitsPromise
// do something
}
这实际上是一个干净的解决方案,但是生成器的实现是怎样的呢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++
}
}
这个解决方案还不错,但有一个大问题: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++
}
}
}
这不是一个好的解决方案,因为我们实际上是对“下一个”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...
}
}
理想情况下,我们希望在每次请求后获取“下一个” 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
}
}
调用函数的逻辑也变得非常简单:
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()
您还有其他关于如何使用异步生成器的示例吗?
鏂囩珷鏉ユ簮锛�https://dev.to/exacs/es2018-real-life-simple-usage-of-async-iteration-get-pagulated-data-from-rest-apis-3i2e