跳到主要内容

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、定时任务等一体化方案。

对比总览

特性ExpressKoaNestJSFastifyMidway.js
定位通用 Web 框架极简中间件框架企业级全功能框架高性能 Web 框架企业级一体化框架
首发年份20102013201720162018
中间件模型线性(next 回调)洋葱模型管线模型Hook 生命周期洋葱 + 装饰器
TypeScript需手动配置需手动配置原生支持良好支持原生支持
学习曲线中高
性能一般一般取决于底层引擎极高中等
生态极其丰富较丰富丰富且自成体系成长中阿里生态
适用场景快速原型、中小项目轻量 API、中间件定制大型企业项目高性能 APIServerless、阿里系

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-middleware.ts
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' });
});
面试要点

Express 的 next() 是同步回调,调用后当前函数继续执行但无法确保在响应之后;Koa 的 next() 返回 Promise,await next() 确保内层全部完成后才执行后续代码。这是两者最根本的区别。更详细的对比可参考 Express 与 Koa


3. Express 核心

信息

Express 是 Node.js 生态的事实标准,npm 周下载量超 3000 万。即使选择 NestJS,底层默认也是 Express。

路由系统

express-router.ts
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 错误处理

express-error-handling.ts
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,它将所有中间件组合成一个递归调用链:

koa-compose-simplified.ts
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 将 reqres 封装到统一的 ctx 对象中,提供了更优雅的 API:

koa-context.ts
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 的核心差异

方面ExpressKoa
响应方式res.send() / res.json() 直接发送ctx.body = xxx 赋值,框架统一发送
中间件执行next() 是回调,不返回 Promisenext() 返回 Promise,支持 await
错误处理错误中间件(4 参数)try/catch 包裹 await next()
上下文reqres 独立对象统一 ctx 对象
内置功能路由、模板引擎、静态文件几乎为零,全靠插件

5. Fastify 核心

为什么快

Fastify 在多项基准测试中吞吐量是 Express 的 2-3 倍,原因在于:

优化点实现方式
路由查找基于 Radix Tree 的 find-my-way,查找复杂度 O(k)O(k)(k 为路径长度)
JSON 序列化使用 fast-json-stringify 根据 JSON Schema 预编译序列化函数,比 JSON.stringify 快 2-5 倍
Schema 编译启动时编译 Schema 为验证/序列化函数,运行时无额外开销
请求解析使用高效的 fast-content-type-parse
日志内置 pino——最快的 Node.js 日志库

JSON Schema 验证

fastify-schema.ts
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 的插件系统支持封装隔离:子插件可以访问父级上下文,但父级无法访问子级注册的内容。

fastify-plugin.ts
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)

fastify-decorate.ts
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 启发,使用装饰器 + 依赖注入构建应用。

核心架构

装饰器 + 依赖注入

nestjs-example.ts
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 管线组件详解

auth.guard.ts
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: '管理员数据' };
}
}

NestJS 在构建大型企业应用时特别适合搭配 BFF 网关层架构


7. Midway.js 核心

IoC 容器

Midway 基于自研的 @midwayjs/core IoC 容器,通过装饰器自动管理依赖关系:

midway-example.ts
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 的异同

方面NestJSMidway.js
IoC 容器自研,基于 Reflect Metadata自研 injection
装饰器风格Angular 风格更接近 Spring / Java 风格
底层 HTTPExpress / 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 APIFastify / Express简单直接,性能好
BFF 中间层NestJS / Midway模块化好,适合聚合逻辑
全栈应用NestJS搭配 Next.js / Nuxt 形成全栈
微服务NestJS / Midway内置微服务支持(gRPC、NATS 等)
ServerlessMidway原生 Serverless 适配
高并发 APIFastify极致性能,Schema 驱动
快速原型Express生态丰富,资料最多

决策流程图


9. 性能对比

数据说明

以下数据来自社区基准测试(fastify/benchmarks),使用 autocannon 压测工具,环境为 Node.js 20,仅供参考。实际性能因业务逻辑、中间件数量、数据库 IO 等因素差异很大。

基准测试(简单 JSON 响应)

框架请求/秒 (req/s)延迟 (avg)相对性能
Fastify~78,0001.2ms100% (基准)
Koa~50,0001.9ms~64%
Express~35,0002.8ms~45%
NestJS (Fastify)~72,0001.3ms~92%
NestJS (Express)~30,0003.2ms~38%
Midway.js~45,0002.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 性能提升

NestJS 底层引擎从 Express 切换为 Fastify,只需改一行代码,性能提升约 2 倍

main.ts
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 的中间件机制有什么区别?

答案

核心区别在于执行模型异步处理

维度ExpressKoa
执行模型线性(瀑布流)洋葱模型
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 使用线性数组匹配路由(O(n)O(n));Fastify 使用 find-my-way 基于 Radix Tree(前缀树)的路由查找(O(k)O(k),k 为路径长度),路由越多差距越大。

3. 请求/响应处理

  • 避免不必要的对象创建和原型链查找
  • 使用高效的 Header 解析
  • 内置高性能日志 pino(JSON 日志,无字符串拼接)

4. Schema 验证

使用 Ajv 预编译验证函数,避免运行时解释执行。


Q3: NestJS 和 Express/Koa 的本质区别是什么?

答案

NestJS 与 Express/Koa 是不同层次的框架:

维度Express/KoaNestJS
层次HTTP 框架(处理请求/响应)应用框架(组织整个应用)
架构无约束,自由组织强约定:Module + Controller + Service
依赖管理手动 require/importIoC 容器自动注入
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。

手写 compose
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();
执行过程详解
  1. 调用 dispatch(0),进入 m1
  2. m1 执行 logs.push('m1-before'),然后 await next()
  3. next()dispatch(1),进入 m2
  4. m2 执行 logs.push('m2-before'),然后 await next()
  5. next()dispatch(2),进入 m3
  6. m3 执行 logs.push('m3'),没有调用 next()
  7. dispatch(2) 返回的 Promise resolve
  8. m2await next() 恢复,执行 logs.push('m2-after')
  9. dispatch(1) 返回的 Promise resolve
  10. m1await next() 恢复,执行 logs.push('m1-after')

这就是洋葱模型——请求从外到内穿过(before),响应从内到外穿出(after)。


Q6: Express 的错误处理中间件有什么特殊之处?如何正确处理异步错误?

答案

Express 的错误处理中间件必须有 4 个参数 (err, req, res, next),少一个都不会被识别为错误处理器:

Express 错误处理
// 普通中间件: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

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.bodyctx.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 的中间件本质是一个函数数组的顺序调用

手写 mini-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 有什么区别?分别适合什么场景?

答案

维度NestJSMidway.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/yupFastify JSON Schema
验证时机运行时解释执行启动时编译为验证函数
性能每次请求都解析 Schema编译一次,后续直接执行
序列化JSON.stringify()(遍历所有字段)fast-json-stringify(只序列化 Schema 声明的字段)
标准库特有 DSLJSON Schema 标准(可复用给文档、前端等)
自动文档需额外配置Schema 直接生成 Swagger 文档
Fastify Schema 验证 + 序列化 + 文档三合一
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.stringifySchema 序列化(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
// 在 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);
}

迁移清单

迁移项ExpressNestJS
路由app.get('/path', handler)@Get('path') Controller 方法
中间件app.use(fn)MiddlewareConsumerapp.use()
认证passport 中间件@UseGuards(AuthGuard)
参数校验joi/yup 手动校验class-validator + ValidationPipe
错误处理4 参数中间件@Catch() ExceptionFilter
数据库直接 query / sequelizeTypeORM / Prisma + Module 注入

Q15: 为什么说 Koa 比 Express"更现代"?Koa 有什么局限?

答案

Koa 的现代之处

  1. 原生 async/await:中间件全部基于 Promise,告别回调地狱
  2. 洋葱模型:请求和响应在同一个中间件中处理,代码更直观
  3. 更小更纯粹:核心只有 ~600 行代码,不捆绑路由/body-parser 等
  4. Context 封装:统一的 ctx 对象替代分散的 req/res
  5. 更好的错误处理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。


相关链接