Node.js 主流框架对比
问题
Node.js 有哪些主流框架?它们各自有什么特点和适用场景?在实际项目中如何做技术选型?
答案
Node.js 生态中有多个成熟的 Web 框架,从极简的 Express/Koa,到企业级的 NestJS/Midway,再到追求极致性能的 Fastify。理解它们的设计理念、中间件机制和适用场景,是做出正确技术选型的关键。
1. 框架概览
Express
最老牌、生态最丰富的 Node.js Web 框架。2010 年发布,至今仍是 npm 下载量最高的服务端框架。API 简洁,学习曲线平缓,社区中间件生态极其丰富。
Koa
Express 原班人马打造的下一代框架。去掉了内置的路由和模板等功能,核心极其精简(约 550 行代码)。原生支持 async/await,采用洋葱模型中间件机制。
NestJS
企业级全功能框架,深受 Angular 启发。使用装饰器 + 依赖注入(DI)架构,开箱即用地支持 TypeScript。底层可选 Express 或 Fastify 作为 HTTP 引擎。
Fastify
专注极致性能的框架。通过 JSON Schema 驱动的序列化、高效的路由查找算法(Radix Tree)实现了远超 Express 的吞吐量。插件系统设计精巧,支持封装隔离。
Midway.js
阿里巴巴出品的 Node.js 框架,基于 IoC(控制反转)容器,天然适配 Serverless 场景。提供 HTTP、WebSocket、RPC、定时任务等一体化方案。
对比总览
| 特性 | Express | Koa | NestJS | Fastify | Midway.js |
|---|---|---|---|---|---|
| 定位 | 通用 Web 框架 | 极简中间件框架 | 企业级全功能框架 | 高性能 Web 框架 | 企业级一体化框架 |
| 首发年份 | 2010 | 2013 | 2017 | 2016 | 2018 |
| 中间件模型 | 线性(next 回调) | 洋葱模型 | 管线模型 | Hook 生命周期 | 洋葱 + 装饰器 |
| TypeScript | 需手动配置 | 需手动配置 | 原生支持 | 良好支持 | 原生支持 |
| 学习曲线 | 低 | 低 | 高 | 中 | 中高 |
| 性能 | 一般 | 一般 | 取决于底层引擎 | 极高 | 中等 |
| 生态 | 极其丰富 | 较丰富 | 丰富且自成体系 | 成长中 | 阿里生态 |
| 适用场景 | 快速原型、中小项目 | 轻量 API、中间件定制 | 大型企业项目 | 高性能 API | Serverless、阿里系 |
2. 中间件机制对比
中间件是 Node.js 框架的核心设计,不同框架采用了截然不同的机制。
Express:线性中间件
请求依次通过中间件链,每个中间件通过调用 next() 传递控制权。一旦调用了 res.send() / res.json(),响应即刻发出。
Koa:洋葱模型
中间件像洋葱层一样包裹,请求从外向内穿过,响应从内向外穿出。await next() 等待内层全部执行完后再执行后置逻辑。
NestJS:管线模型
NestJS 拥有最完整的请求处理管线,每个环节职责明确:
| 组件 | 职责 | 类比 |
|---|---|---|
| Middleware | 通用预处理(日志、CORS) | Express 中间件 |
| Guard | 权限认证、角色校验 | 路由守卫 |
| Interceptor | 前后拦截(缓存、日志、转换) | AOP 切面 |
| Pipe | 参数校验与转换 | 数据管道 |
| Exception Filter | 统一异常处理 | 错误过滤器 |
Fastify:Hook 生命周期
Fastify 提供了精细的请求/响应生命周期 Hook:
代码对比
- Express
- Koa
- NestJS
- Fastify
import express, { Request, Response, NextFunction } from 'express';
const app = express();
// 线性中间件
app.use((req: Request, res: Response, next: NextFunction) => {
console.log('中间件 1 - 开始');
next();
console.log('中间件 1 - next() 后(不保证在响应后执行)');
});
app.use((req: Request, res: Response, next: NextFunction) => {
console.log('中间件 2 - 开始');
next();
});
app.get('/', (req: Request, res: Response) => {
res.json({ message: 'Hello Express' });
});
import Koa, { Context, Next } from 'koa';
const app = new Koa();
// 洋葱模型中间件
app.use(async (ctx: Context, next: Next) => {
console.log('中间件 1 - 进入');
await next();
console.log('中间件 1 - 离开(保证在内层执行完后执行)');
});
app.use(async (ctx: Context, next: Next) => {
console.log('中间件 2 - 进入');
await next();
console.log('中间件 2 - 离开');
});
app.use(async (ctx: Context) => {
ctx.body = { message: 'Hello Koa' };
});
import {
Controller, Get, UseGuards, UseInterceptors, UsePipes,
Injectable, CanActivate, NestInterceptor, PipeTransform,
ExecutionContext, CallHandler, NestMiddleware
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';
// Middleware
@Injectable()
class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => void) {
console.log('Middleware: 请求日志');
next();
}
}
// Guard
@Injectable()
class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
console.log('Guard: 权限校验');
return true;
}
}
// Interceptor
@Injectable()
class TimingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now();
console.log('Interceptor: 前置');
return next.handle().pipe(
tap(() => console.log(`Interceptor: 后置 - 耗时 ${Date.now() - start}ms`))
);
}
}
@Controller('hello')
@UseGuards(AuthGuard)
@UseInterceptors(TimingInterceptor)
class HelloController {
@Get()
getHello(): { message: string } {
return { message: 'Hello NestJS' };
}
}
import Fastify, { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
const app: FastifyInstance = Fastify({ logger: true });
// Hook 生命周期
app.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => {
console.log('Hook: onRequest - 请求到达');
});
app.addHook('preHandler', async (request: FastifyRequest, reply: FastifyReply) => {
console.log('Hook: preHandler - 处理前');
});
app.addHook('onSend', async (request, reply, payload) => {
console.log('Hook: onSend - 响应发送前');
return payload;
});
app.addHook('onResponse', async (request: FastifyRequest, reply: FastifyReply) => {
console.log(`Hook: onResponse - 请求完成 ${reply.elapsedTime}ms`);
});
app.get('/', async () => {
return { message: 'Hello Fastify' };
});
Express 的 next() 是同步回调,调用后当前函数继续执行但无法确保在响应之后;Koa 的 next() 返回 Promise,await next() 确保内层全部完成后才执行后续代码。这是两者最根本的区别。更详细的对比可参考 Express 与 Koa。
3. Express 核心
Express 是 Node.js 生态的事实标准,npm 周下载量超 3000 万。即使选择 NestJS,底层默认也是 Express。
路由系统
import express, { Router, Request, Response } from 'express';
const app = express();
const router: Router = express.Router();
// 路由分组
router.get('/users', async (req: Request, res: Response) => {
res.json([{ id: 1, name: 'Alice' }]);
});
router.get('/users/:id', async (req: Request, res: Response) => {
const { id } = req.params;
res.json({ id, name: 'Alice' });
});
router.post('/users', express.json(), async (req: Request, res: Response) => {
const user = req.body;
res.status(201).json({ id: Date.now(), ...user });
});
// 路由参数中间件
router.param('id', (req, res, next, id) => {
// 预处理参数,如查询数据库
console.log(`访问用户: ${id}`);
next();
});
// 挂载到前缀
app.use('/api', router);
错误处理中间件
Express 通过四参数中间件处理错误。更多错误处理模式可参考 Node.js 错误处理。
import express, { Request, Response, NextFunction } from 'express';
// 自定义错误类
class AppError extends Error {
constructor(
public statusCode: number,
message: string,
public code: string = 'INTERNAL_ERROR'
) {
super(message);
this.name = 'AppError';
}
}
// 异步错误包装器
const asyncHandler = (fn: Function) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
const app = express();
app.get('/users/:id', asyncHandler(async (req: Request, res: Response) => {
const user = await findUser(req.params.id);
if (!user) {
throw new AppError(404, '用户不存在', 'USER_NOT_FOUND');
}
res.json(user);
}));
// 错误处理中间件(必须 4 个参数)
app.use((err: AppError, req: Request, res: Response, next: NextFunction) => {
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
code: err.code || 'INTERNAL_ERROR',
message: err.message,
});
});
常用中间件生态
| 中间件 | 用途 | 说明 |
|---|---|---|
cors | 跨域处理 | 配置 CORS 策略 |
helmet | 安全加固 | 设置安全相关 HTTP 头 |
morgan | 请求日志 | HTTP 请求日志记录 |
multer | 文件上传 | 处理 multipart/form-data |
compression | 响应压缩 | gzip/brotli 压缩 |
express-rate-limit | 限流 | 请求频率限制 |
express-validator | 参数校验 | 请求参数校验 |
4. Koa 核心
洋葱模型原理(koa-compose)
Koa 的核心在于 koa-compose,它将所有中间件组合成一个递归调用链:
type KoaMiddleware<T> = (ctx: T, next: () => Promise<void>) => Promise<void>;
function compose<T>(middlewares: KoaMiddleware<T>[]) {
return function (ctx: T, next?: () => Promise<void>): Promise<void> {
let index = -1;
function dispatch(i: number): Promise<void> {
// 防止同一中间件中多次调用 next()
if (i <= index) {
return Promise.reject(new Error('next() called multiple times'));
}
index = i;
let fn: KoaMiddleware<T> | undefined = middlewares[i];
if (i === middlewares.length) {
fn = next as KoaMiddleware<T> | undefined;
}
if (!fn) return Promise.resolve();
try {
// 核心:将 dispatch(i+1) 作为 next 传入
return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
return dispatch(0);
};
}
不要在 Koa 中间件中多次调用 next()。koa-compose 内部通过 index 变量做了保护,多次调用会直接抛出 next() called multiple times 错误。
Context 对象
Koa 将 req 和 res 封装到统一的 ctx 对象中,提供了更优雅的 API:
import Koa, { Context } from 'koa';
const app = new Koa();
app.use(async (ctx: Context) => {
// 请求信息(代理到 ctx.request)
ctx.method; // GET
ctx.url; // /users?page=1
ctx.path; // /users
ctx.query; // { page: '1' }
ctx.headers; // 请求头
ctx.ip; // 客户端 IP
// 响应操作(代理到 ctx.response)
ctx.status = 200; // 设置状态码
ctx.body = { data: [] }; // 设置响应体
ctx.set('X-Custom', '1');// 设置响应头
ctx.type = 'json'; // 设置 Content-Type
// 状态共享(跨中间件传递数据)
ctx.state.user = { id: 1, name: 'Alice' };
});
与 Express 的核心差异
| 方面 | Express | Koa |
|---|---|---|
| 响应方式 | res.send() / res.json() 直接发送 | ctx.body = xxx 赋值,框架统一发送 |
| 中间件执行 | next() 是回调,不返回 Promise | next() 返回 Promise,支持 await |
| 错误处理 | 错误中间件(4 参数) | try/catch 包裹 await next() |
| 上下文 | req 和 res 独立对象 | 统一 ctx 对象 |
| 内置功能 | 路由、模板引擎、静态文件 | 几乎为零,全靠插件 |
5. Fastify 核心
为什么快
Fastify 在多项基准测试中吞吐量是 Express 的 2-3 倍,原因在于:
| 优化点 | 实现方式 |
|---|---|
| 路由查找 | 基于 Radix Tree 的 find-my-way,查找复杂度 (k 为路径长度) |
| JSON 序列化 | 使用 fast-json-stringify 根据 JSON Schema 预编译序列化函数,比 JSON.stringify 快 2-5 倍 |
| Schema 编译 | 启动时编译 Schema 为验证/序列化函数,运行时无额外开销 |
| 请求解析 | 使用高效的 fast-content-type-parse |
| 日志 | 内置 pino——最快的 Node.js 日志库 |
JSON Schema 验证
import Fastify, { FastifyInstance } from 'fastify';
const app: FastifyInstance = Fastify();
// Schema 定义(同时用于验证和序列化)
const createUserSchema = {
body: {
type: 'object' as const,
required: ['name', 'email'],
properties: {
name: { type: 'string' as const, minLength: 2 },
email: { type: 'string' as const, format: 'email' },
age: { type: 'integer' as const, minimum: 0 },
},
},
response: {
201: {
type: 'object' as const,
properties: {
id: { type: 'integer' as const },
name: { type: 'string' as const },
email: { type: 'string' as const },
},
},
},
};
app.post('/users', { schema: createUserSchema }, async (request, reply) => {
const { name, email } = request.body as { name: string; email: string };
const user = { id: Date.now(), name, email };
reply.code(201);
return user; // 自动根据 response schema 序列化
});
Fastify 的 response schema 不只是文档——它会生成预编译的序列化函数,跳过不在 schema 中的字段。这既提升了性能,又防止了敏感数据泄露。
插件系统(Encapsulation)
Fastify 的插件系统支持封装隔离:子插件可以访问父级上下文,但父级无法访问子级注册的内容。
import Fastify, { FastifyInstance, FastifyPluginAsync } from 'fastify';
import fp from 'fastify-plugin';
// 插件定义
const dbPlugin: FastifyPluginAsync<{ uri: string }> = async (
fastify: FastifyInstance,
opts
) => {
const connection = await connectDB(opts.uri);
// 使用 decorate 挂载到 fastify 实例
fastify.decorate('db', connection);
// 关闭 Hook
fastify.addHook('onClose', async () => {
await connection.close();
});
};
// fp() 包装后可突破封装,使 db 在全局可用
const dbPluginExposed = fp(dbPlugin, { name: 'db-plugin' });
// 使用
const app = Fastify();
app.register(dbPluginExposed, { uri: 'mongodb://localhost/test' });
app.get('/users', async function (request, reply) {
const users = await this.db.collection('users').find().toArray();
return users;
});
Fastify 装饰器模式(decorate)
import Fastify from 'fastify';
const app = Fastify();
// 装饰 Fastify 实例
app.decorate('config', { port: 3000, env: 'production' });
// 装饰 Request
app.decorateRequest('user', null);
app.addHook('preHandler', async (request) => {
const token = request.headers.authorization;
if (token) {
request.user = await verifyToken(token);
}
});
// 装饰 Reply
app.decorateReply('success', function (data: unknown) {
return this.code(200).send({ success: true, data });
});
app.get('/profile', async (request, reply) => {
return reply.success(request.user);
});
6. NestJS 核心
NestJS 是目前最流行的 Node.js 企业级框架,GitHub Star 超过 70k。它深受 Angular 启发,使用装饰器 + 依赖注入构建应用。
核心架构
装饰器 + 依赖注入
import {
Module, Controller, Injectable, Get, Post, Body,
Param, HttpException, HttpStatus
} from '@nestjs/common';
// Service(可注入)
@Injectable()
class UserService {
private users = [{ id: 1, name: 'Alice' }];
findAll() {
return this.users;
}
findOne(id: number) {
const user = this.users.find(u => u.id === id);
if (!user) {
throw new HttpException('用户不存在', HttpStatus.NOT_FOUND);
}
return user;
}
create(dto: { name: string }) {
const user = { id: Date.now(), name: dto.name };
this.users.push(user);
return user;
}
}
// Controller
@Controller('users')
class UserController {
constructor(private readonly userService: UserService) {} // 依赖注入
@Get()
findAll() {
return this.userService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.userService.findOne(Number(id));
}
@Post()
create(@Body() dto: { name: string }) {
return this.userService.create(dto);
}
}
// Module
@Module({
controllers: [UserController],
providers: [UserService],
})
class UserModule {}
NestJS 管线组件详解
- Guard 守卫
- Interceptor 拦截器
- Pipe 管道
import {
Injectable, CanActivate, ExecutionContext,
SetMetadata, UseGuards
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
// 自定义装饰器标记角色
const Roles = (...roles: string[]) => SetMetadata('roles', roles);
@Injectable()
class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>(
'roles',
context.getHandler()
);
if (!requiredRoles) return true;
const request = context.switchToHttp().getRequest();
const user = request.user;
return requiredRoles.some(role => user?.roles?.includes(role));
}
}
// 使用
@Controller('admin')
@UseGuards(RolesGuard)
class AdminController {
@Get()
@Roles('admin')
getAdminData() {
return { secret: '管理员数据' };
}
}
import {
Injectable, NestInterceptor, ExecutionContext, CallHandler
} from '@nestjs/common';
import { Observable, map, tap } from 'rxjs';
// 统一响应格式拦截器
@Injectable()
class TransformInterceptor<T> implements NestInterceptor<T, { data: T }> {
intercept(
context: ExecutionContext,
next: CallHandler<T>
): Observable<{ data: T }> {
const start = Date.now();
return next.handle().pipe(
map(data => ({
code: 0,
data,
message: 'success',
})),
tap(() => {
console.log(`请求耗时: ${Date.now() - start}ms`);
})
);
}
}
import {
Injectable, PipeTransform, ArgumentMetadata,
BadRequestException
} from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
@Injectable()
class ValidationPipe implements PipeTransform {
async transform(value: unknown, { metatype }: ArgumentMetadata) {
if (!metatype) return value;
const object = plainToInstance(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
const messages = errors.map(e =>
Object.values(e.constraints || {}).join(', ')
);
throw new BadRequestException(messages);
}
return value;
}
}
NestJS 在构建大型企业应用时特别适合搭配 BFF 网关层架构。
7. Midway.js 核心
IoC 容器
Midway 基于自研的 @midwayjs/core IoC 容器,通过装饰器自动管理依赖关系:
import {
Controller, Get, Post, Body, Inject, Provide, Config
} from '@midwayjs/core';
// Service
@Provide()
class UserService {
@Config('database')
dbConfig: { host: string; port: number };
async findAll() {
// 使用注入的配置
return [{ id: 1, name: 'Alice' }];
}
}
// Controller
@Controller('/api/users')
class UserController {
@Inject()
userService: UserService; // 自动注入
@Get('/')
async list() {
return await this.userService.findAll();
}
@Post('/')
async create(@Body() body: { name: string }) {
return { id: Date.now(), name: body.name };
}
}
一体化方案
Midway 最大的特色是提供了 HTTP、WebSocket、RPC、定时任务、消息队列等一体化方案:
与 NestJS 的异同
| 方面 | NestJS | Midway.js |
|---|---|---|
| IoC 容器 | 自研,基于 Reflect Metadata | 自研 injection 库 |
| 装饰器风格 | Angular 风格 | 更接近 Spring / Java 风格 |
| 底层 HTTP | Express / Fastify 可选 | Koa / Express / Fastify 可选 |
| Serverless | 需额外适配 | 原生支持(@midwayjs/faas) |
| 一体化 | 按需安装 | 统一框架,开箱即用 |
| 生态 | 国际化,社区大 | 阿里生态,中文文档完善 |
| 适用场景 | 国际化团队、大型企业 | 阿里系、Serverless 场景 |
如果团队在阿里云生态中(FC、OSS、RocketMQ 等),Midway 的一体化方案和 Serverless 支持会更顺滑;如果面向国际化社区或需要更丰富的第三方库支持,NestJS 是更稳妥的选择。
8. 框架选型指南
按团队规模选择
| 团队规模 | 推荐框架 | 理由 |
|---|---|---|
| 个人 / 1-3 人 | Express / Koa / Fastify | 轻量灵活,快速上手 |
| 3-10 人 | Fastify / NestJS | 需要规范约束,又不能太重 |
| 10+ 人 | NestJS / Midway | 强约定、DI 架构、模块化分工 |
按项目类型选择
| 项目类型 | 推荐框架 | 理由 |
|---|---|---|
| REST API | Fastify / Express | 简单直接,性能好 |
| BFF 中间层 | NestJS / Midway | 模块化好,适合聚合逻辑 |
| 全栈应用 | NestJS | 搭配 Next.js / Nuxt 形成全栈 |
| 微服务 | NestJS / Midway | 内置微服务支持(gRPC、NATS 等) |
| Serverless | Midway | 原生 Serverless 适配 |
| 高并发 API | Fastify | 极致性能,Schema 驱动 |
| 快速原型 | Express | 生态丰富,资料最多 |
决策流程图
9. 性能对比
以下数据来自社区基准测试(fastify/benchmarks),使用 autocannon 压测工具,环境为 Node.js 20,仅供参考。实际性能因业务逻辑、中间件数量、数据库 IO 等因素差异很大。
基准测试(简单 JSON 响应)
| 框架 | 请求/秒 (req/s) | 延迟 (avg) | 相对性能 |
|---|---|---|---|
| Fastify | ~78,000 | 1.2ms | 100% (基准) |
| Koa | ~50,000 | 1.9ms | ~64% |
| Express | ~35,000 | 2.8ms | ~45% |
| NestJS (Fastify) | ~72,000 | 1.3ms | ~92% |
| NestJS (Express) | ~30,000 | 3.2ms | ~38% |
| Midway.js | ~45,000 | 2.1ms | ~58% |
各框架性能优化手段
| 框架 | 关键优化手段 |
|---|---|
| Express | 生产环境关闭 etag/x-powered-by、使用 compression、路由缓存 |
| Koa | 减少中间件数量、使用 koa-compress、缓存 compose 结果 |
| NestJS | 切换 Fastify 引擎、启用 CacheInterceptor、使用 LazyModuleLoader |
| Fastify | 定义 response schema、使用 fastify-compress、开启 trustProxy |
| Midway.js | 预加载 IoC 容器、减少动态注入、使用缓存中间件 |
NestJS 底层引擎从 Express 切换为 Fastify,只需改一行代码,性能提升约 2 倍:
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter()
);
await app.listen(3000);
}
bootstrap();
常见面试问题
Q1: Express 和 Koa 的中间件机制有什么区别?
答案:
核心区别在于执行模型和异步处理:
| 维度 | Express | Koa |
|---|---|---|
| 执行模型 | 线性(瀑布流) | 洋葱模型 |
| next() 返回值 | void(无返回值) | Promise<void> |
| 后置处理 | 需监听 res.on('finish') | await next() 后自然执行 |
| 错误处理 | 4 参数错误中间件 | try/catch 包裹 await next() |
Express 中 next() 调用后,控制权交给下一个中间件,但当前函数继续执行;Koa 中 await next() 会暂停当前中间件,等内层全部执行完后恢复。
// 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`); // 在响应完成后执行
});
更详细的中间件原理和代码实现,参考 Express 与 Koa。
Q2: 为什么 Fastify 比 Express 快?
答案:
Fastify 的性能优势来自多个底层优化:
1. JSON 序列化优化
Express 使用 JSON.stringify(),每次调用都需要遍历对象;Fastify 使用 fast-json-stringify,根据 JSON Schema 预编译序列化函数:
// Express - 运行时动态序列化
res.json({ id: 1, name: 'Alice', secret: 'xxx' });
// 每次调用 JSON.stringify(),遍历所有字段
// Fastify - 编译时生成序列化函数
// 启动时根据 schema 生成如下函数(伪代码):
function serialize(obj: { id: number; name: string }) {
return `{"id":${obj.id},"name":"${obj.name}"}`;
// 不会序列化 schema 外的字段(如 secret)
}
2. 路由查找优化
Express 使用线性数组匹配路由();Fastify 使用 find-my-way 基于 Radix Tree(前缀树)的路由查找(,k 为路径长度),路由越多差距越大。
3. 请求/响应处理
- 避免不必要的对象创建和原型链查找
- 使用高效的 Header 解析
- 内置高性能日志
pino(JSON 日志,无字符串拼接)
4. Schema 验证
使用 Ajv 预编译验证函数,避免运行时解释执行。
Q3: NestJS 和 Express/Koa 的本质区别是什么?
答案:
NestJS 与 Express/Koa 是不同层次的框架:
| 维度 | Express/Koa | NestJS |
|---|---|---|
| 层次 | HTTP 框架(处理请求/响应) | 应用框架(组织整个应用) |
| 架构 | 无约束,自由组织 | 强约定:Module + Controller + Service |
| 依赖管理 | 手动 require/import | IoC 容器自动注入 |
| TypeScript | 可选,需手动配置 | 原生支持,装饰器驱动 |
| 关注点 | 路由 + 中间件 | 完整的应用架构(认证、校验、ORM、消息队列...) |
NestJS 的本质是在 Express/Fastify 之上加了一层架构约束,通过 IoC/DI 和装饰器实现了关注点分离。它解决的不是「如何处理 HTTP 请求」,而是「如何组织一个可维护的大型应用」。
// Express - 自由但缺乏约束
const app = express();
app.get('/users', async (req, res) => {
const users = await db.query('SELECT * FROM users');
res.json(users); // 路由、业务、数据访问混在一起
});
// NestJS - 关注点分离
@Controller('users')
class UserController {
constructor(private userService: UserService) {} // 自动注入
@Get()
findAll() {
return this.userService.findAll(); // 只负责路由映射
}
}
Q4: 如何根据项目需求选择 Node.js 框架?
答案:
框架选型需要考虑四个维度:
1. 项目规模与复杂度
- 简单 API / 快速原型 → Express(上手最快,资料最多)
- 中等规模 API → Fastify(性能好,Schema 驱动)
- 大型企业应用 → NestJS / Midway(架构约束,团队协作)
2. 团队背景
- 前端团队首次写后端 → Express(门槛最低)
- 有 Angular / Java / Spring 经验 → NestJS(DI 模式熟悉)
- 阿里系技术栈 → Midway(生态融合)
3. 性能要求
- 高并发 API 网关 → Fastify
- NestJS 项目需要提升性能 → 切换 Fastify 引擎
- IO 密集型应用 → 框架差异不大,瓶颈在数据库
4. 运维环境
- 传统服务器 → 任意框架
- Serverless → Midway(原生支持)/ NestJS(需适配)
- 微服务 → NestJS(内置 gRPC、NATS、Kafka 支持)
对于中小型项目,Express + TypeScript 已经足够好用。不要因为「NestJS 更先进」就盲目上 NestJS——框架越重,理解和维护成本越高。能解决问题的最简单方案就是最好的方案。
Q5: Koa 洋葱模型的原理是什么?手写一个简单的 compose 函数
答案:
洋葱模型的核心是 koa-compose 函数。它将中间件数组转换为一个递归调用链——每个中间件通过 await next() 调用下一个,next() 返回的 Promise 在内层全部执行完后才 resolve。
type Context = Record<string, any>;
type Next = () => Promise<void>;
type Middleware = (ctx: Context, next: Next) => Promise<void>;
function compose(middlewares: Middleware[]) {
// 参数校验
if (!Array.isArray(middlewares)) {
throw new TypeError('middlewares must be an array');
}
for (const fn of middlewares) {
if (typeof fn !== 'function') {
throw new TypeError('middleware must be a function');
}
}
return function (ctx: Context, finalNext?: Next): Promise<void> {
let index = -1;
function dispatch(i: number): Promise<void> {
// 防止多次调用 next()
if (i <= index) {
return Promise.reject(new Error('next() called multiple times'));
}
index = i;
// 取出当前中间件,到达末尾则取 finalNext
const fn = i < middlewares.length ? middlewares[i] : finalNext;
if (!fn) return Promise.resolve();
try {
// 关键:将 dispatch(i+1) 作为 next 传入当前中间件
return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
return dispatch(0);
};
}
// 验证
async function test() {
const logs: string[] = [];
const m1: Middleware = async (ctx, next) => {
logs.push('m1-before');
await next();
logs.push('m1-after');
};
const m2: Middleware = async (ctx, next) => {
logs.push('m2-before');
await next();
logs.push('m2-after');
};
const m3: Middleware = async (ctx, next) => {
logs.push('m3');
};
const fn = compose([m1, m2, m3]);
await fn({});
console.log(logs);
// ['m1-before', 'm2-before', 'm3', 'm2-after', 'm1-after']
}
test();
执行过程详解
- 调用
dispatch(0),进入m1 m1执行logs.push('m1-before'),然后await next()next()即dispatch(1),进入m2m2执行logs.push('m2-before'),然后await next()next()即dispatch(2),进入m3m3执行logs.push('m3'),没有调用next()dispatch(2)返回的 Promise resolvem2的await next()恢复,执行logs.push('m2-after')dispatch(1)返回的 Promise resolvem1的await next()恢复,执行logs.push('m1-after')
这就是洋葱模型——请求从外到内穿过(before),响应从内到外穿出(after)。
Q6: Express 的错误处理中间件有什么特殊之处?如何正确处理异步错误?
答案:
Express 的错误处理中间件必须有 4 个参数 (err, req, res, next),少一个都不会被识别为错误处理器:
// 普通中间件:3 个参数
app.use((req, res, next) => { next(); });
// 错误处理中间件:4 个参数(必须全部声明)
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ message: err.message });
});
异步错误的坑:Express 4 不会自动捕获异步错误(Promise rejection),需要手动处理:
// ❌ Express 4: Promise rejection 不会被错误中间件捕获,直接崩溃
app.get('/users', async (req, res) => {
const users = await db.query('SELECT * FROM users'); // 如果抛错,进程崩溃
res.json(users);
});
// ✅ 方案1:try-catch 包裹
app.get('/users', async (req, res, next) => {
try {
const users = await db.query('SELECT * FROM users');
res.json(users);
} catch (err) {
next(err); // 手动传递给错误中间件
}
});
// ✅ 方案2:asyncHandler 高阶函数(推荐)
const asyncHandler = (fn: Function) =>
(req: Request, res: Response, next: NextFunction) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get('/users', asyncHandler(async (req, res) => {
const users = await db.query('SELECT * FROM users');
res.json(users);
}));
// ✅ 方案3:Express 5 自动捕获 async 错误(无需额外处理)
Express 5 原生支持 async 错误捕获,async 路由处理器中的 rejection 会自动传递给错误中间件。
Q7: Fastify 的插件封装模型是什么?有什么优势?
答案:
Fastify 的插件系统基于 Encapsulation(封装) 模型——每个插件都运行在自己的上下文中,插件注册的装饰器、钩子和路由默认对外部不可见:
import fp from 'fastify-plugin';
// 封装插件(默认行为):装饰器和钩子只对子级可见
async function dbPlugin(fastify: FastifyInstance) {
const db = await connectDB();
fastify.decorate('db', db);
}
// 全局插件:使用 fastify-plugin 打破封装,所有层级可见
export default fp(dbPlugin);
优势:避免全局污染、支持多数据库连接(每个插件连不同的库)、天然适合微服务拆分。
Q8: Koa 的 Context 和 Express 的 req/res 有什么区别?
答案:
| 维度 | Express (req + res) | Koa (ctx) |
|---|---|---|
| 对象模型 | 两个独立对象,原生 Node.js 对象的扩展 | 统一的 Context 对象,封装 req/res |
| 响应方式 | res.json() / res.send() | ctx.body = value(赋值即响应) |
| 状态码 | res.status(200).json() | ctx.status = 200 |
| 请求参数 | req.query / req.params / req.body | ctx.query / ctx.params / ctx.request.body |
| 类型安全 | 需要类型断言 | 支持泛型扩展 |
| 代理属性 | 无 | ctx.query 代理到 ctx.request.query |
// Express
app.get('/api/users', (req, res) => {
const page = Number(req.query.page) || 1;
res.status(200).json({ data: users, page });
});
// Koa - 更简洁的赋值式响应
app.use(async (ctx) => {
if (ctx.path === '/api/users' && ctx.method === 'GET') {
const page = Number(ctx.query.page) || 1;
ctx.status = 200;
ctx.body = { data: users, page }; // 赋值即响应
}
});
Q9: 如何手写一个简单的 Express 中间件系统?
答案:
Express 的中间件本质是一个函数数组的顺序调用:
type Req = Record<string, any>;
type Res = { json: (data: any) => void; statusCode: number };
type Next = (err?: Error) => void;
type Handler = (req: Req, res: Res, next: Next) => void;
type ErrorHandler = (err: Error, req: Req, res: Res, next: Next) => void;
class MiniExpress {
private middlewares: (Handler | ErrorHandler)[] = [];
use(fn: Handler | ErrorHandler) {
this.middlewares.push(fn);
return this;
}
handle(req: Req, res: Res) {
let index = 0;
const next: Next = (err?: Error) => {
const fn = this.middlewares[index++];
if (!fn) return;
// 4 参数 → 错误处理中间件,3 参数 → 普通中间件
if (err) {
if (fn.length === 4) (fn as ErrorHandler)(err, req, res, next);
else next(err); // 跳过普通中间件,继续找错误处理器
} else {
if (fn.length < 4) (fn as Handler)(req, res, next);
else next(); // 跳过错误处理器
}
};
next();
}
}
// 验证
const app = new MiniExpress();
app.use((req, res, next) => { console.log('middleware 1'); next(); });
app.use((req, res, next) => { throw new Error('oops'); });
app.use((err, req, res, next) => { console.log('caught:', err.message); });
Express 通过判断函数的参数个数(fn.length)来区分普通中间件(3参数)和错误处理中间件(4参数)。这是 JavaScript 中罕见的利用 Function.length 的设计。
Q10: Midway.js 和 NestJS 有什么区别?分别适合什么场景?
答案:
| 维度 | NestJS | Midway.js |
|---|---|---|
| 出品方 | Kamil Mysliwiec(独立开源) | 阿里巴巴 |
| IoC 容器 | 内置(reflect-metadata) | @midwayjs/core(injection 库) |
| 运行时 | 仅 Node.js 服务 | Node.js + Serverless + FaaS |
| 一体化 | HTTP 为主,微服务适配器 | HTTP + WebSocket + gRPC + Cron + Bull + FaaS 统一 |
| 装饰器风格 | Angular 风格(类 Spring) | 类似 NestJS,但 API 不同 |
| 生态 | 全球化社区,npm 下载量远超 | 国内社区为主,阿里系生态集成 |
| Serverless | 需适配(冷启动较慢) | 原生支持(极快冷启动) |
| TypeScript | 一等公民 | 一等公民 |
| 学习资源 | 英文为主,文档完善 | 中文为主,配套完整 |
选择建议:
- 国际化团队、主流技术栈 → NestJS(社区更大、生态更丰富)
- 阿里系项目、需要 Serverless → Midway.js(原生支持、中文文档)
- 已有 NestJS 经验 → 迁移到 Midway 成本低,概念相通
Q11: Fastify 的 JSON Schema 验证相比 Express 的 joi/yup 有什么优势?
答案:
| 维度 | Express + joi/yup | Fastify JSON Schema |
|---|---|---|
| 验证时机 | 运行时解释执行 | 启动时编译为验证函数 |
| 性能 | 每次请求都解析 Schema | 编译一次,后续直接执行 |
| 序列化 | JSON.stringify()(遍历所有字段) | fast-json-stringify(只序列化 Schema 声明的字段) |
| 标准 | 库特有 DSL | JSON Schema 标准(可复用给文档、前端等) |
| 自动文档 | 需额外配置 | Schema 直接生成 Swagger 文档 |
fastify.post('/users', {
schema: {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 2 },
email: { type: 'string', format: 'email' },
},
},
response: {
200: {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
// 注意:没声明 password,所以即使返回了也不会被序列化(防泄漏)
},
},
},
},
}, async (request) => {
return await createUser(request.body);
});
Fastify 的 response schema 会自动过滤掉未声明的字段。即使 Handler 返回了完整的用户对象(含 password),响应中也不会包含 password。这是 Schema 驱动开发的安全性加成。
Q12: Node.js 框架的性能瓶颈通常在哪里?
答案:
框架本身很少是瓶颈。真正的性能瓶颈通常在:
| 瓶颈层级 | 常见原因 | 优化方向 |
|---|---|---|
| IO 层 | 数据库慢查询、外部 API 超时 | 连接池、索引优化、缓存、并发请求 |
| 序列化 | 大对象 JSON.stringify | Schema 序列化(Fastify)、Stream 输出 |
| 中间件 | 不必要的中间件对所有路由生效 | 按路由挂载、减少全局中间件 |
| 计算 | CPU 密集型操作阻塞事件循环 | Worker Threads、子进程 |
| 内存 | 大数组/对象常驻内存 | Stream 处理、分页查询 |
| 连接 | 高并发下 TCP 连接耗尽 | Keep-Alive、连接池、负载均衡 |
// 空路由基准测试(只测框架开销)
app.get('/ping', (req, res) => res.json({ pong: true }));
// 如果空路由能达到 30000+ req/s(Express)或 60000+ req/s(Fastify)
// 那说明框架不是瓶颈,问题在业务代码
99% 的性能问题不在框架,而在数据库查询和外部 API 调用。优化时应该先用 APM 工具(如 clinic.js)定位真正的瓶颈,而不是盲目换框架。
Q13: Express 中间件的加载顺序为什么重要?
答案:
Express 中间件是严格按注册顺序执行的,顺序错误会导致严重的 Bug:
// ❌ 错误:路由在日志/CORS 之前,日志记录不到这个路由的请求
app.get('/api/data', handler);
app.use(morgan('dev'));
app.use(cors());
// ✅ 正确顺序
app.use(cors()); // 1. 跨域(最先)
app.use(helmet()); // 2. 安全头
app.use(morgan('dev')); // 3. 日志
app.use(express.json()); // 4. Body 解析
app.use(authMiddleware); // 5. 认证
app.use('/api', apiRouter); // 6. 业务路由
app.use(notFoundHandler); // 7. 404 处理
app.use(errorHandler); // 8. 错误处理(最后)
标准注册顺序:安全中间件 → 日志 → Body 解析 → 认证 → 路由 → 404 → 错误处理。
Q14: 如何从 Express 迁移到 NestJS?
答案:
渐进式迁移策略(不是一次性重写):
// 在 NestJS 中复用现有 Express 路由
import * as express from 'express';
import { existingRouter } from './legacy/routes';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 将旧 Express 路由挂载到 NestJS
const expressApp = app.getHttpAdapter().getInstance() as express.Application;
expressApp.use('/legacy', existingRouter);
await app.listen(3000);
}
迁移清单:
| 迁移项 | Express | NestJS |
|---|---|---|
| 路由 | app.get('/path', handler) | @Get('path') Controller 方法 |
| 中间件 | app.use(fn) | MiddlewareConsumer 或 app.use() |
| 认证 | passport 中间件 | @UseGuards(AuthGuard) |
| 参数校验 | joi/yup 手动校验 | class-validator + ValidationPipe |
| 错误处理 | 4 参数中间件 | @Catch() ExceptionFilter |
| 数据库 | 直接 query / sequelize | TypeORM / Prisma + Module 注入 |
Q15: 为什么说 Koa 比 Express"更现代"?Koa 有什么局限?
答案:
Koa 的现代之处:
- 原生 async/await:中间件全部基于 Promise,告别回调地狱
- 洋葱模型:请求和响应在同一个中间件中处理,代码更直观
- 更小更纯粹:核心只有 ~600 行代码,不捆绑路由/body-parser 等
- Context 封装:统一的
ctx对象替代分散的req/res - 更好的错误处理:
try/catch替代 4 参数错误中间件
Koa 的局限:
| 局限 | 说明 |
|---|---|
| 生态较小 | 路由、Body 解析等需要安装第三方包(koa-router, koa-body) |
| Express 中间件不兼容 | 基于回调的 Express 中间件不能直接在 Koa 中使用 |
| 社区活跃度下降 | 近年更新放缓,NestJS/Fastify 吸引了更多开发者 |
| 缺乏上层架构 | 和 Express 一样是 HTTP 层框架,大型项目仍需自行组织 |
| TypeScript 支持一般 | 类型定义不如 Fastify/NestJS 完善 |
Koa 的价值在于教育意义和轻量场景。理解 Koa 的洋葱模型和 compose 原理对理解 NestJS 的 Interceptor 和 Redux 的 middleware 都有帮助。但在生产环境,大型项目推荐 NestJS,追求性能推荐 Fastify。