Node.js: Express 进阶

最后更新:2026-08-26

1. 1 Bob 的生产危机

Bob 的 API 上线第一天就出了问题:用户提交了乱码邮箱,数据库报错整站崩溃;有人上传了 500MB 的图片,磁盘瞬间爆满;竞争对手写了脚本每秒发 1000 个请求,服务器直接 502。

一条 app.use(errorHandler) 就能挡住 80% 的生产事故。

TEXT 📖 仅展示
Bob 的排雷时间线:
Day 1  📧 无效邮箱 → 数据库异常 → 500 错误
Day 2  🖼️ 500MB 上传 → 磁盘满 → 服务宕机
Day 3  🤖 1000 req/s → CPU 100% → 502 Bad Gateway
Day 4  🔒 加中间件 → 逐个击破 → 服务稳定


2. 2 错误处理中间件

(1) 四参数签名机制

Express 通过参数数量识别错误中间件——必须是 4 个参数 (err, req, res, next),少一个就变成普通中间件。

▶ 示例:全局错误处理中间件

JAVASCRIPT
const express = require('express');
const app = express();

app.get('/boom', (req, res, next) => {
  try {
    throw new Error('故意炸了');
  } catch (err) {
    next(err);
  }
});

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    code: err.status || 500,
    data: null,
    message: err.message
  });
});

app.listen(3000);
▶ 试一试

(2) 自定义业务错误类

▶ 示例:业务错误与 HTTP 错误分离

JAVASCRIPT
class AppError extends Error {
  constructor(message, status) {
    super(message);
    this.status = status;
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource} 未找到`, 404);
  }
}

class ValidationError extends AppError {
  constructor(message) {
    super(message, 400);
  }
}

app.get('/users/:id', (req, res, next) => {
  const user = findUser(req.params.id);
  if (!user) return next(new NotFoundError('用户'));
  res.json({ code: 0, data: user, message: 'ok' });
});
▶ 试一试

(3) 错误处理方式对比

方式 参数个数 捕获范围 适用场景 异步支持
app.use(errHandler) 4 全局所有错误 最后兜底 需手动 next(err)
try/catch + next(err) 单个路由 同步代码 仅同步
express-async-errors 0 全局异步错误 async/await 路由 自动
domain 模块(已废弃) 进程级 不推荐
process.on('uncaughtException') 进程级 最后防线 全局
Promise .catch() 单个 Promise 单个异步 单个


3. 3 express-validator 请求验证

(1) 验证链与中间件用法

express-validator 基于 validator.js,用验证链(validation chain)声明式定义规则,验证失败自动收集错误。

▶ 示例:用户注册验证

JAVASCRIPT
const { body, validationResult } = require('express-validator');

app.post('/register',
  body('username')
    .isLength({ min: 3, max: 20 }).withMessage('用户名需3-20字符')
    .isAlphanumeric().withMessage('用户名仅允许字母数字'),
  body('email')
    .isEmail().withMessage('邮箱格式无效')
    .normalizeEmail(),
  body('password')
    .isLength({ min: 8 }).withMessage('密码至少8位')
    .matches(/\d/).withMessage('密码需包含数字')
    .matches(/[A-Z]/).withMessage('密码需包含大写字母'),
  body('age')
    .optional()
    .isInt({ min: 1, max: 150 }).withMessage('年龄需1-150'),
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        code: 400,
        data: errors.array(),
        message: '请求验证失败'
      });
    }
    next();
  },
  (req, res) => {
    res.json({ code: 0, data: req.body, message: 'ok' });
  }
);
▶ 试一试

(2) express-validator 常用验证器

验证器 用途 示例 配套修饰符
isEmail() 邮箱 body('email').isEmail() .normalizeEmail()
isLength() 长度 body('name').isLength({min:2,max:50})
isInt() / isFloat() 数字 query('page').isInt({min:1}) .toInt()
isBoolean() 布尔 body('active').isBoolean() .toBoolean()
isDate() 日期 body('birthday').isDate() .toDate()
isURL() URL body('website').isURL()
isIn() 枚举 body('role').isIn(['admin','user'])
matches() 正则 body('code').matches(/^\d{6}$/)
isMongoId() ObjectId param('id').isMongoId()
optional() 可选 body('nickname').optional().isLength({max:30}) {nullable:true}

(3) express-validator 与 Joi 对比

对比维度 express-validator Joi
集成方式 Express 原生中间件 独立验证库,需手动集成
语法风格 链式调用,逐字段定义 Schema 对象,一次性定义
底层依赖 validator.js 自实现
错误收集 validationResult() 自动收集 validate().error 手动处理
适用场景 Express 项目快速集成 任意 Node.js 项目
学习曲线 低(若熟悉 validator.js) 中(独立 Schema 语法)
类型转换 .toInt() .toDate() 自动类型转换
社区规模 周下载 ~1.5M 周下载 ~5M


4. 4 文件上传 multer

(1) 三种存储策略

multer 提供 singlearrayfields 三种上传模式,存储方式可选 memoryStorage(内存)和 diskStorage(磁盘)。

▶ 示例:磁盘存储 + 文件过滤

JAVASCRIPT
const multer = require('multer');
const path = require('path');

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, 'uploads/');
  },
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname);
    const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`;
    cb(null, uniqueName);
  }
});

const fileFilter = (req, file, cb) => {
  const allowed = /\.(jpg|jpeg|png|gif|webp)$/i;
  if (allowed.test(path.extname(file.originalname))) {
    cb(null, true);
  } else {
    cb(new Error('仅支持 jpg/png/gif/webp 格式'), false);
  }
};

const upload = multer({
  storage,
  fileFilter,
  limits: { fileSize: 5 * 1024 * 1024 }
});

app.post('/avatar', upload.single('avatar'), (req, res) => {
  if (!req.file) return res.status(400).json({ code: 400, data: null, message: '请上传文件' });
  res.json({
    code: 0,
    data: { url: `/uploads/${req.file.filename}`, size: req.file.size },
    message: 'ok'
  });
});

app.post('/photos', upload.array('photos', 9), (req, res) => {
  const urls = req.files.map(f => `/uploads/${f.filename}`);
  res.json({ code: 0, data: urls, message: 'ok' });
});
▶ 试一试

(2) multer 配置选项对比

配置项 类型 默认值 说明 示例
storage StorageEngine memoryStorage 存储引擎 multer.diskStorage({...})
dest string 目标目录(与 storage 二选一) 'uploads/'
fileFilter Function 全部允许 文件过滤回调 (req,file,cb)=>{...}
limits.fileSize number 无限制 单文件最大字节数 5*1024*1024
limits.files number 无限制 多文件上传最大数量 9
limits.fields number 无限制 非文件字段最大数量 10
limits.fieldSize number 1MB 非文件字段最大字节数 1024*100
limits.parts number 无限制 multipart 总 part 数 20

(3) memoryStorage vs diskStorage

对比维度 memoryStorage diskStorage
存储位置 内存(Buffer) 磁盘文件
req.file 属性 buffer path, filename
适用场景 小文件、即时处理(如图片压缩后转存) 大文件、持久化存储
性能 快(无磁盘 I/O) 稍慢(需写磁盘)
内存风险 大文件或并发高时 OOM
重启丢失
文件名控制 不需要 需自定义 filename 回调


5. 5 静态文件服务配置

(1) express.static 详解

▶ 示例:多目录静态服务 + 缓存控制

JAVASCRIPT
const express = require('express');
const app = express();

app.use('/static', express.static('public', {
  maxAge: '7d',
  etag: true,
  lastModified: true,
  immutable: true,
  setHeaders: (res, filePath) => {
    if (filePath.endsWith('.html')) {
      res.setHeader('Cache-Control', 'no-cache');
    }
    if (filePath.match(/\.(jpg|png|gif|webp|svg)$/)) {
      res.setHeader('Cache-Control', 'public, max-age=2592000, immutable');
    }
  }
}));

app.use('/uploads', express.static('uploads', {
  maxAge: '30d',
  dotfiles: 'deny'
}));
▶ 试一试

(2) 静态服务安全注意事项



6. 6 统一响应格式

(1) {code, data, message} 规范

▶ 示例:响应封装中间件

JAVASCRIPT
const responseHandler = (req, res, next) => {
  res.success = (data = null, message = 'ok') => {
    res.json({ code: 0, data, message });
  };
  res.fail = (message = '操作失败', code = -1, data = null) => {
    res.json({ code, data, message });
  };
  res.paginate = (list, total, page, pageSize) => {
    res.json({
      code: 0,
      data: { list, total, page, pageSize, totalPages: Math.ceil(total / pageSize) },
      message: 'ok'
    });
  };
  next();
};

app.use(responseHandler);

app.get('/users', (req, res) => {
  const users = getUserList();
  res.success(users);
});

app.get('/users/:id', (req, res, next) => {
  const user = findUser(req.params.id);
  if (!user) return res.fail('用户不存在', 404);
  res.success(user);
});
▶ 试一试

(2) 国际化 6 铁律



7. 7 rate-limit 限流

(1) express-rate-limit 配置

▶ 示例:分级限流策略

JAVASCRIPT
const rateLimit = require('express-rate-limit');

const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false,
  message: { code: 429, data: null, message: '请求过于频繁,请稍后再试' }
});

const apiLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 30,
  message: { code: 429, data: null, message: 'API 调用超过限制' }
});

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  skipSuccessfulRequests: true,
  message: { code: 429, data: null, message: '登录失败次数过多,请15分钟后重试' }
});

app.use(globalLimiter);
app.use('/api/', apiLimiter);
app.use('/auth/login', loginLimiter);
▶ 试一试

(2) 安全中间件列表

中间件 用途 默认行为 关键配置 npm 包
helmet HTTP 安全头 设置 15 个安全头 contentSecurityPolicy, hsts helmet
express-rate-limit 限流 windowMs, max express-rate-limit
cors 跨域控制 拒绝所有跨域 origin, methods, credentials cors
express-validator 输入验证 body(), query(), param() express-validator
multer 文件上传 limits, fileFilter multer
express-mongo-sanitize NoSQL 注入 移除 $. express-mongo-sanitize
xss-clean XSS 清洗 转义 HTML xss-clean
hpp 参数污染 取最后值 whitelist hpp
compression gzip 压缩 threshold, level compression


8. 8 环境配置 dotenv

(1) dotenv 基础用法

▶ 示例:环境变量分层加载

JAVASCRIPT
const dotenv = require('dotenv');
const path = require('path');

dotenv.config({ path: path.resolve(process.env.NODE_ENV ? `.env.${process.env.NODE_ENV}` : '.env') });

const config = {
  port: parseInt(process.env.PORT, 10) || 3000,
  env: process.env.NODE_ENV || 'development',
  db: {
    host: process.env.DB_HOST || 'localhost',
    port: parseInt(process.env.DB_PORT, 10) || 27017,
    name: process.env.DB_NAME || 'myapp_dev'
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '7d'
  },
  upload: {
    maxFileSize: parseInt(process.env.MAX_FILE_SIZE, 10) || 5 * 1024 * 1024,
    allowedTypes: (process.env.ALLOWED_TYPES || 'jpg,jpeg,png,gif,webp').split(',')
  },
  rateLimit: {
    windowMs: parseInt(process.env.RATE_WINDOW_MS, 10) || 15 * 60 * 1000,
    max: parseInt(process.env.RATE_MAX, 10) || 100
  }
};

module.exports = config;
▶ 试一试

▶ 示例:(2) .env 文件最佳实践

TEXT 📖 仅展示
# .env              ← 默认(开发),不提交到 Git
# .env.production   ← 生产环境,严格限制访问
# .env.test         ← 测试环境

PORT=3000
NODE_ENV=development

DB_HOST=localhost
DB_PORT=27017
DB_NAME=myapp_dev

JWT_SECRET=your-super-secret-key-change-in-production
JWT_EXPIRES_IN=7d

MAX_FILE_SIZE=5242880
ALLOWED_TYPES=jpg,jpeg,png,gif,webp

RATE_WINDOW_MS=900000
RATE_MAX=100
TEXT 📖 仅展示
# .gitignore 必须包含
.env
.env.*
!.env.example


9. 9 Express 请求处理完整流程

▶ 示例:(1) Mermaid 流程图

100%
flowchart TD
    A[客户端请求] --> B[helmet 安全头]
    B --> C[cors 跨域检查]
    C --> D[rate-limit 限流检查]
    D -->|429| E[返回限流响应]
    D -->|通过| F[express.json 解析体]
    F --> G[multer 文件上传处理]
    G -->|文件过大/格式错误| H[next error]
    G -->|通过| I[express-validator 验证]
    I -->|验证失败| J[返回 400 验证错误]
    I -->|通过| K[业务路由处理]
    K -->|业务错误| L[next error]
    K -->|成功| M[统一响应封装 success/fail]
    M --> N[返回 JSON 响应]
    L --> O[全局错误处理中间件]
    H --> O
    O --> P[统一错误响应 code/data/message]
    P --> N

    style E fill:#f66,stroke:#333,color:#fff
    style J fill:#f66,stroke:#333,color:#fff
    style P fill:#f66,stroke:#333,color:#fff
    style N fill:#6f6,stroke:#333

(2) 中间件加载顺序原则



10. 10 综合示例:完整 Express API 安全配置

▶ 示例:生产级 Express 服务器

JAVASCRIPT 📖 仅展示
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const multer = require('multer');
const { body, param, validationResult } = require('express-validator');
const compression = require('compression');
const path = require('path');

const config = require('./config');

const app = express();

app.use(helmet());
app.use(cors({ origin: config.corsOrigin, credentials: true }));
app.use(compression({ threshold: 1024 }));
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true }));

const globalLimiter = rateLimit({
  windowMs: config.rateLimit.windowMs,
  max: config.rateLimit.max,
  standardHeaders: true,
  legacyHeaders: false,
  message: { code: 429, data: null, message: 'error.rate_limited' }
});
app.use(globalLimiter);

const upload = multer({
  storage: multer.diskStorage({
    destination: 'uploads/',
    filename: (req, file, cb) => {
      const ext = path.extname(file.originalname);
      cb(null, `${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`);
    }
  }),
  fileFilter: (req, file, cb) => {
    const ext = path.extname(file.originalname).toLowerCase();
    if (config.upload.allowedTypes.some(t => `.${t}` === ext)) {
      cb(null, true);
    } else {
      cb(new Error('error.invalid_file_type'), false);
    }
  },
  limits: { fileSize: config.upload.maxFileSize, files: 5 }
});

const responseHandler = (req, res, next) => {
  res.success = (data = null, message = 'ok') => {
    res.json({ code: 0, data, message });
  };
  res.fail = (message = 'error.internal', code = -1, data = null) => {
    res.json({ code, data, message });
  };
  next();
};
app.use(responseHandler);

const validate = (rules) => [
  ...rules,
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        code: 400,
        data: errors.array().map(e => ({ field: e.path, message: e.msg })),
        message: 'error.validation_failed'
      });
    }
    next();
  }
];

app.post('/api/users',
  validate([
    body('username').isLength({ min: 3, max: 20 }).withMessage('error.username_length'),
    body('email').isEmail().withMessage('error.invalid_email').normalizeEmail(),
    body('password').isLength({ min: 8 }).withMessage('error.password_length')
  ]),
  (req, res) => {
    const user = createUser(req.body);
    res.success(user, 'ok');
  }
);

app.post('/api/upload',
  upload.array('files', 5),
  (req, res) => {
    if (!req.files || req.files.length === 0) {
      return res.fail('error.no_file_uploaded', 400);
    }
    const urls = req.files.map(f => `/uploads/${f.filename}`);
    res.success(urls, 'ok');
  }
);

app.get('/api/users/:id',
  validate([param('id').isMongoId().withMessage('error.invalid_id')]),
  (req, res) => {
    const user = findUser(req.params.id);
    if (!user) return res.fail('error.user_not_found', 404);
    res.success(user);
  }
);

app.use('/uploads', express.static('uploads', { maxAge: '30d', dotfiles: 'deny' }));

app.use((req, res) => {
  res.status(404).json({ code: 404, data: null, message: 'error.not_found' });
});

class AppError extends Error {
  constructor(message, status) {
    super(message);
    this.status = status;
    this.isOperational = true;
  }
}

app.use((err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    if (err.code === 'LIMIT_FILE_SIZE') {
      return res.status(413).json({ code: 413, data: null, message: 'error.file_too_large' });
    }
    return res.status(400).json({ code: 400, data: null, message: 'error.upload_failed' });
  }
  const status = err.status || 500;
  const message = err.isOperational ? err.message : 'error.internal';
  if (config.env === 'development') console.error(err.stack);
  res.status(status).json({ code: status, data: null, message });
});

app.listen(config.port, () => {
  console.log(`Server running on port ${config.port} [${config.env}]`);
});
逻辑代码 120 行(超过 40 行限制,仅展示)

11. 11 本课小结


❓ 常见问题

Q 中间件和路由哪个先执行?
A 按注册顺序执行。app.use 注册的全局中间件先于路由中间件,路由内中间件按路由定义顺序。
Q 如何实现 API 版本控制?
A 常用三种方式:URL 路径(/v1/users)、请求头(Accept: application/vnd.api.v1+json)、查询参数(?version=1)。
Q 错误处理中间件必须 4 个参数吗?
A 是的。Express 通过参数个数识别错误处理中间件,必须签名为 (err, req, res, next),否则会被当作普通中间件。
Q 如何做请求限流?
A 使用 express-rate-limit 中间件,设置 windowMs 时间窗口和 max 最大请求数,超限返回 429 状态码。
Q 静态文件服务性能如何优化?
A 生产环境用 Nginx 或 CDN 托管静态文件,Express 只处理动态 API;开发环境可用 express.static 的 maxAge 缓存。

📖 小节

📝 作业

  1. 为现有 Express 项目添加全局错误处理中间件,区分业务错误(isOperational)和未知错误,未知错误返回通用提示不暴露堆栈。
  2. 用 express-validator 为用户注册接口编写完整验证链(用户名、邮箱、密码强度、确认密码一致),验证失败返回字段级错误数组。
  3. 配置 multer 实现头像上传(单文件、限 2MB、仅 jpg/png),上传成功返回文件 URL,上传失败返回具体原因。
  4. 添加 express-rate-limit 分级限流:全局 15 分钟 100 次、API 1 分钟 30 次、登录 15 分钟 5 次失败。
  5. 创建 .env.example 文件列出所有环境变量及默认值,确保 .env 已加入 .gitignore

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏