在 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 非常适合以下场景:
-
- 请求追踪:记录请求 ID、跟踪 ID 等元数据,无需在每个函数间传递。
-
- 事务管理:在多函数间传递数据库事务对象。
-
- 日志记录:在日志中自动附加当前请求上下文。
请求追踪示例
下面来写一个请求追踪的简单示例,如下代码:
// 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
实现了请求上下文的管理,用于在异步操作中自动记录日志。每次请求到达时,会生成一个包含 requestId
、userId
、path
和时间戳的上下文,并将其与请求绑定。随后,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
实现了请求上下文的全局管理,每个请求在进入中间件时创建一个包含 userId
、requestId
和 startTime
的上下文,并与该请求绑定。在后续的异步函数(如用户验证、订单处理和通知发送)中,可以直接获取上下文数据,避免在函数之间显式传递参数,从而简化代码逻辑并提高可维护性。代码如下:
// 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
可能不符合用户的架构需求。
参考来源:
评论(0)