Node.js + Express 面试题
生成时间:2026-06-06 21:17 | 共 10 题
1. Node.js 事件循环的执行顺序是什么?
难度:⭐⭐⭐
标准答案:
Node.js 事件循环分为 6 个阶段,按顺序循环执行:
- timers:执行
setTimeout/setInterval到期的回调 - pending callbacks:执行系统级回调(如 TCP 错误回调)
- idle, prepare:内部使用(仅 libuv 使用)
- poll:获取新 I/O 事件,执行 I/O 相关回调(这是最核心的阶段)
- check:执行
setImmediate回调 - close callbacks:执行
socket.on('close', ...)等关闭回调
每个阶段都有一个 FIFO 队列,当前阶段队列清空或达到回调上限后进入下一阶段。在两个阶段之间,Node.js 会检查是否有 process.nextTick 或微任务(Promise.then),如果有则优先全部执行完。
关键区分:process.nextTick 属于微任务但优先级高于 Promise 微任务;setImmediate 在 check 阶段执行,而 setTimeout(fn, 0) 在 timers 阶段执行——在非 I/O 上下文中二者顺序不确定,但在 I/O 回调中 setImmediate 总是先执行。
延伸思考: 为什么 process.nextTick 的回调不属于事件循环的任何一个阶段?它的设计意图是什么?如果 nextTick 回调中递归调用 nextTick 会怎样?
2. Node.js 的异步 I/O 是如何实现的?为什么说它是"非阻塞"的?
难度:⭐⭐
标准答案:
Node.js 的异步 I/O 基于 libuv 事件驱动库实现,核心架构为:
- 单线程事件循环:JavaScript 代码在主线程执行,I/O 操作不阻塞主线程
- 线程池:libuv 维护默认 4 个线程(可通过
UV_THREADPOOL_SIZE调整),处理文件 I/O、DNS 查找等没有内核异步 API 支持的操作 - 内核异步 I/O:网络 I/O(epoll/kqueue/IOCP)由操作系统内核直接通知完成,不占用线程池
工作流程:
- JavaScript 发起 I/O 请求 → 封装请求对象,推入线程池/交给内核
- 主线程继续执行后续代码(不等待结果)
- I/O 完成后,内核/线程池通知 libuv → 将回调放入事件循环对应队列
- 事件循环在 poll 阶段取出回调执行
"非阻塞"指的是:I/O 操作发起后主线程不等待,立即返回继续执行其他代码。阻塞发生在工作线程或内核中,对 JavaScript 主线程不可见。
延伸思考: 如果所有文件 I/O 都走线程池,那 UV_THREADPOOL_SIZE=4 是否会成为瓶颈?在高并发文件操作场景下如何优化?
3. CommonJS 与 ES Modules 的区别是什么?Node.js 如何处理两者的兼容?
难度:⭐⭐
标准答案:
| 特性 | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| 语法 | require() / module.exports | import / export |
| 加载方式 | 同步,运行时加载 | 异步,编译时静态分析 |
| 输出类型 | 值的拷贝(引用的浅拷贝) | 值的绑定(live binding) |
顶层 this | 指向 module.exports | undefined |
| 循环依赖 | 返回已执行部分的快照 | 引用绑定但可能获取未初始化值 |
| Tree-shaking | 不支持(动态加载) | 支持(静态分析) |
Node.js 兼容处理:
.mjs文件自动作为 ESM,.cjs自动作为 CJSpackage.json中"type": "module"使.js默认为 ESM- CJS 中可用
import()动态导入 ESM 模块 - ESM 中不能直接
require(),但可通过createRequire桥接
延伸思考: 在大型项目中,从 CJS 迁移到 ESM 的主要痛点有哪些?双模式发布(Dual Package)如何正确处理?
4. Stream 和 Buffer 在 Node.js 中的作用是什么?如何避免内存泄漏?
难度:⭐⭐⭐
标准答案:
Buffer:Node.js 中处理二进制数据的类,类似定长的字节数组。用于在 TCP 流、文件 I/O 等场景中处理原始二进制数据。常用方法:Buffer.from()、Buffer.alloc()、Buffer.concat()。
Stream:流式数据处理抽象,四种类型:
- Readable:可读流(如
fs.createReadStream) - Writable:可写流(如
fs.createWriteStream) - Duplex:双工流(可读可写,如 TCP Socket)
- Transform:转换流(读入→变换→写出,如
zlib.createGzip)
两种流模式:
- Flowing 模式:自动读取数据,通过
data事件消费 - Paused 模式:需手动调用
read()拉取数据
避免内存泄漏的方法:
- 始终处理 error 事件:未监听 error 会触发 uncaughtException
- 使用 pipeline 替代 pipe:
stream.pipeline()自动处理错误传播和流清理 - 及时销毁流:
stream.destroy()释放底层资源 - 避免积压(backpressure):写速度 > 读速度时用
drain事件控制节奏 - Transform 流中必须调用 callback:忘记回调会导致流挂起
// ✅ 推荐
const { pipeline } = require('stream/promises');
await pipeline(
fs.createReadStream('input.txt'),
zlib.createGzip(),
fs.createWriteStream('output.gz')
);
// ❌ 不推荐(错误处理不完善)
fs.createReadStream('input.txt')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('output.gz'));延伸思考: highWaterMark 参数如何影响流的性能?在高吞吐场景中如何调优?Readable 流的背压机制具体是如何与 Writable 流交互的?
5. Express 中间件的执行原理是什么?如何编写一个可配置的中间件?
难度:⭐⭐
标准答案:
Express 中间件本质是一个函数 (req, res, next) => {},核心执行原理基于洋葱模型:
- Express 内部维护一个中间件栈(数组),按
app.use()/app.METHOD()注册顺序排列 - 请求进来后,从栈顶开始依次执行
- 每个中间件调用
next()将控制权传递给下一个中间件 next('route')跳过当前路由剩余中间件next(err)跳过后续普通中间件,直接进入错误处理中间件(4 参数函数)
中间件分类:
- 应用级中间件:
app.use()/app.METHOD() - 路由级中间件:
router.use()/router.METHOD() - 错误处理中间件:
(err, req, res, next) => {} - 内置中间件:
express.json()/express.static()等 - 第三方中间件:
cors/helmet/morgan等
可配置中间件示例:
function rateLimit(options = {}) {
const {
windowMs = 60_000,
max = 100,
message = 'Too many requests'
} = options;
const hits = new Map();
return (req, res, next) => {
const key = req.ip;
const now = Date.now();
const record = hits.get(key);
if (!record || now - record.startTime > windowMs) {
hits.set(key, { count: 1, startTime: now });
return next();
}
if (record.count >= max) {
return res.status(429).json({ error: message });
}
record.count++;
next();
};
}
// 使用
app.use(rateLimit({ windowMs: 60_000, max: 50 }));延伸思考: Express 中间件与 Koa 的洋葱模型有什么本质区别?为什么 Express 的 next() 是回调式而 Koa 是 await 式?各自有什么优劣?
6. Express 的路由机制是如何实现的?如何设计 RESTful API 路由?
难度:⭐⭐
标准答案:
Express 路由核心由 Layer 和 Route 两个类实现:
- Layer:封装一个中间件 + 路径匹配规则。每个
app.use()/app.METHOD()都创建一个 Layer - Route:一个 Layer 可以包含一个 Route,Route 内部再维护自己的 Layer 栈(处理同路径的不同 HTTP 方法)
路径匹配:使用 path-to-regexp 库将路由字符串转为正则表达式,支持:
- 命名参数:
/users/:id→req.params.id - 可选参数:
/users/:id? - 通配符:
/files/*//files/:path* - 正则约束:
/users/:id(\\d+) - 自定义正则:
/user/:id<\\d+>
RESTful 路由设计:
const router = express.Router();
// 资源命名用复数名词,用 HTTP 方法区分操作
router.get('/users', listUsers); // 列表
router.post('/users', createUser); // 创建
router.get('/users/:id', getUser); // 详情
router.put('/users/:id', updateUser); // 全量更新
router.patch('/users/:id', patchUser); // 部分更新
router.delete('/users/:id', deleteUser); // 删除
// 嵌套资源
router.get('/users/:id/posts', listUserPosts);
router.post('/users/:id/posts', createUserPost);
// 版本控制
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);最佳实践:
- 路由拆分到独立文件,用
express.Router()组织 - 使用
router.route()链式定义同路径多方法 - 参数校验用
express-validator或Joi - 统一响应格式
延伸思考: 如何实现路由级别的权限控制?在微服务架构中,Express 路由层与 API Gateway 的职责如何划分?
7. Express 的错误处理最佳实践是什么?如何实现全局异常捕获?
难度:⭐⭐
标准答案:
Express 错误处理分三层:
1. 同步错误:自动被 Express 捕获,传递给错误处理中间件
app.get('/sync-error', (req, res) => {
throw new Error('同步错误'); // ✅ 自动捕获
});2. 异步错误:必须手动传递给 next()
app.get('/async-error', async (req, res, next) => {
try {
await riskyOperation();
res.json({ ok: true });
} catch (err) {
next(err); // ✅ 必须手动 next(err)
}
});3. 全局错误处理中间件(4 个参数,必须最后注册):
app.use((err, req, res, next) => {
console.error(err.stack);
// 区分业务错误和系统错误
if (err instanceof AppError) {
return res.status(err.statusCode).json({
code: err.code,
message: err.message
});
}
// 未知错误,不暴露内部信息
res.status(500).json({
code: 'INTERNAL_ERROR',
message: '服务内部错误'
});
});4. 进程级异常兜底:
// 未捕获的 Promise 异常
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
// 记录日志,优雅关闭
});
// 未捕获的同步异常(最后防线)
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
process.exit(1); // 必须退出,进程状态不可信
});5. 推荐使用 express-async-errors 自动包装异步路由:
require('express-async-errors'); // 在入口文件顶部引入
// 之后 async 路由中 throw 的错误会被自动传递给 next()延伸思考: uncaughtException 后为什么必须退出进程?如何在生产环境中实现优雅降级和自动重启?
8. Node.js + Express 性能优化的核心策略有哪些?
难度:⭐⭐⭐
标准答案:
应用层优化:
- 中间件精简:移除不必要的中间件,按需加载(如
morgan仅开发环境) - 压缩响应:
compression中间件启用 gzip/brotli - 静态资源:
express.static设置缓存头,或用 CDN + Nginx 托管 - ETag 缓存:合理配置
etag策略减少传输
数据层优化:
- 连接池:数据库使用连接池(如
pg-pool、mysql2/promise) - Redis 缓存:热点数据缓存,减少 DB 查询
- 查询优化:避免 N+1 查询,使用批量查询和索引
并发优化:
- Cluster 模式:利用多核 CPU
const cluster = require('cluster');
const os = require('os');
if (cluster.isPrimary) {
const cpus = os.cpus().length;
for (let i = 0; i < cpus; i++) {
cluster.fork();
}
cluster.on('exit', (worker) => cluster.fork()); // 自动重启
} else {
require('./app'); // 启动 Express
}- PM2 进程管理:
pm2 start app.js -i max自动 cluster + 负载均衡 + 守护 - Worker Threads:CPU 密集任务用工作线程,避免阻塞事件循环
监控与诊断:
- 事件循环延迟监控:
await monitorEventLoopDelay()检测阻塞 - APM 工具:New Relic / Datadog / 阿里云 ARMS 性能监控
- 火焰图:
0x/clinic生成 CPU 火焰图定位热点 - 内存泄漏检测:
heapdump+ Chrome DevTools 对比堆快照
延伸思考: 在 Serverless 环境下(如 AWS Lambda),Node.js 的性能优化策略有何不同?冷启动问题如何解决?
9. Node.js + Express 的生产部署策略是什么?
难度:⭐⭐
标准答案:
部署架构:
客户端 → CDN/Nginx → PM2 Cluster → Express 应用 → Redis/DB
↘ 健康检查 & 负载均衡核心策略:
进程管理:PM2(推荐)或 systemd
- Cluster 模式充分利用多核
- 自动重启(崩溃 / 内存超限)
- 零停机重载:
pm2 reload app
反向代理:Nginx
- SSL 终结,减轻 Node.js 加密负担
- 静态资源托管
- 限流、IP 黑白名单
- 请求头转发(
X-Forwarded-For等,需设置app.set('trust proxy', 1))
Docker 容器化:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]- 环境变量管理:
dotenv+ 环境变量注入,禁止硬编码 - 日志管理:
winston/pino结构化日志 + ELK / Loki 集中收集 - 健康检查:
app.get('/health', (req, res) => {
const dbOk = await checkDB();
const redisOk = await checkRedis();
res.status(dbOk && redisOk ? 200 : 503).json({
status: dbOk && redisOk ? 'healthy' : 'degraded',
uptime: process.uptime(),
memory: process.memoryUsage()
});
});- CI/CD 流水线:GitHub Actions / GitLab CI → 构建 → 测试 → 部署
- 滚动更新:蓝绿部署或金丝雀发布,确保零停机
延伸思考: Kubernetes 环境下 Node.js 应用的 liveness vs readiness probe 如何区分设计?HPA 自动扩缩容的指标选择有哪些考虑?
10. 如何在 Express 项目中集成 TypeScript?有哪些最佳实践?
难度:⭐⭐
标准答案:
项目搭建:
mkdir app && cd app
npm init -y
npm install express
npm install -D typescript @types/express @types/node tsx
npx tsc --inittsconfig.json 关键配置:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}类型安全的请求/响应:
import { Request, Response, NextFunction, Router } from 'express';
// 自定义请求类型(扩展 Express Request)
interface CreateUserRequest extends Request {
body: {
name: string;
email: string;
age?: number;
};
}
// 类型安全的控制器
const createUser = async (
req: CreateUserRequest,
res: Response,
next: NextFunction
) => {
try {
const { name, email } = req.body; // ✅ 有类型提示和校验
const user = await UserService.create({ name, email });
res.status(201).json(user);
} catch (err) {
next(err);
}
};
// 中间件类型
const authMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
// ...
next();
};最佳实践:
- 开发用
tsx:tsx watch src/index.ts实现热重载 - 生产编译:
tsc编译后用node dist/index.js运行,不用 ts-node - 类型校验中间件:用
zod+ 中间件替代手动校验
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().min(0).optional()
});
function validate(schema: z.ZodSchema) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
req.body = result.data; // 替换为校验后的数据
next();
};
}
router.post('/users', validate(CreateUserSchema), createUser);- 错误类类型化:
class AppError extends Error {
constructor(
public statusCode: number,
public code: string,
message: string
) {
super(message);
}
}- 依赖注入:用
tsyringe或inversify实现IoC,方便单元测试 - 路径别名:
tsconfig.json的paths+tsc-alias解决编译后路径问题
延伸思考: 在 Express + TypeScript 项目中,如何实现自动生成 OpenAPI/Swagger 文档?tsoa vs swagger-jsdoc 的选型考虑?
End of Node.js + Express 面试题