一、核心实操一:全局错误处理 ------ 统一捕获异常,简化错误处理
上一课中,我们通过try/catch捕获接口异常,但分散在每个接口中,代码冗余且易遗漏。Express 支持全局错误处理中间件,可集中捕获所有未处理的异常,统一返回错误响应,提升代码可维护性。
1. 创建全局错误处理中间件(middleware/errorHandler.js)
javascript
运行
const logger = require('../config/logger');
const { errorResponse } = require('./response');
// 全局错误处理中间件(必须接收err, req, res, next四个参数)
const errorHandler = (err, req, res, next) => {
// 记录错误日志(包含请求信息,便于排查)
logger.error(`[全局错误] 请求路径:${req.method} ${req.originalUrl} | 错误信息:${err.message} | 堆栈信息:${err.stack}`);
// 区分错误类型,返回对应状态码
if (err.name === 'ValidationError') {
// Mongoose数据验证错误(如模型字段必填、格式错误)
return errorResponse(res, err.message, 400);
} else if (err.name === 'JsonWebTokenError') {
// JWT令牌错误(如无效令牌、令牌过期)
return errorResponse(res, '令牌无效,请重新登录', 401);
} else if (err.name === 'TokenExpiredError') {
// JWT令牌过期错误
return errorResponse(res, '令牌已过期,请重新登录', 401);
} else if (err.statusCode) {
// 自定义错误(携带状态码)
return errorResponse(res, err.message, err.statusCode);
} else {
// 未知错误,返回500服务器错误
return errorResponse(res, '服务器内部错误', 500);
}
};
module.exports = errorHandler;
2. 在入口文件中注册全局错误处理中间件(app.js)
javascript
运行
const express = require('express');
const cors = require('cors');
const connectDB = require('./config/db');
const { successResponse } = require('./middleware/response');
const logger = require('./config/logger');
const path = require('path');
const setupSwagger = require('./config/swagger');
const errorHandler = require('./middleware/errorHandler'); // 引入全局错误处理
require('dotenv').config();
connectDB();
const app = express();
const PORT = process.env.PORT || 3000;
// 跨域配置(生产环境优化)
const corsOptions = {
origin: process.env.NODE_ENV === 'production'
? process.env.FRONTEND_DOMAIN // 前端线上域名(如https://vue-task-system.com)
: '*' // 开发环境允许所有域名
};
app.use(cors(corsOptions));
// 中间件配置
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// 启用Swagger文档
setupSwagger(app);
// 测试接口
app.get('/', (req, res) => {
successResponse(res, { message: 'Vue任务管理系统后端服务运行中' }, '服务正常');
});
// 挂载路由
app.use('/api/users', require('./routes/userRoutes'));
app.use('/api/categories', require('./routes/categoryRoutes'));
app.use('/api/todos', require('./routes/todoRoutes'));
// 404接口(放在路由之后,全局错误处理之前)
app.use('*', (req, res, next) => {
const err = new Error('接口不存在');
err.statusCode = 404;
next(err); // 传递给全局错误处理中间件
});
// 注册全局错误处理中间件(必须放在所有路由和中间件之后)
app.use(errorHandler);
// 启动服务
app.listen(PORT, () => {
logger.info(`后端服务启动成功,运行在端口:${PORT} | 环境:${process.env.NODE_ENV || 'development'}`);
});
3. 简化接口错误处理(移除冗余 try/catch)
以用户登录接口为例(routes/userRoutes.js),移除try/catch,直接抛出错误:
javascript
运行
// 2. 用户登录
router.post('/login', async (req, res, next) => {
// 校验请求参数
const { error } = loginSchema.validate(req.body);
if (error) {
const err = new Error(error.details[0].message);
err.statusCode = 400;
return next(err); // 传递给全局错误处理中间件
}
const { username, password } = req.body;
const user = await User.findOne({ username });
if (!user) {
const err = new Error('用户名不存在');
err.statusCode = 400;
return next(err);
}
const isPasswordValid = await verifyPassword(password, user.password);
if (!isPasswordValid) {
const err = new Error('密码错误');
err.statusCode = 400;
return next(err);
}
// 成功响应(无需try/catch,异常会被全局捕获)
successResponse(res, {
id: user._id,
username: user.username,
email: user.email,
avatar: user.avatar,
token: generateToken(user._id)
}, '登录成功');
});
4. 测试全局错误处理
- 访问不存在的接口(如
/api/xxx),返回接口不存在(404); - 传递无效 JWT 令牌,返回
令牌无效,请重新登录(401); - 数据库查询异常(如连接失败),返回
服务器内部错误(500),并在日志中记录详细堆栈信息。
二、核心实操二:缓存机制 ------Redis 优化热点数据查询
对于频繁访问但不常变化的数据(如用户信息、分类列表),每次从数据库查询会浪费资源。使用 Redis 缓存热点数据,可大幅提升接口响应速度,减轻数据库压力。
1. 安装 Redis 与依赖
(1)安装 Redis
- 本地安装:下载地址https://redis.io/download/(Windows 用户可使用 Redis Desktop Manager);
- 云服务:使用阿里云 Redis、腾讯云 Redis 等,无需本地安装。
(2)安装 Redis 依赖
bash
运行
npm install redis -S
2. Redis 配置(config/redis.js)
javascript
运行
const { createClient } = require('redis');
const logger = require('./logger');
require('dotenv').config();
// 创建Redis客户端
const redisClient = createClient({
url: process.env.REDIS_URL || 'redis://127.0.0.1:6379', // Redis连接地址
// 密码(若Redis设置了密码,添加以下配置)
// password: process.env.REDIS_PASSWORD
});
// 连接Redis
redisClient.connect().then(() => {
logger.info('Redis连接成功');
}).catch((error) => {
logger.error(`Redis连接失败:${error.message}`);
});
// 缓存工具函数
const redis = {
// 设置缓存(expireSeconds:过期时间,单位秒)
set: async (key, value, expireSeconds) => {
try {
const data = typeof value === 'object' ? JSON.stringify(value) : value;
await redisClient.set(key, data);
if (expireSeconds) {
await redisClient.expire(key, expireSeconds);
}
return true;
} catch (error) {
logger.error(`Redis设置缓存失败:key=${key} | 错误:${error.message}`);
return false;
}
},
// 获取缓存
get: async (key) => {
try {
const data = await redisClient.get(key);
if (!data) return null;
// 尝试解析JSON(处理对象类型缓存)
try {
return JSON.parse(data);
} catch (e) {
return data;
}
} catch (error) {
logger.error(`Redis获取缓存失败:key=${key} | 错误:${error.message}`);
return null;
}
},
// 删除缓存
del: async (key) => {
try {
await redisClient.del(key);
return true;
} catch (error) {
logger.error(`Redis删除缓存失败:key=${key} | 错误:${error.message}`);
return false;
}
}
};
module.exports = redis;
3. 在接口中使用 Redis 缓存
以 "获取用户信息" 接口为例(routes/userRoutes.js),缓存用户信息:
javascript
运行
const redis = require('../config/redis'); // 引入Redis工具
// 3. 获取当前用户信息(需要身份验证)
router.get('/me', protect, async (req, res, next) => {
const userId = req.user.id;
const cacheKey = `user:${userId}`;
// 1. 先从Redis获取缓存
const cachedUser = await redis.get(cacheKey);
if (cachedUser) {
logger.info(`用户${userId}从缓存获取信息`);
return successResponse(res, cachedUser, '获取用户信息成功');
}
// 2. 缓存不存在,从数据库查询
const user = await User.findById(userId).select('-password');
if (!user) {
const err = new Error('用户不存在');
err.statusCode = 400;
return next(err);
}
// 3. 存入Redis缓存(过期时间1小时)
await redis.set(cacheKey, user, 3600);
logger.info(`用户${userId}从数据库获取信息并缓存`);
successResponse(res, user, '获取用户信息成功');
});
4. 缓存更新与删除(保证数据一致性)
当用户信息更新(如修改头像、密码)时,需要同步删除缓存,避免返回旧数据:
javascript
运行
// 4. 上传用户头像(修改后删除缓存)
router.post('/avatar', protect, upload.single('avatar'), handleUploadError, async (req, res, next) => {
if (!req.file) {
const err = new Error('请选择要上传的头像');
err.statusCode = 400;
return next(err);
}
const userId = req.user.id;
const avatarUrl = `/uploads/avatars/${req.file.filename}`;
const cacheKey = `user:${userId}`;
// 更新用户头像
const user = await User.findByIdAndUpdate(
userId,
{ avatar: avatarUrl },
{ new: true }
).select('-password');
if (!user) {
const err = new Error('用户不存在');
err.statusCode = 400;
return next(err);
}
// 删除Redis缓存(下次请求将从数据库获取最新数据)
await redis.del(cacheKey);
logger.info(`用户${userId}更新头像,删除缓存`);
successResponse(res, { avatar: user.avatar }, '头像上传成功');
});
5. 测试缓存效果
- 首次访问
/api/users/me:从数据库查询,响应时间约 100ms,日志显示 "从数据库获取并缓存"; - 1 小时内再次访问:从 Redis 缓存获取,响应时间约 10ms,日志显示 "从缓存获取";
- 上传头像后访问:缓存被删除,从数据库获取最新头像信息。
三、核心实操三:接口限流 ------ 防止恶意请求攻击
生产环境中,接口可能遭受恶意请求(如暴力破解、爬虫爬取),导致服务器压力过大。使用express-rate-limit实现接口限流,限制单位时间内的请求次数。
1. 安装限流依赖
bash
运行
npm install express-rate-limit -S
2. 限流配置(middleware/rateLimit.js)
javascript
运行
const rateLimit = require('express-rate-limit');
const logger = require('../config/logger');
// 通用限流配置(针对所有接口)
const globalRateLimit = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 每个IP最多100次请求
message: { code: 429, message: '请求过于频繁,请15分钟后再试', data: null },
standardHeaders: true, // 启用标准限流响应头
legacyHeaders: false, // 禁用旧版限流响应头
// 记录限流日志
skipFailedRequests: true, // 只记录成功的请求
handler: (req, res, next, options) => {
logger.warn(`IP ${req.ip} 请求过于频繁,已被限流 | 路径:${req.method} ${req.originalUrl}`);
res.status(options.statusCode).json(options.message);
}
});
// 登录接口限流(更严格,防止暴力破解)
const loginRateLimit = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 5, // 每个IP最多5次登录请求
message: { code: 429, message: '登录请求过于频繁,请15分钟后再试', data: null },
standardHeaders: true,
legacyHeaders: false,
handler: (req, res, next, options) => {
logger.warn(`IP ${req.ip} 登录请求过于频繁,已被限流`);
res.status(options.statusCode).json(options.message);
}
});
module.exports = {
globalRateLimit,
loginRateLimit
};
3. 在入口文件中使用限流中间件(app.js)
javascript
运行
const { globalRateLimit, loginRateLimit } = require('./middleware/rateLimit'); // 引入限流配置
// 全局限流(所有接口生效)
app.use(globalRateLimit);
// 测试接口
app.get('/', (req, res) => {
successResponse(res, { message: 'Vue任务管理系统后端服务运行中' }, '服务正常');
});
// 登录接口单独限流(放在路由挂载前,优先级高于全局限流)
app.use('/api/users/login', loginRateLimit);
// 挂载路由
app.use('/api/users', require('./routes/userRoutes'));
app.use('/api/categories', require('./routes/categoryRoutes'));
app.use('/api/todos', require('./routes/todoRoutes'));
4. 测试限流效果
- 15 分钟内多次访问登录接口(超过 5 次),返回 "登录请求过于频繁,请 15 分钟后再试";
- 15 分钟内访问其他接口超过 100 次,返回 "请求过于频繁,请 15 分钟后再试";
- 日志中记录限流信息,便于监控恶意请求。
四、核心实操四:数据库索引优化 ------ 提升查询效率
当数据库数据量较大时(如 10 万 + 条任务),未优化的查询会变慢。MongoDB 索引可大幅提升查询速度,针对常用查询字段创建索引是企业级项目的必备优化。
1. 为数据模型添加索引(models/Todo.js、models/Category.js)
(1)任务模型索引(models/Todo.js)
javascript
运行
const mongoose = require('mongoose');
const mongoosePaginate = require('mongoose-paginate-v2');
const todoSchema = new mongoose.Schema({
title: {
type: String,
required: [true, '任务内容不能为空'],
trim: true
},
completed: {
type: Boolean,
default: false
},
categoryId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Category',
required: [true, '分类ID不能为空'],
index: true // 为分类查询字段创建索引
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: [true, '用户ID不能为空'],
index: true // 为用户查询字段创建索引
},
createTime: {
type: Date,
default: Date.now,
index: true // 为排序字段创建索引
}
}, {
timestamps: true
});
// 复合索引(针对多条件查询,如按用户+分类+完成状态查询)
todoSchema.index({ userId: 1, categoryId: 1, completed: 1 });
todoSchema.plugin(mongoosePaginate);
const Todo = mongoose.model('Todo', todoSchema);
module.exports = Todo;
(2)分类模型索引(models/Category.js)
javascript
运行
const mongoose = require('mongoose');
const categorySchema = new mongoose.Schema({
name: {
type: String,
required: [true, '分类名称不能为空'],
unique: true,
trim: true
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: [true, '用户ID不能为空'],
index: true // 为用户查询字段创建索引
},
createTime: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
// 复合索引(确保同一用户下分类名称唯一,配合unique: true)
categorySchema.index({ userId: 1, name: 1 }, { unique: true });
const Category = mongoose.model('Category', categorySchema);
module.exports = Category;
2. 查看索引与查询优化效果
(1)查看索引
使用 MongoDB Compass 连接数据库,进入对应集合(如todos),点击 "Indexes" 可查看创建的索引。
(2)测试查询效率
- 未创建索引:查询 10 万条任务中 "用户 A 的未完成任务",耗时约 500ms;
- 创建索引后:相同查询耗时约 50ms,查询效率提升 10 倍。
3. 索引优化原则
- 只为常用查询字段创建索引(如
userId、categoryId); - 避免为频繁修改的字段创建索引(索引会增加写入开销);
- 复合索引的字段顺序需符合查询习惯(如先按
userId查询,再按categoryId); - 定期删除无用索引(通过 MongoDB 的
explain()分析查询是否使用索引)。
五、核心实操五:单元测试 ------ 确保接口稳定性
企业级项目需要通过单元测试验证接口功能,避免后续修改代码导致功能异常。使用Jest+supertest实现接口单元测试。
1. 安装测试依赖
bash
运行
npm install jest supertest -D
2. 配置测试脚本(package.json)
json
{
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js",
"prod": "NODE_ENV=production node app.js",
"test": "jest --runInBand --detectOpenHandles" // 新增测试脚本
},
"jest": {
"testEnvironment": "node",
"testMatch": ["**/__tests__/**/*.test.js"] // 测试文件路径
}
}
3. 创建测试文件夹与测试文件
新建__tests__/api/user.test.js,测试用户相关接口:
javascript
运行
const request = require('supertest');
const app = require('../../app');
const User = require('../../models/User');
const mongoose = require('mongoose');
const redis = require('../../config/redis');
// 测试前连接数据库,测试后清理数据
beforeAll(async () => {
await mongoose.connect(process.env.MONGODB_URL);
});
afterAll(async () => {
// 清理测试数据
await User.deleteMany({ username: 'testuser_test' });
await redis.del('user:test');
await mongoose.disconnect();
});
// 测试用户注册接口
describe('POST /api/users/register', () => {
it('should register a new user', async () => {
const res = await request(app)
.post('/api/users/register')
.send({
username: 'testuser_test',
password: '123456',
email: 'testuser_test@163.com'
});
expect(res.statusCode).toBe(200);
expect(res.body.code).toBe(200);
expect(res.body.message).toBe('注册成功');
expect(res.body.data).toHaveProperty('token');
expect(res.body.data.username).toBe('testuser_test');
});
it('should return 400 if username already exists', async () => {
const res = await request(app)
.post('/api/users/register')
.send({
username: 'testuser_test',
password: '123456',
email: 'testuser_test2@163.com'
});
expect(res.statusCode).toBe(400);
expect(res.body.code).toBe(400);
expect(res.body.message).toBe('用户名已存在');
});
});
// 测试用户登录接口
describe('POST /api/users/login', () => {
it('should login successfully with correct credentials', async () => {
const res = await request(app)
.post('/api/users/login')
.send({
username: 'testuser_test',
password: '123456'
});
expect(res.statusCode).toBe(200);
expect(res.body.code).toBe(200);
expect(res.body.message).toBe('登录成功');
expect(res.body.data).toHaveProperty('token');
});
it('should return 400 with incorrect password', async () => {
const res = await request(app)
.post('/api/users/login')
.send({
username: 'testuser_test',
password: '1234567'
});
expect(res.statusCode).toBe(400);
expect(res.body.code).toBe(400);
expect(res.body.message).toBe('密码错误');
});
});
4. 运行单元测试
bash
运行
npm run test
5. 测试结果说明
- 绿色对勾:测试通过,接口功能正常;
- 红色叉号:测试失败,需检查接口逻辑;
- 每次修改核心接口后,建议运行单元测试,确保功能未被破坏。
六、综合实战:后端安全加固与生产环境部署
1. 安全加固补充
(1)接口签名验证(防止接口被篡改)
为敏感接口(如修改密码、转账)添加签名验证,前端通过 "参数 + 密钥 + 时间戳" 生成签名,后端验证签名合法性:
javascript
运行
// utils/sign.js
const crypto = require('crypto');
require('dotenv').config();
const SIGN_SECRET = process.env.SIGN_SECRET || 'vue_task_system_sign_secret';
// 生成签名(前端使用)
const generateSign = (params, timestamp) => {
// 按参数名排序
const sortedParams = Object.keys(params).sort().reduce((obj, key) => {
obj[key] = params[key];
return obj;
}, {});
// 拼接参数+时间戳+密钥
const signStr = `${JSON.stringify(sortedParams)}${timestamp}${SIGN_SECRET}`;
// SHA256加密
return crypto.createHash('sha256').update(signStr).digest('hex');
};
// 验证签名(后端使用)
const verifySign = (params, timestamp, sign) => {
// 验证时间戳(防止签名过期,有效期5分钟)
if (Date.now() - timestamp > 5 * 60 * 1000) {
return false;
}
// 生成签名并对比
const generatedSign = generateSign(params, timestamp);
return generatedSign === sign;
};
module.exports = {
generateSign,
verifySign
};
(2)HTTPS 配置(生产环境必须)
- 购买 SSL 证书(阿里云、腾讯云免费证书);
- 在
app.js中配置 HTTPS:
javascript
运行
const https = require('https');
const fs = require('fs');
const path = require('path');
// 生产环境启用HTTPS
if (process.env.NODE_ENV === 'production') {
const options = {
key: fs.readFileSync(path.join(__dirname, 'cert', 'private.key')), // 私钥路径
cert: fs.readFileSync(path.join(__dirname, 'cert', 'cert.pem')) // 证书路径
};
// 用HTTPS启动服务
https.createServer(options, app).listen(443, () => {
logger.info('HTTPS服务启动成功,运行在端口:443');
});
// HTTP重定向到HTTPS
app.listen(80, () => {
logger.info('HTTP服务启动成功,自动重定向到HTTPS');
app.use((req, res) => {
res.redirect(`https://${req.headers.host}${req.url}`);
});
});
} else {
// 开发环境HTTP启动
app.listen(PORT, () => {
logger.info(`后端服务启动成功,运行在端口:${PORT}`);
});
}
2. 生产环境部署完整流程(阿里云 ECS 为例)
(1)服务器环境准备
- 安装 Node.js、MongoDB、Redis、PM2;
- 配置防火墙(开放 80、443、3000 端口);
- 上传后端项目文件(使用 FTP 或 Git)。
(2)部署步骤
- 进入项目目录,安装依赖:
bash
运行
npm install --production
- 配置
.env.production文件:
env
PORT=3000
NODE_ENV=production
MONGODB_URL=mongodb://127.0.0.1:27017/vue_task_system_prod
REDIS_URL=redis://127.0.0.1:6379
JWT_SECRET=vue_task_system_backend_prod_secret
JWT_EXPIRES_IN=86400s
FRONTEND_DOMAIN=https://your-frontend-domain.com
SIGN_SECRET=vue_task_system_sign_secret_prod
- 上传 SSL 证书到
cert文件夹; - 用 PM2 启动服务:
bash
运行
pm2 start app.js --name vue-task-system-backend
pm2 startup
pm2 save
- 测试接口:访问
https://your-backend-domain.com/api/users/login,验证服务正常。
七、本节课总结与后续学习建议
1. 本节课核心收获
- 工程化能力:掌握全局错误处理、单元测试、接口文档自动化,让后端项目更规范、易维护;
- 性能优化:学会 Redis 缓存、数据库索引优化、接口限流,提升服务响应速度和稳定性;
- 安全加固:实现 HTTPS、接口签名、JWT 令牌验证,抵御常见网络攻击;
- 生产部署:掌握云服务器部署流程、PM2 进程守护,让服务具备生产环境运行能力。
2. 课后作业(必做)
- 为所有核心接口添加单元测试(任务、分类接口);
- 实现 "修改密码" 接口,并添加签名验证和缓存更新;
- 为生产环境配置 HTTPS,确保所有接口通过 HTTPS 访问;
- 对数据库所有常用查询字段创建索引,并用
explain()验证查询是否使用索引。
3. 后续后端学习建议
- 框架进阶:学习 NestJS(企业级 Node.js 框架),掌握模块化、依赖注入、拦截器等高级特性;
- 数据库深化:学习 MongoDB 事务、聚合查询,或 MySQL/PostgreSQL 关系型数据库及 ORM 工具(Sequelize/TypeORM);
- 微服务与云原生:学习 Docker 容器化、Kubernetes 编排、微服务拆分(如用户服务、任务服务)、服务注册与发现(Nacos/Consul);
- 高级架构:学习消息队列(RabbitMQ/Kafka)解耦服务、分布式缓存(Redis 集群)、分布式事务;
- 性能监控:学习 Prometheus+Grafana 监控服务指标、ELK 栈(Elasticsearch+Logstash+Kibana)分析日志。
至此,Node.js 后端企业级进阶课程已全部结束!你已掌握从基础接口开发到生产环境部署、性能优化、安全加固的完整后端开发能力,能够独立设计和开发中小型企业级后端服务。后端开发的核心是 "稳定性、性能、安全性",后续需通过实际项目持续积累经验,不断深化技术栈,成为一名具备架构设计能力的资深后端开发工程师!