Express 与 Koa
问题
Express 和 Koa 有什么区别?中间件的工作原理是什么?
答案
Express 和 Koa 都是 Node.js 的 Web 框架,Koa 是 Express 团队打造的下一代框架,更轻量、基于 async/await。
Express
基本用法
import express, { Request, Response, NextFunction } from 'express';
const app = express();
// 中间件
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 路由
app.get('/', (req: Request, res: Response) => {
res.send('Hello World');
});
app.get('/users/:id', (req, res) => {
res.json({ id: req.params.id, name: 'Alice' });
});
app.post('/users', (req, res) => {
const user = req.body;
res.status(201).json({ id: 1, ...user });
});
// 错误处理
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Express 中间件
// 中间件签名
type Middleware = (
req: Request,
res: Response,
next: NextFunction
) => void;
// 应用级中间件
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// 路由级中间件
const router = express.Router();
router.use(authMiddleware);
router.get('/profile', getProfile);
app.use('/api', router);
// 错误处理中间件(4个参数)
app.use((err: Error, req, res, next) => {
res.status(500).json({ error: err.message });
});
Express 执行流程
Koa
基本用法
import Koa, { Context, Next } from 'koa';
import Router from '@koa/router';
import bodyParser from 'koa-bodyparser';
const app = new Koa();
const router = new Router();
// 中间件
app.use(bodyParser());
// 路由
router.get('/', async (ctx: Context) => {
ctx.body = 'Hello World';
});
router.get('/users/:id', async (ctx) => {
ctx.body = { id: ctx.params.id, name: 'Alice' };
});
router.post('/users', async (ctx) => {
const user = ctx.request.body;
ctx.status = 201;
ctx.body = { id: 1, ...user };
});
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);
Koa 洋葱模型
// Koa 中间件签名
type Middleware = (ctx: Context, next: Next) => Promise<void>;
// 洋葱模型演示
app.use(async (ctx, next) => {
console.log('1. 进入中间件 A');
await next();
console.log('6. 离开中间件 A');
});
app.use(async (ctx, next) => {
console.log('2. 进入中间件 B');
await next();
console.log('5. 离开中间件 B');
});
app.use(async (ctx, next) => {
console.log('3. 进入中间件 C');
ctx.body = 'Hello';
console.log('4. 离开中间件 C');
});
// 输出顺序:
// 1. 进入中间件 A
// 2. 进入中间件 B
// 3. 进入中间件 C
// 4. 离开中间件 C
// 5. 离开中间件 B
// 6. 离开中间件 A
Koa 错误处理
// 错误处理中间件(放在最前面)
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500;
ctx.body = {
error: err.message
};
// 触发应用级错误事件
ctx.app.emit('error', err, ctx);
}
});
// 应用级错误监听
app.on('error', (err, ctx) => {
console.error('Server Error:', err);
});
对比
| 特性 | Express | Koa |
|---|---|---|
| 异步处理 | 回调/Promise | async/await |
| 中间件模型 | 线性 | 洋葱模型 |
| 内置功能 | 较多(路由、模板等) | 极简(需插件) |
| Context | req/res 分离 | 统一 ctx |
| 错误处理 | 错误中间件 | try/catch |
| 体积 | 较大 | 轻量 |
// Express - 回调风格
app.get('/users', (req, res, next) => {
User.find()
.then(users => res.json(users))
.catch(next);
});
// Koa - async/await
router.get('/users', async (ctx) => {
ctx.body = await User.find();
});
中间件实现原理
Express compose
// Express 中间件执行(简化版)
function createApplication() {
const middleware: Function[] = [];
function app(req: Request, res: Response) {
let index = 0;
function next(err?: Error) {
if (err) {
// 找到错误处理中间件
return handleError(err, req, res);
}
const fn = middleware[index++];
if (fn) {
fn(req, res, next);
}
}
next();
}
app.use = (fn: Function) => {
middleware.push(fn);
};
return app;
}
Koa compose
// Koa koa-compose(简化版)
function compose(middleware: Function[]) {
return function(ctx: Context, next?: Function) {
let index = -1;
function dispatch(i: number): Promise<void> {
if (i <= index) {
return Promise.reject(new Error('next() called multiple times'));
}
index = i;
let fn = middleware[i];
if (i === middleware.length) {
fn = next;
}
if (!fn) {
return Promise.resolve();
}
try {
return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
return dispatch(0);
};
}
常见面试问题
Q1: Express 和 Koa 的中间件有什么区别?
答案:
| 特性 | Express | Koa |
|---|---|---|
| 执行模型 | 线性(瀑布流) | 洋葱模型 |
| 异步处理 | 回调 + next() | async/await |
| 后置处理 | 需要特殊处理 | 自然支持(await next() 后) |
| 响应时机 | 可以多次 | ctx.body 赋值即可 |
// Express - 无法简单实现后置处理
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
console.log(`耗时: ${Date.now() - start}ms`);
});
next();
});
// Koa - 自然支持后置处理
app.use(async (ctx, next) => {
const start = Date.now();
await next();
console.log(`耗时: ${Date.now() - start}ms`);
});
Q2: 什么是洋葱模型?为什么 Koa 采用洋葱模型?
答案:
洋葱模型是指中间件的执行像剥洋葱一样,请求从外到内穿过各层中间件,响应再从内到外穿出。
优势:
- 支持后置处理(计时、日志、错误处理)
- 代码逻辑清晰
- 更好的错误处理
// 洋葱模型实现计时
app.use(async (ctx, next) => {
const start = Date.now();
await next(); // 等待内层中间件执行完
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
Q3: 如何实现一个简单的路由?
答案:
// 简单路由实现
class SimpleRouter {
private routes: Map<string, Map<string, Function>> = new Map();
get(path: string, handler: Function) {
this.addRoute('GET', path, handler);
}
post(path: string, handler: Function) {
this.addRoute('POST', path, handler);
}
private addRoute(method: string, path: string, handler: Function) {
if (!this.routes.has(method)) {
this.routes.set(method, new Map());
}
this.routes.get(method)!.set(path, handler);
}
middleware() {
return async (ctx: Context, next: Next) => {
const methodRoutes = this.routes.get(ctx.method);
if (methodRoutes) {
const handler = methodRoutes.get(ctx.path);
if (handler) {
await handler(ctx);
return;
}
}
await next();
};
}
}
// 使用
const router = new SimpleRouter();
router.get('/users', async (ctx) => {
ctx.body = [{ id: 1 }];
});
app.use(router.middleware());
Q4: Express 的 next() 和 Koa 的 next() 有什么区别?
答案:
// Express - next() 是普通函数
app.use((req, res, next) => {
// next() 调用后立即执行下一个中间件
// 但当前代码继续执行
next();
console.log('这会在 next() 后立即执行');
});
// Koa - next() 返回 Promise
app.use(async (ctx, next) => {
// await next() 等待后续中间件全部执行完
await next();
console.log('这在所有后续中间件执行完后执行');
});
// Express 实现类似洋葱模型
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
// 响应结束后执行
console.log(`耗时: ${Date.now() - start}ms`);
});
next();
});
Q5: 如何选择 Express 还是 Koa?
答案:
选择 Express:
- 成熟稳定,生态丰富
- 团队熟悉回调风格
- 快速开发,内置功能多
- 需要大量社区中间件
选择 Koa:
- 现代 async/await 风格
- 需要精细控制中间件
- 追求轻量和灵活
- 新项目,团队熟悉 Koa
// Express - 适合快速开发
import express from 'express';
const app = express();
app.use(express.json());
// 大量内置中间件
// Koa - 更灵活但需要更多配置
import Koa from 'koa';
import bodyParser from 'koa-bodyparser';
import Router from '@koa/router';
const app = new Koa();
app.use(bodyParser());
// 需要手动添加各种功能