Express.js 速查表
这是一份 Express.js 开发中常用的语法和配置速查表,涵盖了从基础设置到生产部署的核心知识点。
基础设置
Express 版本选择
Express 5.0 于 2024 年 10 月正式发布,带来了 Promise 支持、安全增强等重要改进:
# 安装 Express 5(推荐)
npm install express@5
# 安装 Express 4(稳定版)
npm install express@4
Express 5 环境要求:Node.js 18 或更高版本
Express 5 主要改进:
- 原生支持 Promise 错误处理,async 函数错误自动传递
- 移除正则路由,防止 ReDoS 攻击
- 路径匹配语法更新,通配符必须有名称
- API 签名统一,移除废弃方法
创建应用
Express 应用的起点是调用 express() 函数创建应用实例。这是所有 Express 应用的基础:
const express = require('express');
const app = express();
// 监听端口
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
内置中间件
Express 4.16.0 之后内置了几个常用的中间件,不再需要单独安装 body-parser:
// 解析 JSON 格式的请求体
// Content-Type: application/json
app.use(express.json());
// 解析 URL 编码的请求体(表单数据)
// Content-Type: application/x-www-form-urlencoded
app.use(express.urlencoded({ extended: true }));
// 提供静态文件服务
// 访问 http://localhost:3000/images/logo.png
app.use(express.static('public'));
// 静态文件可配置多个目录和虚拟路径
app.use('/static', express.static('public'));
express.urlencoded 的 extended 选项决定使用哪个库解析数据:true 使用 qs 库(支持嵌套对象),false 使用 querystring 库(不支持嵌套)。
应用配置
// 设置应用配置
app.set('view engine', 'ejs'); // 模板引擎
app.set('views', './views'); // 模板目录
app.set('trust proxy', true); // 信任反向代理
app.set('x-powered-by', false); // 隐藏 Express 标识
// 获取配置值
const engine = app.get('view engine');
// 布尔配置的快捷方法
app.enable('trust proxy'); // 等同于 app.set('trust proxy', true)
app.disable('x-powered-by'); // 等同于 app.set('x-powered-by', false)
路由
路由定义了应用如何响应客户端的特定请求。每个路由可以有一个或多个处理函数。
基本 HTTP 方法
Express 支持所有标准 HTTP 方法:
// GET 请求 - 获取资源
app.get('/users', (req, res) => {
res.json({ users: [] });
});
// POST 请求 - 创建资源
app.post('/users', (req, res) => {
const user = req.body;
res.status(201).json(user);
});
// PUT 请求 - 完整更新资源
app.put('/users/:id', (req, res) => {
res.json({ id: req.params.id, ...req.body });
});
// PATCH 请求 - 部分更新资源
app.patch('/users/:id', (req, res) => {
res.json({ id: req.params.id, updated: req.body });
});
// DELETE 请求 - 删除资源
app.delete('/users/:id', (req, res) => {
res.status(204).send();
});
// 匹配所有 HTTP 方法
app.all('/secret', (req, res, next) => {
console.log('访问秘密区域...');
next();
});
路由参数
路由参数用于捕获 URL 中的动态值,通过 : 定义:
// 单个参数
app.get('/users/:id', (req, res) => {
res.json({ userId: req.params.id });
});
// 多个参数
app.get('/users/:userId/posts/:postId', (req, res) => {
const { userId, postId } = req.params;
res.json({ userId, postId });
});
// 参数带正则验证(只匹配数字)
app.get('/products/:id(\\d+)', (req, res) => {
res.json({ productId: req.params.id });
});
// 可选参数
app.get('/books/:year?', (req, res) => {
const year = req.params.year || new Date().getFullYear();
res.json({ year });
});
查询字符串
查询字符串是 URL 中 ? 后面的部分,通过 req.query 访问:
// GET /search?q=express&page=2&limit=10
app.get('/search', (req, res) => {
const { q, page = 1, limit = 10 } = req.query;
res.json({ keyword: q, page, limit });
});
// 多个同名参数会变成数组
// GET /filter?tag=node&tag=express
app.get('/filter', (req, res) => {
// req.query.tag = ['node', 'express']
res.json({ tags: req.query.tag });
});
路由链式定义
app.route() 可以避免重复写相同路径:
app.route('/articles')
.get((req, res) => {
res.json({ action: '获取文章列表' });
})
.post((req, res) => {
res.status(201).json({ action: '创建文章' });
})
.put((req, res) => {
res.json({ action: '更新文章' });
})
.delete((req, res) => {
res.status(204).send();
});
路由模块化
使用 express.Router() 创建可复用的路由模块:
// routes/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({ users: [] });
});
router.post('/', (req, res) => {
res.status(201).json({ created: true });
});
module.exports = router;
// app.js
const usersRouter = require('./routes/users');
app.use('/api/users', usersRouter);
Router 配置选项:
const router = express.Router({
caseSensitive: true, // 区分大小写
mergeParams: true, // 保留父路由的 params
strict: true // 严格模式,/foo 和 /foo/ 不同
});
请求对象 (req)
req 对象代表 HTTP 请求,包含了客户端发送的所有信息。
常用属性
| 属性 | 说明 | 示例值 |
|---|---|---|
req.params | 路由参数 | { id: '123' } |
req.query | 查询字符串 | { page: '1' } |
req.body | 请求体(需中间件) | { name: 'John' } |
req.headers | 请求头对象 | { 'content-type': 'application/json' } |
req.method | HTTP 方法 | 'GET', 'POST' |
req.url | 完整请求路径 | '/users?page=1' |
req.path | 路径部分 | '/users' |
req.hostname | 主机名 | 'localhost' |
req.ip | 客户端 IP | '127.0.0.1' |
req.protocol | 协议 | 'http' |
req.secure | 是否 HTTPS | false |
req.xhr | 是否 AJAX | false |
常用方法
// 获取请求头(不区分大小写)
const contentType = req.get('Content-Type');
const auth = req.get('authorization');
// 检查 Accept 头
req.accepts('json'); // 'json' 或 false
req.accepts(['json', 'html']); // 返回匹配的类型
// 检查 Content-Type
req.is('json'); // 'json' 或 false
req.is('application/json'); // true 或 false
// 获取参数化的路由
const route = req.route; // 当前匹配的路由对象
文件上传信息
使用 multer 中间件后,可通过以下属性访问文件:
// 单文件
app.post('/upload', upload.single('file'), (req, res) => {
console.log(req.file);
// {
// fieldname: 'file',
// originalname: 'photo.jpg',
// mimetype: 'image/jpeg',
// size: 102400,
// path: 'uploads/abc123'
// }
});
// 多文件
app.post('/photos', upload.array('photos'), (req, res) => {
console.log(req.files); // 文件数组
});
响应对象 (res)
res 对象用于向客户端发送响应。
发送响应
// 发送文本(自动设置 Content-Type)
res.send('Hello World');
// 发送 JSON
res.json({ message: 'success', data: {} });
// 发送带状态码的响应
res.status(404).json({ error: 'Not Found' });
res.status(201).send('Created');
// 发送状态码和对应文本
res.sendStatus(200); // 发送 'OK'
res.sendStatus(404); // 发送 'Not Found'
// 发送空响应
res.end();
设置响应头
// 设置单个响应头
res.set('Content-Type', 'application/json');
// 设置多个响应头
res.set({
'Cache-Control': 'no-cache',
'X-Custom-Header': 'value'
});
// CORS 相关头
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
Cookie 操作
// 设置 Cookie
res.cookie('name', 'value', {
maxAge: 900000, // 有效期(毫秒)
httpOnly: true, // 仅服务器可访问
secure: true, // 仅 HTTPS
sameSite: 'strict', // CSRF 防护:strict | lax | none
domain: 'example.com', // 域名
path: '/' // 路径
});
// 清除 Cookie
res.clearCookie('name');
res.clearCookie('name', { path: '/admin' }); // 需要相同的 path/domain
重定向
// 重定向到相对路径(默认 302)
res.redirect('/login');
// 永久重定向(301)
res.redirect(301, '/new-url');
// 重定向到外部 URL
res.redirect('https://example.com');
// 返回上一页
res.redirect('back'); // 基于 Referer 头
文件操作
// 下载文件(提示保存对话框)
res.download('/files/report.pdf');
res.download('/files/report.pdf', '报告.pdf'); // 自定义文件名
// 发送文件(直接显示)
res.sendFile('/path/to/file.html');
res.sendFile('index.html', { root: './public' });
// 设置附件头(配合 fs.createReadStream)
res.attachment('data.csv');
res.send('col1,col2\nval1,val2');
模板渲染
// 先设置模板引擎
app.set('view engine', 'ejs');
// 渲染模板
res.render('index', {
title: '首页',
user: { name: 'John' }
});
中间件
中间件是 Express 的核心机制,每个中间件可以访问 req、res 和 next。
中间件结构
// 标准中间件签名
function middleware(req, res, next) {
// 1. 执行代码
console.log('请求处理中');
// 2. 修改 req 或 res
req.user = { id: 1 };
// 3. 调用 next() 或结束响应
next(); // 传递控制权
}
// 错误处理中间件(四个参数)
function errorHandler(err, req, res, next) {
res.status(500).json({ error: err.message });
}
应用级中间件
// 匹配所有请求
app.use((req, res, next) => {
console.log('请求到达');
next();
});
// 匹配特定路径前缀
app.use('/api', (req, res, next) => {
console.log('API 请求');
next();
});
// 路由中使用的中间件
app.get('/protected',
authMiddleware, // 第一个中间件
checkPermission, // 第二个中间件
(req, res) => { // 最终处理
res.json({ data: 'protected data' });
}
);
路由级中间件
const router = express.Router();
// 只对该路由器生效
router.use((req, res, next) => {
console.log('路由级中间件');
next();
});
router.get('/list', (req, res) => {
res.json({ list: [] });
});
错误处理
// 触发错误
app.get('/error', (req, res, next) => {
const err = new Error('出错了');
err.status = 400;
next(err);
});
// 错误处理中间件(必须放最后)
app.use((err, req, res, next) => {
const status = err.status || 500;
res.status(status).json({
error: {
message: err.message,
// 生产环境不暴露堆栈
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
}
});
});
Express 5 异步错误
Express 5 原生支持 Promise 错误处理,这是最重要的改进之一:
// Express 4.x - 必须手动捕获和传递错误
app.get('/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: '用户不存在' });
}
res.json(user);
} catch (err) {
next(err); // 必须手动传递
}
});
// Express 5.x - 错误自动传递
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
// 如果 findById 抛出错误,会自动传递给错误处理中间件
if (!user) {
throw new Error('用户不存在'); // 也会被自动捕获
}
res.json(user);
});
// 错误处理中间件(Express 4/5 通用)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production'
? '服务器错误'
: err.message
});
});
注意:成功完成操作时仍需手动调用 next() 或发送响应。
常用第三方中间件
安全中间件
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
// 设置安全相关的 HTTP 头
app.use(helmet());
// 跨域资源共享
app.use(cors({
origin: 'https://example.com',
methods: ['GET', 'POST'],
credentials: true
}));
// 请求限流
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 最多 100 次请求
message: { error: '请求过于频繁' }
});
app.use('/api', limiter);
日志中间件
const morgan = require('morgan');
// 开发环境:彩色输出
app.use(morgan('dev'));
// 生产环境:Apache 格式
app.use(morgan('combined'));
// 自定义格式
morgan.token('user-id', req => req.user?.id);
app.use(morgan(':method :url :status :user-id'));
Cookie 和 Session
const cookieParser = require('cookie-parser');
const session = require('express-session');
// Cookie 解析
app.use(cookieParser('secret-key'));
// Session
app.use(session({
secret: 'your-secret',
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // 仅 HTTPS
maxAge: 86400000 // 24 小时
}
}));
JWT 认证
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET;
// 生成 Token
function generateToken(user) {
return jwt.sign(
{ id: user.id, role: user.role },
SECRET,
{ expiresIn: '7d' }
);
}
// 验证 Token
function verifyToken(token) {
try {
return jwt.verify(token, SECRET);
} catch {
return null;
}
}
// 认证中间件
function auth(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: '未提供 Token' });
}
const decoded = verifyToken(token);
if (!decoded) {
return res.status(401).json({ error: 'Token 无效或已过期' });
}
req.user = decoded;
next();
}
// 使用
app.post('/login', (req, res) => {
const user = authenticate(req.body);
const token = generateToken(user);
res.json({ token });
});
app.get('/profile', auth, (req, res) => {
res.json({ user: req.user });
});
数据库操作 (Mongoose)
const mongoose = require('mongoose');
// 连接数据库
mongoose.connect(process.env.DATABASE_URL);
// 定义 Schema
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, unique: true },
age: { type: Number, min: 0 },
createdAt: { type: Date, default: Date.now }
});
// 创建模型
const User = mongoose.model('User', userSchema);
// CRUD 操作
// 查询所有
const users = await User.find();
// 条件查询
const user = await User.findOne({ email: '[email protected]' });
const user = await User.findById('507f1f77bcf86cd799439011');
// 创建
const newUser = await User.create({ name: 'John', email: '[email protected]' });
// 更新
await User.findByIdAndUpdate(id, { name: 'New Name' }, { new: true });
// 删除
await User.findByIdAndDelete(id);
// 计数
const count = await User.countDocuments({ age: { $gte: 18 } });
// 分页
const page = 1, limit = 10;
const results = await User.find()
.skip((page - 1) * limit)
.limit(limit);
文件上传 (Multer)
const multer = require('multer');
// 基本配置
const upload = multer({
dest: 'uploads/',
limits: {
fileSize: 5 * 1024 * 1024 // 5MB
},
fileFilter: (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png'];
if (!allowedTypes.includes(file.mimetype)) {
return cb(new Error('只允许上传图片'));
}
cb(null, true);
}
});
// 单文件上传
app.post('/avatar', upload.single('avatar'), (req, res) => {
res.json({ file: req.file });
});
// 多文件上传(同名字段)
app.post('/photos', upload.array('photos', 10), (req, res) => {
res.json({ count: req.files.length });
});
// 多字段上传
app.post('/profile', upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'documents', maxCount: 5 }
]), (req, res) => {
res.json({ files: req.files });
});
// 自定义存储
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({ storage });
测试 (Supertest)
const request = require('supertest');
const app = require('./app');
// 测试 GET 请求
test('GET /users 返回用户列表', async () => {
const res = await request(app)
.get('/users')
.expect('Content-Type', /json/)
.expect(200);
expect(Array.isArray(res.body)).toBe(true);
});
// 测试 POST 请求
test('POST /users 创建用户', async () => {
const res = await request(app)
.post('/users')
.send({ name: 'Test', email: '[email protected]' })
.expect(201);
expect(res.body.name).toBe('Test');
});
// 测试带认证的请求
test('GET /profile 需要认证', async () => {
const res = await request(app)
.get('/profile')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
// 测试文件上传
test('POST /upload 上传文件', async () => {
const res = await request(app)
.post('/upload')
.attach('file', __dirname + '/fixtures/test.jpg')
.expect(200);
});
进程管理 (PM2)
# 启动应用
pm2 start app.js
# 集群模式(利用多核)
pm2 start app.js -i max
# 常用命令
pm2 list # 查看所有进程
pm2 logs # 查看日志
pm2 monit # 监控面板
pm2 restart all # 重启所有
pm2 stop all # 停止所有
pm2 delete all # 删除所有
# 持久化
pm2 save # 保存当前进程列表
pm2 startup # 设置开机自启
# 配置文件 ecosystem.config.js
module.exports = {
apps: [{
name: 'my-app',
script: './app.js',
instances: 'max',
exec_mode: 'cluster',
env_production: {
NODE_ENV: 'production',
PORT: 3000
}
}]
};
pm2 start ecosystem.config.js --env production
Docker 部署
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=mongodb://mongo:27017/mydb
depends_on:
- mongo
mongo:
image: mongo:7
volumes:
- mongo_data:/data/db
volumes:
mongo_data:
# 构建镜像
docker build -t my-express-app .
# 运行容器
docker run -p 3000:3000 my-express-app
# Docker Compose
docker-compose up -d # 后台运行
docker-compose logs -f # 查看日志
docker-compose down # 停止并删除
环境变量
// 加载 .env 文件
require('dotenv').config();
// 常用环境变量
const config = {
port: process.env.PORT || 3000,
env: process.env.NODE_ENV || 'development',
database: {
url: process.env.DATABASE_URL,
name: process.env.DATABASE_NAME
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
}
};
# .env.example(提交到仓库)
NODE_ENV=development
PORT=3000
DATABASE_URL=mongodb://localhost:27017/mydb
JWT_SECRET=your-secret-key
# .env(不提交,添加到 .gitignore)
NODE_ENV=production
DATABASE_URL=mongodb://prod-server:27017/mydb
JWT_SECRET=your-production-secret
HTTP 状态码速查
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 2xx 成功 | ||
| 200 | OK | 一般成功响应 |
| 201 | Created | POST 创建资源成功 |
| 204 | No Content | DELETE/PUT 成功,无返回体 |
| 3xx 重定向 | ||
| 301 | Moved Permanently | 永久重定向 |
| 302 | Found | 临时重定向 |
| 304 | Not Modified | 缓存有效 |
| 4xx 客户端错误 | ||
| 400 | Bad Request | 参数错误、格式错误 |
| 401 | Unauthorized | 未登录、Token 无效 |
| 403 | Forbidden | 无权限 |
| 404 | Not Found | 资源不存在 |
| 409 | Conflict | 资源冲突(如重复创建) |
| 422 | Unprocessable Entity | 验证失败 |
| 429 | Too Many Requests | 请求过于频繁 |
| 5xx 服务器错误 | ||
| 500 | Internal Server Error | 服务器内部错误 |
| 502 | Bad Gateway | 上游服务错误 |
| 503 | Service Unavailable | 服务不可用 |
常见错误处理
// 404 处理
app.use((req, res) => {
res.status(404).json({ error: '路由不存在' });
});
// 全局错误处理
app.use((err, req, res, next) => {
console.error(err.stack);
// 处理特定错误类型
if (err.name === 'ValidationError') {
return res.status(400).json({ error: err.message });
}
if (err.name === 'UnauthorizedError') {
return res.status(401).json({ error: '认证失败' });
}
if (err.name === 'SyntaxError') {
return res.status(400).json({ error: 'JSON 格式错误' });
}
// 默认 500 错误
res.status(500).json({
error: process.env.NODE_ENV === 'production'
? '服务器错误'
: err.message
});
});
最佳实践清单
- 安全:使用 helmet、验证输入、参数化查询、HTTPS
- 性能:启用压缩、静态文件缓存、数据库连接池
- 日志:请求日志、错误日志、结构化日志格式
- 错误处理:全局错误中间件、友好的错误消息
- 测试:单元测试、集成测试、测试覆盖率
- 文档:API 文档(Swagger/OpenAPI)、代码注释
- 监控:健康检查端点、性能监控、告警机制
Express 5 迁移速查
自动迁移工具
# 运行所有 codemods
npx codemod@latest @expressjs/v5-migration-recipe
破坏性变化速查表
| Express 4.x | Express 5.x | 说明 |
|---|---|---|
app.del() | app.delete() | 方法名变更 |
req.param('name') | req.params/body/query.name | 明确来源 |
res.json(obj, status) | res.status(status).json(obj) | 状态码顺序 |
res.send(status) | res.sendStatus(status) | 发送状态文本 |
res.redirect('back') | res.redirect(req.get('Referrer') || '/') | 移除魔法字符串 |
res.sendfile() | res.sendFile() | 驼峰命名 |
req.acceptsCharset() | req.acceptsCharsets() | 方法名复数化 |
/* | /*splat 或 /{*splat} | 通配符必须有名称 |
/:file.:ext? | /:file{.:ext} | 可选参数新语法 |
路由参数变化
// Express 4.x - 通配符
app.get('/*', handler);
// Express 5.x - 通配符必须有名称
app.get('/*splat', handler); // 不匹配根路径
app.get('/{*splat}', handler); // 匹配所有路径包括根路径
// Express 4.x - 正则路由
app.get('/user/:id(\\d+)', handler);
// Express 5.x - 使用验证中间件
app.get('/user/:id',
param('id').isInt(),
handler
);
请求体变化
// Express 4.x - req.body 默认为 {}
console.log(req.body); // {}
// Express 5.x - req.body 默认为 undefined
console.log(req.body); // undefined
// 安全访问
const data = req.body || {};
const { name = '' } = req.body || {};
完整迁移指南
详细的迁移说明和代码示例,请参阅 Express 5.x 新特性与迁移。