异步中间件的实现方式
异步中间件的实现方式
Express中间件本质上是函数,负责处理HTTP请求和响应。异步中间件在处理过程中涉及异步操作,比如数据库查询、文件读写或API调用。理解异步中间件的实现方式对构建高效应用至关重要。
回调函数方式
传统Express中间件通过回调函数处理异步操作。当异步任务完成时,调用next()
传递控制权或直接响应客户端。
app.get('/user', (req, res, next) => {
User.findById(req.params.id, (err, user) => {
if (err) return next(err);
res.json(user);
});
});
这种方式需要手动处理错误,通过return next(err)
将错误传递给Express错误处理中间件。缺点是容易产生"回调地狱",嵌套层级过深时代码难以维护。
Promise方式
ES6引入Promise后,中间件可以更清晰地处理异步流程。许多现代数据库驱动和库都返回Promise对象。
app.get('/posts', (req, res, next) => {
Post.find()
.then(posts => res.json(posts))
.catch(err => next(err));
});
使用Promise链可以扁平化异步代码结构。.catch()
集中处理错误,避免在每个回调中重复错误处理逻辑。
async/await方式
ES2017的async/await语法让异步代码看起来像同步代码,大幅提升可读性。这是当前Express异步中间件的最佳实践。
app.get('/comments', async (req, res, next) => {
try {
const comments = await Comment.find({ postId: req.params.postId });
res.json(comments);
} catch (err) {
next(err);
}
});
async函数自动返回Promise,内部可以使用await暂停执行直到Promise解决。错误通过try/catch块处理,代码结构更加直观。
中间件错误处理
异步中间件需要特别注意错误传播。未捕获的Promise拒绝可能导致应用崩溃。Express 5.x会自动捕获并传递async函数中的错误,但在4.x中需要显式处理。
// Express 4.x中的安全写法
const asyncHandler = fn => (req, res, next) => {
Promise.resolve(fn(req, res, next))
.catch(next);
};
app.get('/profile', asyncHandler(async (req, res) => {
const user = await User.findById(req.user.id);
const posts = await Post.find({ author: user.id });
res.render('profile', { user, posts });
}));
可以创建高阶函数包装async中间件,确保所有错误都被捕获并传递给Express错误处理链。
并行异步操作
当中间件需要执行多个独立异步操作时,Promise.all可以提升性能。
app.get('/dashboard', async (req, res, next) => {
try {
const [user, messages, notifications] = await Promise.all([
User.findById(req.user.id),
Message.find({ recipient: req.user.id }),
Notification.find({ userId: req.user.id })
]);
res.render('dashboard', { user, messages, notifications });
} catch (err) {
next(err);
}
});
这种方式并行执行所有异步操作,总耗时取决于最慢的那个操作,而不是所有操作耗时的总和。
中间件组合
复杂业务逻辑可能需要组合多个异步中间件。可以使用express-async-handler
等库简化错误处理。
const asyncHandler = require('express-async-handler');
const validateUser = asyncHandler(async (req, res, next) => {
const user = await User.findById(req.params.id);
if (!user) throw new Error('User not found');
req.user = user;
next();
});
const checkPermission = asyncHandler(async (req, res, next) => {
const hasAccess = await checkUserAccess(req.user);
if (!hasAccess) throw new Error('Access denied');
next();
});
app.get('/admin/:id', validateUser, checkPermission, (req, res) => {
res.json(req.user);
});
每个中间件专注于单一职责,通过next()传递控制权,错误自动传递给错误处理中间件。
流式处理
对于大文件或数据流处理,中间件可以采用流式接口避免内存溢出。
const fs = require('fs');
const { pipeline } = require('stream');
app.get('/download', (req, res, next) => {
const fileStream = fs.createReadStream('./large-file.zip');
res.setHeader('Content-Type', 'application/zip');
pipeline(fileStream, res, err => {
if (err) next(err);
});
});
使用Node.js的stream.pipeline替代pipe()方法,可以自动处理流错误和清理资源。
定时任务中间件
有些中间件需要执行定时异步任务,比如缓存更新或数据清理。
const schedule = require('node-schedule');
// 每天凌晨清理过期会话
app.use((req, res, next) => {
schedule.scheduleJob('0 0 * * *', async () => {
try {
await Session.cleanExpired();
} catch (err) {
console.error('Session cleanup failed:', err);
}
});
next();
});
定时任务应该有自己的错误处理逻辑,避免影响主请求流程。任务执行与请求响应周期解耦。
性能考量
异步中间件虽然强大,但不当使用可能影响性能。避免不必要的await,特别是循环中的await。
// 低效写法
app.get('/slow', async (req, res) => {
const results = [];
for (const id of req.query.ids) {
results.push(await Item.findById(id)); // 顺序执行
}
res.json(results);
});
// 优化写法
app.get('/fast', async (req, res) => {
const promises = req.query.ids.map(id => Item.findById(id));
const results = await Promise.all(promises); // 并行执行
res.json(results);
});
批量处理异步操作通常比逐个处理更高效。数据库查询应考虑使用$in操作符替代多个findById调用。
上下文保持
异步中间件中需要注意保持请求上下文。避免直接引用req/res对象在异步回调中。
// 有问题的写法
app.get('/problem', (req, res) => {
someAsyncOperation(() => {
res.json(req.user); // req可能已不是当前请求
});
});
// 正确写法
app.get('/solution', (req, res) => {
const { user } = req; // 提前捕获上下文
someAsyncOperation(() => {
res.json(user); // 使用局部变量
});
});
在长时间运行的异步操作中,req/res对象可能已被垃圾回收,导致难以诊断的错误。
中间件测试
异步中间件的测试需要特殊处理,确保异步操作完成后再断言结果。
const request = require('supertest');
const app = require('../app');
describe('Auth Middleware', () => {
it('should reject unauthorized requests', async () => {
const res = await request(app)
.get('/protected')
.expect(401);
expect(res.body.error).toBe('Unauthorized');
});
it('should allow authorized requests', async () => {
const token = createTestToken();
const res = await request(app)
.get('/protected')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(res.body.data).toBeDefined();
});
});
使用async/await测试框架可以简化异步测试代码,使测试用例更清晰易读。
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:错误处理中间件的特殊用法
下一篇:中间件的复用与模块化