跳到主要内容

Node.js 主流框架对比

问题​

Node.js 有哪些主流框架?它们各自有什么特点和适用场景?在实际项目中如何做技术选型?

面试速答版

Node.js 有哪些主流框架? 按设计理念分四档:

  • 极简派:Express(2010,回调 + 线性中间件,生态最大)、Koa(2013,async/await + 洋葱模型,核心 ~550 行)。
  • 企业级:NestJS(Angular 风格、装饰器 + IoC,TS 原生)、Midway.js(阿里系,IoC + Serverless 友好)。
  • 高性能:Fastify(Radix Tree 路由 + JSON Schema 序列化,吞吐量约是 Express 的 2-3 倍)。
  • 国内还有 Egg.js(阿里出品,约定大于配置),但新项目大多迁向 Midway。

它们各自的特点和适用场景? 挑框架本质是「自由度 vs 约束力」的取舍:

  • Express/Koa:API 极少,怎么组织全靠你,适合小型 API、原型、微服务或团队基建已经成熟的场景。
  • NestJS:强约束(Module/Controller/Provider 三件套)+ 依赖注入,适合中大型企业项目、需要长期维护和团队协作的系统,但学习曲线陡。
  • Fastify:性能压榨场景,比如 BFF、网关、高并发 REST API;插件系统优雅,但生态小于 Express。
  • Midway:阿里云 Serverless 优先选它,一套代码可部署到函数计算和传统服务器。

实际项目如何做技术选型? 按团队 + 场景三步评估:

  • 团队:TS 熟练度高、能接受装饰器→ NestJS;偏 JS、追求简单→ Express/Koa。
  • 规模:长期维护的中大型业务用 NestJS;脚手架/工具/边缘服务用 Express/Fastify。
  • 性能:QPS 不敏感选生态最丰富的;要求极致吞吐选 Fastify 或 NestJS + Fastify Adapter。
  • 一旦选定,重点是把分层(Controller/Service/Repository)和工程规范沉淀下来,框架本身反而不是瓶颈。

答案​

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 将 req 和 res 封装到统一的 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()
上下文req 和 res 独立对象统一 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. m2 的 await next() 恢复,执行 logs.push('m2-after')
  9. dispatch(1) 返回的 Promise resolve
  10. m1 的 await 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)MiddlewareConsumer 或 app.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。


相关链接​