首页
Preview

AsyncLocalStorage:简化 Node.js 中的上下文管理

在 Node.js 异步操作中,使用 AsyncLocalStorage 可以轻松维护上下文,而无需在每个函数间手动传递数据。可以将其想象为一个“存储箱”,它伴随请求流转,携带关键信息,使代码的任何部分都能访问这些数据。

传统方式:手动传递参数

以下是一个未使用 AsyncLocalStorage 的 Express 应用示例,我们需要在多个函数之间传递 userId

// App.js
async function handleRequest(req, res) {
  const userId = req.headers['user-id'];
  await validateUser(userId);
  await processOrder(userId);
  await sendNotification(userId);
}

async function validateUser(userId) {
}

async function processOrder(userId) {
  await updateInventory(userId);
}

async function updateInventory(userId) {
}

async function sendNotification(userId) {
}

上面的函数中每个都要传递userId,如果再添加其他参数,函数签名就会变得非常复杂。

何时使用 AsyncLocalStorage

AsyncLocalStorage 非常适合以下场景:

    1. 请求追踪:记录请求 ID、跟踪 ID 等元数据,无需在每个函数间传递。
    1. 事务管理:在多函数间传递数据库事务对象。
    1. 日志记录:在日志中自动附加当前请求上下文。

请求追踪示例

下面来写一个请求追踪的简单示例,如下代码:

// App.js
const { AsyncLocalStorage } = require('node:async_hooks');
//负责管理每个请求的上下文
const storage = new AsyncLocalStorage();

function setupRequestTracing(app) {
  app.use((req, res, next) => {
  //为每个 HTTP 请求分配一个唯一的 traceId 并将其注入上下文,便于请求追踪,从请求头中读取 x-trace-id,如果没有提供,则生成一个唯一的 UUID。
    const traceId = req.headers['x-trace-id'] || crypto.randomUUID();
//storage.run() 方法将指定的上下文(这里是 { traceId })与当前请求绑定。该上下文在 storage.getStore() 中可以被任何异步操作访问。
    storage.run({ traceId }, () => {
    //将生成的 traceId 写入响应头,方便客户端和其他服务端查看。
      res.setHeader('x-trace-id', traceId);
      //调用 next() 以进入下一个中间件或路由处理程序。
      next();
    });
  });
}

function log(message) {
  const { traceId } = storage.getStore();
  console.log(`[${traceId}] ${message}`);
}

日志记录示例

通过 AsyncLocalStorage 实现了请求上下文的管理,用于在异步操作中自动记录日志。每次请求到达时,会生成一个包含 requestIduserIdpath 和时间戳的上下文,并将其与请求绑定。随后,logger 函数可以随时获取该上下文,生成带有详细请求信息的结构化日志,简化了日志记录并提高了可追踪性。

// App.js
import { AsyncLocalStorage } from 'node:async_hooks';
const logStorage = new AsyncLocalStorage();

app.use((req, res, next) => {
  const logContext = {
    requestId: crypto.randomUUID(),
    userId: req.headers['user-id'],
    path: req.path,
    timestamp: new Date().toISOString(),
  };

  logStorage.run(logContext, () => {
    next();
  });
});

function logger(message, level = 'info') {
  const context = logStorage.getStore();
  console.log(JSON.stringify({
    level,
    message,
    requestId: context.requestId,
    userId: context.userId,
    path: context.path,
    timestamp: context.timestamp,
  }));
}

使用 AsyncLocalStorage 简化代码

回到之前的问题,我们使用AsyncLocalStorage来简化之前的Express 应用示例。通过 AsyncLocalStorage 实现了请求上下文的全局管理,每个请求在进入中间件时创建一个包含 userIdrequestIdstartTime 的上下文,并与该请求绑定。在后续的异步函数(如用户验证、订单处理和通知发送)中,可以直接获取上下文数据,避免在函数之间显式传递参数,从而简化代码逻辑并提高可维护性。代码如下:

// App.js
const { AsyncLocalStorage } = require('node:async_hooks');

const storage = new AsyncLocalStorage();

const app = express();
app.use((req, res, next) => {
  const context = {
    userId: req.headers['user-id'],
    requestId: crypto.randomUUID(),
    startTime: Date.now(),
  };

  storage.run(context, () => {
    next();
  });
});

async function validateUser() {
  const context = storage.getStore();
  console.log(`Validating user ${context.userId}`);
}

async function processOrder() {
  const context = storage.getStore();
  console.log(`Processing order for ${context.userId}`);
}

async function sendNotification() {
  const context = storage.getStore();
  console.log(`Sending notification to ${context.userId}`);
}

app.post('/orders', async (req, res) => {
  await validateUser();
  await processOrder();
  await sendNotification();
});

何时避免使用 AsyncLocalStorage

尽管 AsyncLocalStorage 功能强大,但在以下情况中应谨慎使用:

  • 上下文只需在少数函数间流转:直接传递参数更直观。
  • 处理同步代码:AsyncLocalStorage 增加了不必要的复杂性。
  • 构建公共 API:强制使用 AsyncLocalStorage 可能不符合用户的架构需求。

参考来源:

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
Hedy
大家好!我是一位前端开发工程师,拥有6年以上的前端开发经验。我熟练掌握HTML、CSS、JavaScript等语言,能够灵活运用各种前端框架,如Vue、React、Uniapp、Flutter等。我注重理论与实践相结合,能够为学员提供丰富的案例和实践项目,并以生动、易懂的语言为学员讲解前端开发的核心知识和技能。我不仅注重传授技能,更关注学员的职业发展,希望通过我的教学,帮助学员成为一名优秀的前端开发工程师。

评论(0)

添加评论