Express.js 中等待多个 Promise 完成再响应的策略

Express.js 中等待多个 Promise 完成再响应的策略

本文探讨了在 Express.js API 中,如何有效管理并等待多个异步操作(Promise)全部完成后再向客户端发送响应。通过分析常见的实现误区,如 async 关键字的遗漏或 await 的不当使用,文章详细演示了如何结合 async/await 语法和 Promise.all 方法,以及利用 fs.promises 模块来构建健壮、可读性强的异步处理逻辑,确保所有任务并行执行并统一等待结果,从而避免过早响应导致的数据不完整问题。

理解 Express.js 中的异步操作与 Promise

node.js 和 express.js 开发中,处理异步操作是核心技能。当一个 api 请求需要执行多个独立的、耗时的任务(例如,并发请求外部服务、读写文件等)时,我们通常会使用 promise 来管理这些操作。promise.all 是一个非常有用的工具,它允许我们并行地执行一组 promise,并等待它们全部成功完成。如果所有 promise 都成功,promise.all 返回一个包含所有 promise 结果的数组;如果其中任何一个 promise 失败,promise.all 会立即拒绝,并返回第一个拒绝的 promise 的错误。

结合 ES2017 引入的 async/await 语法,我们可以用同步代码的风格来编写异步逻辑,使得代码更易读、更易维护。一个 async 函数总是返回一个 Promise,而 await 关键字只能在 async 函数内部使用,它会暂停 async 函数的执行,直到其后的 Promise 解决(resolved)或拒绝(rejected)。

常见问题与实现误区

在处理多个并发异步任务时,开发者常遇到的一个问题是,尽管使用了 Promise.all,API 却似乎没有等待所有任务完成就发送了响应。这通常是由于以下一个或多个原因造成的:

  1. async 关键字的遗漏: 如果路由处理函数或包含 await 的函数没有被声明为 async,那么 await 关键字将无法正确暂停函数的执行。
  2. await 关键字的遗漏: 即使函数是 async 的,如果没有在 Promise.all 调用前加上 await,那么 Promise.all 返回的 Promise 不会被等待,函数会继续执行。
  3. Promise 链式调用的误用: 有时开发者可能会在 await Promise.all(tasks) 之后再次使用 .then(),这在逻辑上是多余的,且可能导致混淆。如果已经 await 了一个 Promise,它的结果可以直接在下一行获取,或者其错误可以通过 try…catch 捕获。
  4. Promise 构造函数内部的错误处理不当: 当手动创建 new Promise 时,内部的异步操作(如 fs.writeFile 的回调)如果发生错误,必须显式地调用 reject(err) 将错误传递出去,否则外部的 Promise 无法感知到内部的失败。

让我们看一个初始的错误示例:

// app.post 路由处理器 (可能存在问题) app.post('/', async (req: Request, res: Response) => {    const tasksRequest = req.body as TasksRequest;    let tasks: Promise<any>[] = [];     // 这里的 await Promise.all(tasks) 看起来正确,但如果 processTask 有问题,可能仍无法等待    tasks = tasksRequest.tasks.map((t) => processTask(t, tasksRequest.configs));    await Promise.all(tasks); // 问题可能出在 processTask 的实现或后续没有发送响应    // ... 缺少 res.send() 或 res.json() });  // processTask 函数 (使用回调风格的 fs.writeFile,且错误处理不完善) function processTask(task: Task, configs: Configs) {   return new Promise<void>((resolve, reject) => {     try {       const fileName = './output/' + task.tag + 's.json';        fetch(configs.Host + configs.APIsBasePrefix + task.parentResource + task.mostRelatedPath, {         method: 'GET'       }).then(result => {         result.json().then(jsonResult => {           // fs.writeFile 是回调风格,需要检查 err 并 reject           fs.writeFile(fileName, JSON.stringify(jsonResult), function () {             console.log('finished writing :' + fileName);             resolve();           });         }).catch(err => reject(err));       }).catch(err => reject(err));     } catch (err) {       console.log(err); // 这里的 catch 只能捕获同步错误     }   }); }

在这个 processTask 的初始版本中,fs.writeFile 是一个回调函数,其回调中并没有检查 err 参数并调用 reject(err)。这意味着即使文件写入失败,外部的 Promise 也可能被 resolve(),导致 Promise.all 无法感知到这个内部的失败。

另一个常见的错误是在 async 函数中,虽然使用了 Promise.all,但却没有 await 它,或者在 await 之后又错误地使用了 .then():

// app.post 路由处理器 (缺少 await Promise.all) app.post('/', async (req: Request, res: Response) => { // 声明为 async 是正确的   const tasksRequest = req.body as TasksRequest;   let tasks = [];    tasks = tasksRequest.tasks.map( (t) =>  processTask(t, tasksRequest.configs));    // 这里的 Promise.all(tasks).then(...) 没有被 await   // 导致路由处理器会立即继续执行,可能在任务完成前就结束   Promise.all(tasks).then(results => {     console.log('After awaiting');     // ... 应该在这里发送响应,但如果这里没有 res.send(),外部也无法收到响应   });   // 如果这里没有 res.send(),且上面的 Promise 没被 await,API 将挂起或超时 });

最佳实践:使用 async/await 和 fs.promises 进行重构

为了解决上述问题,我们应该采用现代 JavaScript 的 async/await 语法,并利用 Node.js fs 模块的 Promise 版本 (fs.promises),这能大大提高代码的可读性和健壮性。

1. 重构 processTask 函数

将 processTask 函数转换为 async 函数,并使用 await 来处理异步操作,包括 fetch 请求和文件写入。

Express.js 中等待多个 Promise 完成再响应的策略

Riffo

Riffo是一个免费的文件智能命名和管理工具

Express.js 中等待多个 Promise 完成再响应的策略131

查看详情 Express.js 中等待多个 Promise 完成再响应的策略

import * as fs from 'fs/promises'; // 导入 fs.promises import fetch from 'node-fetch'; // 如果在 Node.js 环境,可能需要安装 node-fetch  // 定义类型 (假设已定义) interface Task {   tag: string;   parentResource: string;   mostRelatedPath: string; }  interface Configs {   Host: string;   APIsBasePrefix: string; }  async function processTask(task: Task, configs: Configs): Promise<void> {   try {     const fileName = `./output/${task.tag}s.json`;      // 使用 await 等待 fetch 请求完成     const result = await fetch(configs.Host + configs.APIsBasePrefix + task.parentResource + task.mostRelatedPath, {       method: 'GET'     });      // 使用 await 等待 JSON 解析完成     const jsonResult = await result.json();      // 使用 fs.promises.writeFile,它返回一个 Promise,可以直接 await     await fs.writeFile(fileName, JSON.stringify(jsonResult));     console.log(`finished writing: ${fileName}`);   } catch (err) {     console.error(`Error processing task ${task.tag}:`, err);     // 捕获并重新抛出错误,以便 Promise.all 能够感知到     throw err;   } }

说明:

  • processTask 被声明为 async 函数,它隐式返回一个 Promise。
  • fetch 和 result.json() 都使用了 await,使得代码像同步一样顺序执行。
  • fs.promises.writeFile 返回一个 Promise,因此可以直接 await 它,无需回调函数。
  • try…catch 块能够捕获 fetch、json() 解析或 writeFile 过程中发生的任何错误,并通过 throw err 将错误传递给外部调用者。

2. 重构 Express.js 路由处理器

确保 Express.js 路由处理函数被声明为 async,并且在调用 Promise.all 时使用 await 关键字。

import express, { Request, Response } from 'express'; // ... 其他导入,如 processTask 函数  const app = express(); app.use(express.json()); // 用于解析请求体  // 定义类型 (假设已定义) interface TasksRequest {   tasks: Task[];   configs: Configs; }  app.post('/', async (req: Request, res: Response) => {   try {     const tasksRequest = req.body as TasksRequest;      // 映射任务为 Promise 数组     const tasks: Promise<void>[] = tasksRequest.tasks.map((t) =>       processTask(t, tasksRequest.configs)     );      console.log('Starting to process tasks...');      // 使用 await 等待所有 Promise 完成     // Promise.all 会等待所有任务成功,如果任何一个失败,它会立即拒绝     await Promise.all(tasks);      console.log('All tasks finished successfully.');     // 所有任务完成后,发送成功响应     res.status(200).json({ message: 'All tasks processed successfully.' });    } catch (error) {     console.error('An error occurred during task processing:', error);     // 捕获任何一个任务失败的错误,并发送错误响应     res.status(500).json({ message: 'Failed to process some tasks.', error: (error as Error).message });   } });  // 启动服务器 (示例) const PORT = 3000; app.listen(PORT, () => {   console.log(`Server running on port ${PORT}`); });

说明:

  • 路由处理函数被声明为 async。
  • tasksRequest.tasks.map(…) 会立即生成一个 Promise 数组,这些 Promise 会并行开始执行。
  • await Promise.all(tasks) 是关键。它会暂停 app.post 函数的执行,直到 tasks 数组中的所有 Promise 都解决(resolved)或其中一个被拒绝(rejected)。
  • try…catch 块用于捕获 Promise.all 可能抛出的错误(即任何一个任务失败的情况),确保 API 能够发送适当的错误响应。
  • 在所有任务成功完成后,res.status(200).json(…) 才会被调用,向客户端发送最终响应。

总结与注意事项

通过以上重构,我们实现了在 Express.js API 中等待多个 Promise 完成再发送响应的健壮机制。

关键点总结:

  • 声明 async 函数: 任何需要使用 await 的函数(包括 Express.js 路由处理函数)都必须声明为 async。
  • 使用 await Promise.all(): 这是等待所有并发 Promise 完成的核心方法。确保在 Promise.all 调用前加上 await。
  • 利用 Promise-based API: 优先使用 Node.js 核心模块提供的 Promise 版本(如 fs.promises),或使用返回 Promise 的第三方库(如 node-fetch),避免回调地狱。
  • 完善错误处理: 在 async 函数中使用 try…catch 块来捕获 await 表达式可能抛出的错误,并在 Promise 构造函数内部确保所有错误都被 reject 掉。对于 Promise.all,如果其中一个 Promise 拒绝,整个 Promise.all 就会拒绝,其错误可以通过外部的 try…catch 捕获。
  • 发送响应: 确保在所有异步操作成功完成后,才通过 res.status().json() 或 res.send() 等方法发送响应。

遵循这些最佳实践,可以构建出高效、稳定且易于维护的 Express.js 异步 API。

以上就是Express.javascript java js node.js json node 处理器 app 回调函数 工具 ai 路由 JavaScript json express 构造函数 try throw catch 回调函数 map 并发 JS promise 异步 重构

大家都在看:

javascript java js node.js json node 处理器 app 回调函数 工具 ai 路由 JavaScript json express 构造函数 try throw catch 回调函数 map 并发 JS promise 异步 重构

app
上一篇
下一篇