跳到主要内容

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);
});

对比

特性ExpressKoa
异步处理回调/Promiseasync/await
中间件模型线性洋葱模型
内置功能较多(路由、模板等)极简(需插件)
Contextreq/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 的中间件有什么区别?

答案

特性ExpressKoa
执行模型线性(瀑布流)洋葱模型
异步处理回调 + 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());
// 需要手动添加各种功能

相关链接