如何使用Promise.all获取URL数组?

89
如果我有一个URL数组:
var urls = ['1.txt', '2.txt', '3.txt']; // these text files contain "one", "two", "three", respectively.

我想要构建一个类似这样的对象:

var text = ['one', 'two', 'three'];

我一直在尝试使用fetch来学习这个,当然它返回的是Promise

以下是我尝试过但无法实现的一些方法:

var promises = urls.map(url => fetch(url));
var texts = [];
Promise.all(promises)
  .then(results => {
     results.forEach(result => result.text()).then(t => texts.push(t))
  })

这看起来不对,而且无论如何它都不能工作-我最终没有得到一个数组['one','two','three']。

在这里使用Promise.all是正确的方法吗?


这看起来像是括号错误。你真的想在.forEach(…)的返回值上调用.then吗?还是应该在….text()上调用? - Bergi
你在哪里查看/记录 texts 并观察到它仍为空的? - Bergi
8个回答

164

是的,Promise.all是正确的方法,但如果您想首先fetch所有网址,然后从它们中获取所有text(这些再次是响应主体的promise),则实际上需要两次使用它。所以您需要执行:

Promise.all(urls.map(u=>fetch(u))).then(responses =>
    Promise.all(responses.map(res => res.text()))
).then(texts => {
    …
})

你当前的代码无法工作是因为forEach没有返回任何内容(既不是数组也不是Promise)。

当然,你可以简化代码并在相应的fetch promise完成后直接获取每个响应的主体:

Promise.all(urls.map(url =>
    fetch(url).then(resp => resp.text())
)).then(texts => {})

或者使用await来实现相同的功能:

const texts = await Promise.all(urls.map(async url => {
  const resp = await fetch(url);
  return resp.text();
}));

3
针对问题中的感受,因为 JavaScript 中异步操作的工作原理,你无法将结果“提取”到外部变量中,但你可以使用生成器或 async/await 来模拟。请参考 这个答案 获取有关 JS 异步性的完整指南。 - Benjamin Gruenbaum
这看起来很棒!但我无法理解 :( JavaScript 是一种奇怪的语言。 - yota
@sansSpoon 你确定你使用了我在答案中提供的简洁箭头函数体吗?它确实会隐式返回 Promise。 - Bergi
哦,当然,手掌>脸。我总是混淆我的es5/6。谢谢。 - sansSpoon
为什么需要第二个 Promise.allMDN指出:"Promise.all() 方法返回一个 Promise,该 Promise 在可迭代的所有 Promise 都已解决或可迭代不包含 Promise 时解决。" 这让我认为应该返回单个 Promise,而不是需要第二个 Promise.all 的 Promise 数组。但如果没有第二个 Promise.all,它将无法正常工作。 - 1252748
1
@1252748 是的,Promise.all() 为响应对象数组返回单个 Promise。但是,responses.map(res => res.text()) 会生成另一个 Promise 数组,需要第二个 Promise.all - Bergi

32

由于某些原因,Bergi的两个示例都无法正常工作。它只会给我空结果。经过一些调试,似乎Promise会在fetch完成之前返回,因此产生了空结果。

然而,Benjamin Gruenbaum先前在这里给出了答案,但已删除。他的方法对我有效,因此我将在此粘贴它,作为第一个解决方案出现问题时的替代方法。

var promises = urls.map(url => fetch(url).then(y => y.text()));
Promise.all(promises).then(results => {
    // do something with results.
});

谢谢,我查看了几个与在承诺的任务实际完成后执行某些操作有关的SO答案。这个答案实际上等待所有文件完成获取后再触发我的回调函数。 - Reahreic
你(/ Benjamin Gruenbaum)的回答和Bergi的第二个片段之间实际上没有区别。 - 假设我将你的第二个 promises 替换为 urls.map(url => fetch(url).then(y => y.text())) ,然后用 resp 替换你的 y ,最后用 texts 替换你的 results 。 与Bergi的第二个片段唯一的区别是// do something with results的注释以及行末分号的位置(以及最后的换行符)。 (这可能解释了为什么Benjamin Gruenbaum删除了他的回答?) - Henke

15

应该使用 map 而不是 forEach

Promise.all(urls.map(url => fetch(url)))
.then(resp => Promise.all( resp.map(r => r.text()) ))
.then(result => {
    // ...
});

2
为什么要使用map而不是forEach?只是问一下吗? - Christian Matthew
1
@ChristianMatthew map 返回每个 fetch(url) 的结果,而 forEach 不返回任何内容。请参阅已接受的答案或 https://dev59.com/hlsW5IYBdhLWcg3w8q5S#34426481。 - Holden Lewis
谢谢@HoldenLewis,这就是我想到的答案。 - Christian Matthew

6

这是一个清晰的方法来实现它。

const requests = urls.map((url) => fetch(url)); 
const responses = await Promise.all(requests); 
const promises = responses.map((response) => response.text());
return await Promise.all(promises);

4
建议使用的数组 urls = ['1.txt', '2.txt', '3.txt'] 对我来说没有太多意义,因此我将使用以下内容代替:
urls = ['https://jsonplaceholder.typicode.com/todos/2',
        'https://jsonplaceholder.typicode.com/todos/3']

两个URL的JSON:
{"userId":1,"id":2,"title":"quis ut nam facilis et officia qui",
 "completed":false}
{"userId":1,"id":3,"title":"fugiat veniam minus","completed":false}

目标是获得一个对象数组,其中每个对象包含对应URL的title值。
为了让它更有趣,我假设已经有一个names数组,我希望将URL结果数组(titles)与之合并。
namesonly = ['two', 'three']

期望的输出是一个对象数组:
[{"name":"two","loremipsum":"quis ut nam facilis et officia qui"},
{"name":"three","loremipsum":"fugiat veniam minus"}]

我将属性名称title更改为loremipsum

const namesonly = ['two', 'three'];
const urls = ['https://jsonplaceholder.typicode.com/todos/2',
  'https://jsonplaceholder.typicode.com/todos/3'];

Promise.all(urls.map(url => fetch(url)
  .then(response => response.json())
  .then(responseBody => responseBody.title)))
  .then(titles => {
    const names = namesonly.map(value => ({ name: value }));
    console.log('names: ' + JSON.stringify(names));
    const fakeLatins = titles.map(value => ({ loremipsum: value }));
    console.log('fakeLatins:\n' + JSON.stringify(fakeLatins));
    const result =
      names.map((item, i) => Object.assign({}, item, fakeLatins[i]));
    console.log('result:\n' + JSON.stringify(result));
  })
  .catch(err => {
    console.error('Failed to fetch one or more of these URLs:');
    console.log(urls);
    console.error(err);
  });
.as-console-wrapper { max-height: 100% !important; top: 0; }

参考文献


2
以下也适用于我。
    Promise.all([
      fetch(QUESTIONS_API_BASE_URL).then(res => res.json()),
      fetch(SUBMISSIONS_API_BASE_URL).then(res => res.json())
    ])
    .then(console.log)

2

如果您正在使用axios,则可以按照以下方式实现:

const apiCall = (endpoint:string)=> axios.get(${baseUrl}/${endpoint})

axios.all([apiCall('https://first-endpoint'),apiCall('https://second-endpoint')]).then(response => {
            response.forEach(values => values)
            }).catch(error => {})  

1
你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community

0
// Here, you have an array called todos containing three URLs.

const todos = [
  "https://jsonplaceholder.typicode.com/todos/1",
  "https://jsonplaceholder.typicode.com/todos/2",
  "https://jsonplaceholder.typicode.com/todos/3",
];

// asunchronous function to fetch todo of the respective url

const fetchTodo = async (url) => {
  const res = await fetch(url);
  if (!res.ok) {
    throw new Error(`HTTP error! status: ${res.status}`);
  }
  return res.json();
};


// The todos.map((item) => fetchTodo(item)) part creates an array of promises by mapping each URL to the result of the fetchTodo function. Promise.all then waits for all these promises to settle (either resolve or reject).

Promise.all(todos.map((item) => fetchTodo(item)))
  .then((res) => {
    console.log(res);
  })
  .catch((err) => console.error(err));

// fetches JSON data from multiple URLs concurrently using Promise.all,

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接