Skip to content

Node.js + Express 面试题 ​

生成时间:2026-06-06 21:17 | 共 10 题


1. Node.js 事件循环的执行顺序是什么? ​

难度:⭐⭐⭐

标准答案:

Node.js 事件循环分为 6 个阶段,按顺序循环执行:

  1. timers:执行 setTimeout / setInterval 到期的回调
  2. pending callbacks:执行系统级回调(如 TCP 错误回调)
  3. idle, prepare:内部使用(仅 libuv 使用)
  4. poll:获取新 I/O 事件,执行 I/O 相关回调(这是最核心的阶段)
  5. check:执行 setImmediate 回调
  6. 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)由操作系统内核直接通知完成,不占用线程池

工作流程:

  1. JavaScript 发起 I/O 请求 → 封装请求对象,推入线程池/交给内核
  2. 主线程继续执行后续代码(不等待结果)
  3. I/O 完成后,内核/线程池通知 libuv → 将回调放入事件循环对应队列
  4. 事件循环在 poll 阶段取出回调执行

"非阻塞"指的是:I/O 操作发起后主线程不等待,立即返回继续执行其他代码。阻塞发生在工作线程或内核中,对 JavaScript 主线程不可见。

延伸思考: 如果所有文件 I/O 都走线程池,那 UV_THREADPOOL_SIZE=4 是否会成为瓶颈?在高并发文件操作场景下如何优化?


3. CommonJS 与 ES Modules 的区别是什么?Node.js 如何处理两者的兼容? ​

难度:⭐⭐

标准答案:

特性CommonJS (CJS)ES Modules (ESM)
语法require() / module.exportsimport / export
加载方式同步,运行时加载异步,编译时静态分析
输出类型值的拷贝(引用的浅拷贝)值的绑定(live binding)
顶层 this指向 module.exportsundefined
循环依赖返回已执行部分的快照引用绑定但可能获取未初始化值
Tree-shaking不支持(动态加载)支持(静态分析)

Node.js 兼容处理:

  • .mjs 文件自动作为 ESM,.cjs 自动作为 CJS
  • package.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() 拉取数据

避免内存泄漏的方法:

  1. 始终处理 error 事件:未监听 error 会触发 uncaughtException
  2. 使用 pipeline 替代 pipe:stream.pipeline() 自动处理错误传播和流清理
  3. 及时销毁流:stream.destroy() 释放底层资源
  4. 避免积压(backpressure):写速度 > 读速度时用 drain 事件控制节奏
  5. Transform 流中必须调用 callback:忘记回调会导致流挂起
javascript
// ✅ 推荐
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) => {},核心执行原理基于洋葱模型:

  1. Express 内部维护一个中间件栈(数组),按 app.use() / app.METHOD() 注册顺序排列
  2. 请求进来后,从栈顶开始依次执行
  3. 每个中间件调用 next() 将控制权传递给下一个中间件
  4. next('route') 跳过当前路由剩余中间件
  5. next(err) 跳过后续普通中间件,直接进入错误处理中间件(4 参数函数)

中间件分类:

  • 应用级中间件:app.use() / app.METHOD()
  • 路由级中间件:router.use() / router.METHOD()
  • 错误处理中间件:(err, req, res, next) => {}
  • 内置中间件:express.json() / express.static() 等
  • 第三方中间件:cors / helmet / morgan 等

可配置中间件示例:

javascript
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 两个类实现:

  1. Layer:封装一个中间件 + 路径匹配规则。每个 app.use() / app.METHOD() 都创建一个 Layer
  2. 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 路由设计:

javascript
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 捕获,传递给错误处理中间件

javascript
app.get('/sync-error', (req, res) => {
  throw new Error('同步错误'); // ✅ 自动捕获
});

2. 异步错误:必须手动传递给 next()

javascript
app.get('/async-error', async (req, res, next) => {
  try {
    await riskyOperation();
    res.json({ ok: true });
  } catch (err) {
    next(err); // ✅ 必须手动 next(err)
  }
});

3. 全局错误处理中间件(4 个参数,必须最后注册):

javascript
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. 进程级异常兜底:

javascript
// 未捕获的 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 自动包装异步路由:

javascript
require('express-async-errors'); // 在入口文件顶部引入
// 之后 async 路由中 throw 的错误会被自动传递给 next()

延伸思考: uncaughtException 后为什么必须退出进程?如何在生产环境中实现优雅降级和自动重启?


8. Node.js + Express 性能优化的核心策略有哪些? ​

难度:⭐⭐⭐

标准答案:

应用层优化:

  1. 中间件精简:移除不必要的中间件,按需加载(如 morgan 仅开发环境)
  2. 压缩响应:compression 中间件启用 gzip/brotli
  3. 静态资源:express.static 设置缓存头,或用 CDN + Nginx 托管
  4. ETag 缓存:合理配置 etag 策略减少传输

数据层优化:

  1. 连接池:数据库使用连接池(如 pg-pool、mysql2/promise)
  2. Redis 缓存:热点数据缓存,减少 DB 查询
  3. 查询优化:避免 N+1 查询,使用批量查询和索引

并发优化:

  1. Cluster 模式:利用多核 CPU
javascript
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
}
  1. PM2 进程管理:pm2 start app.js -i max 自动 cluster + 负载均衡 + 守护
  2. Worker Threads:CPU 密集任务用工作线程,避免阻塞事件循环

监控与诊断:

  1. 事件循环延迟监控:await monitorEventLoopDelay() 检测阻塞
  2. APM 工具:New Relic / Datadog / 阿里云 ARMS 性能监控
  3. 火焰图:0x / clinic 生成 CPU 火焰图定位热点
  4. 内存泄漏检测:heapdump + Chrome DevTools 对比堆快照

延伸思考: 在 Serverless 环境下(如 AWS Lambda),Node.js 的性能优化策略有何不同?冷启动问题如何解决?


9. Node.js + Express 的生产部署策略是什么? ​

难度:⭐⭐

标准答案:

部署架构:

客户端 → CDN/Nginx → PM2 Cluster → Express 应用 → Redis/DB
                    ↘ 健康检查 & 负载均衡

核心策略:

  1. 进程管理:PM2(推荐)或 systemd

    • Cluster 模式充分利用多核
    • 自动重启(崩溃 / 内存超限)
    • 零停机重载:pm2 reload app
  2. 反向代理:Nginx

    • SSL 终结,减轻 Node.js 加密负担
    • 静态资源托管
    • 限流、IP 黑白名单
    • 请求头转发(X-Forwarded-For 等,需设置 app.set('trust proxy', 1))
  3. Docker 容器化:

dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
  1. 环境变量管理:dotenv + 环境变量注入,禁止硬编码
  2. 日志管理:winston / pino 结构化日志 + ELK / Loki 集中收集
  3. 健康检查:
javascript
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()
  });
});
  1. CI/CD 流水线:GitHub Actions / GitLab CI → 构建 → 测试 → 部署
  2. 滚动更新:蓝绿部署或金丝雀发布,确保零停机

延伸思考: Kubernetes 环境下 Node.js 应用的 liveness vs readiness probe 如何区分设计?HPA 自动扩缩容的指标选择有哪些考虑?


10. 如何在 Express 项目中集成 TypeScript?有哪些最佳实践? ​

难度:⭐⭐

标准答案:

项目搭建:

bash
mkdir app && cd app
npm init -y
npm install express
npm install -D typescript @types/express @types/node tsx
npx tsc --init

tsconfig.json 关键配置:

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"]
}

类型安全的请求/响应:

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

最佳实践:

  1. 开发用 tsx:tsx watch src/index.ts 实现热重载
  2. 生产编译:tsc 编译后用 node dist/index.js 运行,不用 ts-node
  3. 类型校验中间件:用 zod + 中间件替代手动校验
typescript
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);
  1. 错误类类型化:
typescript
class AppError extends Error {
  constructor(
    public statusCode: number,
    public code: string,
    message: string
  ) {
    super(message);
  }
}
  1. 依赖注入:用 tsyringe 或 inversify 实现IoC,方便单元测试
  2. 路径别名:tsconfig.json 的 paths + tsc-alias 解决编译后路径问题

延伸思考: 在 Express + TypeScript 项目中,如何实现自动生成 OpenAPI/Swagger 文档?tsoa vs swagger-jsdoc 的选型考虑?


End of Node.js + Express 面试题