22999 字
115 分钟

Express.js 零基础入门指南

Express.js 零基础入门指南#

本指南将帮助您从零开始,逐步掌握 Express.js 框架,学会使用它构建强大的 Web 应用。

🗺️ 学习路径图#

graph TD A[开始学习] --> B{有 JavaScript 基础?} B -->|否| C[学习 JavaScript 基础<br/>1-2周] B -->|是| D[学习 Node.js 基础<br/>1周] C --> D D --> E[5分钟快速入门<br/>先跑起来] E --> F[学习 Express.js 基础<br/>2-3周] F --> G[实战练习<br/>1-2周] G --> H[部署上线<br/>1周] H --> I[完成入门!] style A fill:#e1f5ff style B fill:#fce4ec style C fill:#fff4e1 style D fill:#fff4e1 style E fill:#e8f5e9 style F fill:#fff9c4 style G fill:#fff9c4 style H fill:#ffccbc style I fill:#c8e6c9

学习建议:

  • 总时间:4-8 周(根据基础不同)
  • 📅 每天学习:1-2 小时
  • 🎯 学习方式:理论 + 实践相结合
  • 🔄 循环学习:先快速浏览,再深入细节

目录#



第一步:了解什么是 Express.js#

Express.js(简称 Express)是一个基于 Node.js 的轻量级 Web 应用框架,它提供了丰富的功能来帮助开发人员快速构建 Web 应用和 API。

Express.js 的特点#

  • 简洁灵活:最小化核心,提供强大的扩展能力
  • 丰富的中间件:通过中间件实现各种功能
  • 强大的路由系统:支持灵活的路由定义
  • 易于学习:对新手友好,文档完善
  • 生态系统庞大:有大量的第三方包和工具

Express.js vs 原生 Node.js#

特性原生 Node.jsExpress.js
代码量
路由处理手动实现内置支持
中间件需要自己实现丰富的中间件生态
学习曲线陡峭平缓
适用场景简单应用中大型 Web 应用/API

第二步:前置知识准备#

在开始学习 Express.js 之前,您需要掌握以下基础知识:

必备知识#

  • JavaScript 基础:变量、函数、对象、数组等
  • Node.js 基础:模块系统、npm 包管理
  • HTTP 基础:请求方法(GET、POST 等)、状态码
  • 命令行操作:基本的终端命令
WARNING

⚠️ 重要提醒:如果您还没有掌握以上基础知识,强烈建议先学习这些内容,否则学习 Express.js 会非常吃力。学习编程要循序渐进,打好基础是成功的关键!

推荐学习资源#

如果您还没有掌握这些知识,建议先学习以下资源:

  1. Node.js 入门指南针对 Node.js 的零基础编程入门指南
  2. JavaScript 教程MDN JavaScript 教程

第三步:5分钟快速入门#

目标:在 5 分钟内创建并运行你的第一个 Express.js 应用,先跑起来,再深入理解!

🚀 快速开始(3步搞定)#

第 1 步:创建项目(1分钟)#

打开终端,依次执行以下命令:

Terminal window
# 创建项目目录
mkdir my-express-app
cd my-express-app
# 初始化项目
npm init -y
# 安装 Express
npm install express

第 2 步:创建服务器(2分钟)#

创建一个名为 app.js 的文件,复制以下代码:

// 引入 Express
const express = require('express');
// 创建应用
const app = express();
// 定义路由:访问首页时返回"Hello, Express!"
app.get('/', (req, res) => {
res.send('Hello, Express! 🎉');
});
// 启动服务器
app.listen(3000, () => {
console.log('✅ 服务器已启动!访问 http://localhost:3000');
});

第 3 步:运行应用(1分钟)#

Terminal window
# 运行应用
node app.js

打开浏览器,访问 http://localhost:3000,你会看到:

Hello, Express! 🎉

恭喜! 你已经成功创建了第一个 Express.js 应用!🎊

💡 现在发生了什么?#

让我们用简单的语言解释一下刚才的代码:

const express = require('express');

这行代码的意思是

  • express 这个”工具箱”里取出 Express
  • 就像从厨房拿出”菜刀”准备做饭
const app = express();

这行代码的意思是

  • 创建一个 Express 应用
  • 就像准备好了一个”厨房”
app.get('/', (req, res) => {
res.send('Hello, Express! 🎉');
});

这行代码的意思是

  • 当有人访问首页(/)时
  • 返回”Hello, Express! 🎉”
  • 就像客人来了,你给他端上一杯茶
app.listen(3000, () => {
console.log('✅ 服务器已启动!访问 http://localhost:3000');
});

这行代码的意思是

  • 在 3000 端口启动服务器
  • 就像打开餐厅的大门,准备迎接客人

🎯 试试修改代码#

让我们修改一下代码,让它更有趣:

const express = require('express');
const app = express();
// 添加更多路由
app.get('/', (req, res) => {
res.send('🏠 欢迎来到我的网站!');
});
app.get('/about', (req, res) => {
res.send('👋 这是关于页面');
});
app.get('/contact', (req, res) => {
res.send('📧 联系我们:contact@example.com');
});
// 启动服务器
app.listen(3000, () => {
console.log('✅ 服务器已启动!');
console.log('🏠 首页:http://localhost:3000');
console.log('👋 关于:http://localhost:3000/about');
console.log('📧 联系:http://localhost:3000/contact');
});

现在你可以访问:

  • http://localhost:3000 - 首页
  • http://localhost:3000/about - 关于页面
  • http://localhost:3000/contact - 联系页面

🎉 常见问题#

Q: 我看到 “command not found” 错误

Q: 端口 3000 被占用了怎么办?

  • A: 把 3000 改成其他数字,比如 3001
app.listen(3001, () => {
console.log('✅ 服务器已启动!访问 http://localhost:3001');
});

Q: 如何停止服务器?

  • A: 在终端按 Ctrl + C

🚀 下一步#

现在你已经成功运行了第一个 Express.js 应用!接下来:

  1. 继续学习:阅读后面的章节,深入了解 Express.js
  2. 动手实践:尝试修改代码,添加更多功能
  3. 查看文档:遇到问题时,查看 Express.js 官方文档
TIP

如果你还不太理解上面的代码,没关系!继续往下看,后面的章节会详细解释每一个概念。现在最重要的是先跑起来,建立信心!


第四步:创建第一个 Express 应用(详细版)#

4.1 初始化项目#

首先,创建一个新的项目目录并初始化:

Terminal window
# 创建项目目录
mkdir my-express-app
cd my-express-app
# 初始化 npm 项目
npm init -y
# 安装 Express
npm install express

4.2 创建基本的服务器#

创建一个名为 app.js 的文件:

// 引入 Express 模块
const express = require('express');
// 创建 Express 应用
const app = express();
// 定义端口
const PORT = 3000;
// 定义路由
app.get('/', (req, res) => {
res.send('Hello, Express!');
});
// 启动服务器
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});

4.3 运行应用#

Terminal window
# 运行应用
node app.js

打开浏览器访问 http://localhost:3000,您将看到 “Hello, Express!”。

TIP

在开发过程中,建议使用 nodemon 工具,它可以在代码修改后自动重启服务器:

Terminal window
npm install -D nodemon
nodemon app.js

第五步:路由基础#

路由是 Express.js 的核心功能之一,它决定了应用如何响应客户端请求。

5.1 基本路由#

// GET 请求
app.get('/', (req, res) => {
res.send('首页');
});
// POST 请求
app.post('/users', (req, res) => {
res.send('创建用户');
});
// PUT 请求
app.put('/users/:id', (req, res) => {
res.send(`更新用户 ${req.params.id}`);
});
// DELETE 请求
app.delete('/users/:id', (req, res) => {
res.send(`删除用户 ${req.params.id}`);
});
NOTE

💡 路由定义顺序很重要:Express.js 会按照路由定义的顺序进行匹配。具体路由应该放在通配路由之前,否则通配路由会先匹配,导致具体路由永远不会执行。

5.2 路由参数#

// 路由参数
app.get('/users/:userId', (req, res) => {
const userId = req.params.userId;
res.send(`用户 ID: ${userId}`);
});
// 多个参数
app.get('/posts/:year/:month', (req, res) => {
const { year, month } = req.params;
res.send(`文章归档: ${year}年${month}月`);
});

5.3 查询参数#

app.get('/search', (req, res) => {
const { q, page } = req.query;
res.send(`搜索: ${q}, 页码: ${page}`);
});
// 访问: /search?q=nodejs&page=1
// 输出: 搜索: nodejs, 页码: 1

5.4 路由模块化#

为了保持代码整洁,建议将路由单独放在一个文件中:

routes/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.send('用户列表');
});
router.get('/:id', (req, res) => {
res.send(`用户详情: ${req.params.id}`);
});
module.exports = router;

在主应用中使用路由:

const usersRouter = require('./routes/users');
app.use('/users', usersRouter);

第六步:中间件#

中间件是 Express.js 的一个重要概念,它可以访问请求对象(req)、响应对象(res)和下一个中间件函数。

🗺️ 中间件执行流程#

graph TD A[客户端请求] --> B[中间件 1<br/>日志记录] B --> C[中间件 2<br/>解析 JSON] C --> D[中间件 3<br/>身份验证] D --> E{验证通过?} E -->|是| F[路由处理器<br/>处理业务逻辑] E -->|否| G[错误处理中间件<br/>返回 401] F --> H[响应中间件<br/>设置响应头] H --> I[错误处理中间件<br/>捕获错误] I --> J[返回响应给客户端] G --> J style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#fff4e1 style D fill:#fff4e1 style E fill:#fce4ec style F fill:#e8f5e9 style G fill:#ffccbc style H fill:#fff4e1 style I fill:#ffccbc style J fill:#c8e6c9

中间件执行顺序:

  1. 请求进入应用
  2. 按照注册顺序依次执行中间件
  3. 每个中间件可以修改请求/响应对象
  4. 调用 next() 将控制权传递给下一个中间件
  5. 路由处理器执行业务逻辑
  6. 响应返回(可能经过响应中间件)
  7. 错误处理中间件捕获并处理错误

💡 什么是中间件?(通俗解释)#

中间件就像工厂的流水线,每个工位(中间件)都有特定的任务:

🏭 生活比喻:流水线#

想象一个汽车制造流水线:

原材料 → [工位1:检查材料] → [工位2:组装零件] → [工位3:喷漆] → [工位4:质检] → 成品

在 Express.js 中:

用户请求 → [中间件1:记录日志] → [中间件2:解析数据] → [中间件3:验证身份] → [路由处理器] → 响应

每个中间件就像流水线上的一个工位:

  • 工位1(日志中间件):记录”来了一个请求”
  • 工位2(解析中间件):把用户提交的数据整理好
  • 工位3(验证中间件):检查用户是否有权限
  • 质检(错误处理中间件):如果前面的工位出了问题,这里会处理

🧽 另一个比喻:过滤网#

中间件也像多层过滤网

水 → [粗网:过滤大颗粒] → [细网:过滤小颗粒] → [活性炭:吸附杂质] → [消毒:杀菌] → 干净水

在 Express.js 中:

请求 → [日志中间件] → [解析中间件] → [验证中间件] → [路由处理器] → 响应

每一层过滤网(中间件)都会检查或处理请求,然后传递给下一层。

🎯 中间件的核心特点#

  1. 顺序执行:像流水线一样,按照注册的顺序依次执行
  2. 可以修改:每个中间件可以查看和修改请求/响应数据
  3. 可以终止:某个中间件可以决定不继续传递(比如验证失败)
  4. 可以跳过:某些中间件只对特定路由生效

📝 生活中的例子#

餐厅点餐流程:

  1. 门口服务员(中间件1):欢迎顾客,记录人数
  2. 领位员(中间件2):引导顾客到座位
  3. 服务员(中间件3):介绍菜单,记录点餐
  4. 厨师(路由处理器):烹饪食物
  5. 传菜员(中间件4):检查菜品质量
  6. 服务员(中间件5):上菜,结账

每个环节都是”中间件”,负责处理一部分任务,然后传递给下一个环节。

6.1 应用级中间件#

// 应用级中间件
app.use((req, res, next) => {
console.log(`${new Date().toLocaleString()} - ${req.method} ${req.url}`);
next();
});
// 路由级中间件
app.get('/admin', (req, res, next) => {
// 验证逻辑
const isAuthenticated = true;
if (!isAuthenticated) {
return res.status(401).send('未授权');
}
next();
}, (req, res) => {
res.send('管理员页面');
});

6.2 内置中间件#

// 解析 JSON 请求体
app.use(express.json());
// 解析 URL 编码的请求体
app.use(express.urlencoded({ extended: true }));
// 静态文件服务
app.use(express.static('public'));

6.3 错误处理中间件#

// 错误处理中间件(必须放在最后)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('服务器错误!');
});
WARNING

⚠️ 重要:错误处理中间件必须放在所有路由和其他中间件之后,因为它需要捕获前面的所有错误。如果放在前面,后面的错误就无法被捕获了!

6.4 第三方中间件#

Terminal window
# 安装常用中间件
npm install morgan cors helmet
const morgan = require('morgan');
const cors = require('cors');
const helmet = require('helmet');
// 日志记录
app.use(morgan('dev'));
// 跨域支持
app.use(cors());
// 安全头设置
app.use(helmet());

第七步:静态文件服务#

Express.js 可以轻松地提供静态文件服务,如 HTML、CSS、JavaScript、图片等。

7.1 基本用法#

// 设置静态文件目录
app.use(express.static('public'));
// 访问 public/index.html
// URL: http://localhost:3000/index.html
// 访问 public/images/logo.png
// URL: http://localhost:3000/images/logo.png

7.2 多个静态目录#

// 主静态文件目录
app.use(express.static('public'));
// 上传文件目录
app.use('/uploads', express.static('uploads'));

7.3 虚拟路径#

3000/static/index.html
// 使用虚拟路径
app.use('/static', express.static('public'));
// 实际文件: public/index.html

第八步:请求处理#

🗺️ 请求-响应生命周期#

sequenceDiagram participant C as 客户端 participant E as Express.js participant M as 中间件 participant R as 路由处理器 participant DB as 数据库 C->>E: HTTP 请求 E->>M: 解析请求体 M->>M: 验证身份 M->>R: 路由匹配 R->>DB: 查询数据 DB-->>R: 返回数据 R->>R: 处理业务逻辑 R->>E: 返回响应 E->>C: HTTP 响应 Note over C,DB: 完整的请求-响应流程

请求处理关键点:

  • 请求体解析(JSON、URL 编码)
  • 路由参数提取
  • 查询参数解析
  • 请求头信息获取
  • 文件上传处理

8.1 获取请求数据#

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.post('/api/users', (req, res) => {
// 获取 JSON 数据
const { name, email } = req.body;
// 获取查询参数
const { page, limit } = req.query;
// 获取路由参数
const userId = req.params.id;
// 获取请求头
const userAgent = req.headers['user-agent'];
res.json({
name,
email,
page,
limit,
userId,
userAgent
});
});

8.2 文件上传#

Terminal window
npm install multer
const multer = require('multer');
// 配置存储
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
cb(null, Date.now() + '-' + file.originalname);
}
});
const upload = multer({ storage });
// 单文件上传
app.post('/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).send('没有上传文件');
}
res.send('文件上传成功!');
});
// 多文件上传
app.post('/uploads', upload.array('files', 5), (req, res) => {
res.send('文件上传成功!');
});

第九步:响应处理#

9.1 发送响应#

// 发送文本
app.get('/', (req, res) => {
res.send('Hello, World!');
});
// 发送 JSON
app.get('/api/data', (req, res) => {
res.json({ message: 'Hello, JSON!' });
});
// 发送状态码
app.get('/not-found', (req, res) => {
res.status(404).send('页面未找到');
});
// 发送文件
app.get('/download', (req, res) => {
res.download('/path/to/file.pdf');
});
// 重定向
app.get('/redirect', (req, res) => {
res.redirect('/new-location');
});

9.2 设置响应头#

app.get('/api/data', (req, res) => {
res.set('Content-Type', 'application/json');
res.set('X-Custom-Header', 'Custom Value');
res.json({ message: 'Hello!' });
});

第十步:模板引擎#

模板引擎让您可以使用模板文件生成动态 HTML。

10.1 使用 EJS#

Terminal window
npm install ejs
// 设置模板引擎
app.set('view engine', 'ejs');
app.set('views', './views');
// 渲染模板
app.get('/', (req, res) => {
res.render('index', { title: '首页', user: '张三' });
});

创建模板文件 views/index.ejs

<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<h1>欢迎, <%= user %>!</h1>
</body>
</html>

10.2 使用 Pug#

Terminal window
npm install pug
app.set('view engine', 'pug');
app.set('views', './views');
app.get('/', (req, res) => {
res.render('index', { title: '首页', user: '张三' });
});

创建模板文件 views/index.pug

doctype html
html
head
title= title
body
h1 欢迎, #{user}!

第十一步:错误处理#

TIP

💡 错误处理是生产应用的必备技能:良好的错误处理可以防止应用崩溃,提供友好的错误信息,帮助开发者快速定位问题。不要忽视错误处理,它会让你的应用更加健壮!

11.1 同步错误处理#

app.get('/error', (req, res) => {
throw new Error('这是一个错误');
});

11.2 异步错误处理#

// 使用 next 传递错误
app.get('/async-error', (req, res, next) => {
setTimeout(() => {
next(new Error('异步错误'));
}, 1000);
});
// 使用 async/await
app.get('/async-await', async (req, res, next) => {
try {
const data = await someAsyncFunction();
res.json(data);
} catch (error) {
next(error);
}
});

11.3 自定义错误处理#

// 404 错误处理
app.use((req, res, next) => {
res.status(404).render('404', { url: req.url });
});
// 全局错误处理
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500);
res.json({
error: {
message: err.message,
status: err.status || 500
}
});
});

第十二步:调试技巧#

调试是开发过程中不可或缺的技能,掌握调试技巧可以大大提高开发效率。

💡 为什么需要调试?#

在开发 Express.js 应用时,你可能会遇到各种问题:

  • 🐛 代码逻辑错误
  • 🐛 中间件顺序错误
  • 🐛 路由不匹配
  • 🐛 数据库连接失败
  • 🐛 异步操作错误

🗺️ 调试流程#

graph TD A[发现问题] --> B[添加日志] B --> C{问题解决?} C -->|是| G[完成] C -->|否| D[使用断点调试] D --> E{问题解决?} E -->|是| G E -->|否| F[查看错误堆栈] F --> H[搜索解决方案] H --> B style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#fce4ec style D fill:#fff4e1 style E fill:#fce4ec style F fill:#fff4e1 style G fill:#c8e6c9 style H fill:#fff9c4

12.1 使用 console.log 调试#

最简单的调试方法就是使用 console.log 输出信息:

app.get('/api/users/:id', async (req, res) => {
console.log('收到请求:', req.url);
console.log('请求参数:', req.params);
console.log('查询参数:', req.query);
const userId = req.params.id;
console.log('用户 ID:', userId);
try {
const user = await User.findById(userId);
console.log('查询结果:', user);
if (!user) {
console.log('用户不存在');
return res.status(404).json({ error: '用户不存在' });
}
res.json(user);
} catch (error) {
console.error('查询错误:', error);
res.status(500).json({ error: '服务器错误' });
}
});

💡 console.log 的最佳实践#

推荐做法:

// 使用有意义的标签
console.log('[用户服务] 查询用户 ID:', userId);
console.log('[数据库] 连接成功');
// 使用对象展开
console.log('请求数据:', { ...req.body });
// 使用时间戳
console.log(`[${new Date().toISOString()}] 处理请求: ${req.method} ${req.url}`);

不推荐做法:

// 没有标签
console.log(userId);
// 输出过多信息
console.log(req); // 会输出整个请求对象,非常混乱
// 忘记删除调试代码
console.log('调试信息'); // 生产环境不应该有

12.2 使用 VS Code 调试#

VS Code 提供了强大的调试功能,可以设置断点、单步执行、查看变量等。

📝 配置 launch.json#

在项目根目录创建 .vscode/launch.json 文件:

{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动 Express 应用",
"program": "${workspaceFolder}/app.js",
"skipFiles": [
"<node_internals>/**"
]
}
]
}

🎯 使用断点调试#

  1. 设置断点:在代码行号左侧点击,出现红点
  2. 启动调试:按 F5 或点击调试按钮
  3. 单步执行
    • F10:单步跳过(Step Over)
    • F11:单步进入(Step Into)
    • Shift + F11:单步跳出(Step Out)
  4. 查看变量:在调试面板中查看变量的值
  5. 查看调用栈:了解代码执行路径

📊 调试面板功能#

功能快捷键说明
继续执行F5继续运行到下一个断点
单步跳过F10执行当前行,不进入函数
单步进入F11进入当前行的函数内部
单步跳出Shift+F11跳出当前函数
重启调试Ctrl+Shift+F5重新启动调试
停止调试Shift+F5停止调试

12.3 使用调试中间件#

Express.js 有一些专门的调试中间件可以帮助你排查问题。

morgan(HTTP 请求日志)#

Terminal window
npm install morgan
const morgan = require('morgan');
// 开发环境:详细日志
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
}
// 生产环境:简洁日志
if (process.env.NODE_ENV === 'production') {
app.use(morgan('combined'));
}
// 自定义日志格式
app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));

morgan 预设格式:

格式说明示例
dev开发环境,彩色输出GET /api/users 200 15 ms
combinedApache 组合格式完整的访问日志
commonApache 通用格式简化的访问日志
short短格式GET /api/users 200
tiny最简格式GET /api/users 200

debug(调试信息)#

Terminal window
npm install debug
const debug = require('debug')('myapp:server');
const debugUser = require('debug')('myapp:user');
app.get('/api/users/:id', async (req, res) => {
debug('收到用户查询请求');
const userId = req.params.id;
debugUser('查询用户 ID: %s', userId);
try {
const user = await User.findById(userId);
debugUser('查询结果: %O', user);
if (!user) {
debug('用户不存在');
return res.status(404).json({ error: '用户不存在' });
}
res.json(user);
} catch (error) {
debug('查询错误: %O', error);
res.status(500).json({ error: '服务器错误' });
}
});

使用 debug:

Terminal window
# 启用所有调试信息
DEBUG=* node app.js
# 只启用特定命名空间的调试
DEBUG=myapp:* node app.js
# 只启用用户相关的调试
DEBUG=myapp:user node app.js

12.4 常见错误及解决方法#

错误 1:端口被占用#

错误信息:

Error: listen EADDRINUSE: address already in use :::3000

原因: 端口 3000 已经被其他程序占用

解决方案:

方法 1:找到并关闭占用端口的程序

Terminal window
# 查找占用端口的进程(Linux/Mac)
lsof -i :3000
# 查找占用端口的进程(Windows)
netstat -ano | findstr :3000
# 杀死进程
kill -9 <PID> # Linux/Mac
taskkill /PID <PID> /F # Windows

方法 2:使用其他端口

const PORT = process.env.PORT || 3001; // 改用 3001

方法 3:自动查找可用端口

Terminal window
npm install detect-port
const detectPort = require('detect-port');
detectPort(3000).then(port => {
if (port === 3000) {
console.log('端口 3000 可用');
} else {
console.log(`端口 3000 被占用,使用端口 ${port}`);
}
app.listen(port, () => {
console.log(`服务器运行在端口 ${port}`);
});
});

错误 2:路由不匹配#

问题: 定义了路由,但访问时返回 404

可能原因:

  1. 路由顺序错误
// ❌ 错误:通配路由放前面
app.get('/api/users/*', (req, res) => {
res.send('所有用户');
});
app.get('/api/users/admin', (req, res) => {
res.send('管理员'); // 永远不会执行
});
// ✅ 正确:具体路由放前面
app.get('/api/users/admin', (req, res) => {
res.send('管理员');
});
app.get('/api/users/*', (req, res) => {
res.send('所有用户');
});
  1. 中间件顺序错误
// ❌ 错误:路由在静态文件中间件之前
app.get('/index.html', (req, res) => {
res.send('自定义首页');
});
app.use(express.static('public')); // 会先匹配静态文件
// ✅ 正确:静态文件中间件在前
app.use(express.static('public'));
app.get('/index.html', (req, res) => {
res.send('自定义首页'); // 只有找不到静态文件时才会执行
});
  1. 路径大小写问题
// 路由定义
app.get('/api/Users', (req, res) => {
res.send('用户列表');
});
// ❌ 错误:大小写不匹配
// 访问:/api/users (返回 404)
// ✅ 正确:大小写匹配
// 访问:/api/Users

错误 3:请求体解析失败#

问题: req.bodyundefined

原因: 没有使用解析中间件

解决方案:

// ✅ 添加解析中间件
app.use(express.json()); // 解析 JSON
app.use(express.urlencoded({ extended: true })); // 解析 URL 编码
app.post('/api/users', (req, res) => {
console.log(req.body); // 现在可以正确获取请求体
res.json(req.body);
});

错误 4:CORS 跨域问题#

错误信息:

Access to XMLHttpRequest at 'http://localhost:3000/api/users'
from origin 'http://localhost:8080' has been blocked by CORS policy

原因: 浏览器的同源策略限制

解决方案:

Terminal window
npm install cors
const cors = require('cors');
// 允许所有来源(开发环境)
app.use(cors());
// 只允许特定来源(生产环境)
app.use(cors({
origin: 'https://yourdomain.com'
}));
// 允许多个来源
app.use(cors({
origin: ['https://example1.com', 'https://example2.com']
}));
// 自定义 CORS 配置
app.use(cors({
origin: function (origin, callback) {
// 允许没有 origin 的请求(如移动应用)
if (!origin) return callback(null, true);
if (allowedOrigins.indexOf(origin) === -1) {
const msg = 'CORS policy for this site does not allow access from the specified Origin.';
return callback(new Error(msg), false);
}
return callback(null, true);
}
}));

错误 5:异步错误未捕获#

问题: 异步操作中的错误没有被捕获,导致服务器崩溃

解决方案:

方法 1:使用 try-catch

app.get('/api/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
res.json(user);
} catch (error) {
next(error); // 传递给错误处理中间件
}
});

方法 2:使用全局错误处理

// 捕获未处理的 Promise 拒绝
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise 拒绝:', reason);
});
// 捕获未捕获的异常
process.on('uncaughtException', (error) => {
console.error('未捕获的异常:', error);
process.exit(1); // 退出进程
});

方法 3:使用 express-async-errors

Terminal window
npm install express-async-errors
require('express-async-errors'); // 必须在引入 express 之后
// 现在可以直接使用 async/await,不需要 try-catch
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user); // 如果出错,会自动传递给错误处理中间件
});

错误 6:数据库连接失败#

错误信息:

MongoNetworkError: failed to connect to server [localhost:27017]

解决方案:

  1. 检查数据库服务是否运行
Terminal window
# MongoDB
# Linux/Mac
sudo systemctl status mongod
# Windows
# 检查 MongoDB 服务是否运行
# 启动 MongoDB
# Linux/Mac
sudo systemctl start mongod
# Windows
# 在服务管理器中启动 MongoDB 服务
  1. 检查连接字符串
// ❌ 错误:可能拼写错误
mongoose.connect('mongodb://localhost:27017/myapp');
// ✅ 正确:添加错误处理
mongoose.connect('mongodb://localhost:27017/myapp', {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('数据库连接成功'))
.catch(error => {
console.error('数据库连接失败:', error);
process.exit(1);
});
  1. 检查网络连接
// 测试数据库连接
const mongoose = require('mongoose');
mongoose.connection.on('connected', () => {
console.log('MongoDB 连接成功');
});
mongoose.connection.on('error', (error) => {
console.error('MongoDB 连接错误:', error);
});
mongoose.connection.on('disconnected', () => {
console.log('MongoDB 连接断开');
});

错误 7:内存泄漏#

症状: 应用运行一段时间后变慢,最终崩溃

常见原因:

  1. 全局变量累积
// ❌ 错误:全局变量无限增长
const cache = {};
app.get('/api/data', (req, res) => {
cache[Date.now()] = req.query.data; // 缓存无限增长
res.json({ success: true });
});
// ✅ 正确:限制缓存大小
const cache = new Map();
const MAX_CACHE_SIZE = 1000;
app.get('/api/data', (req, res) => {
if (cache.size >= MAX_CACHE_SIZE) {
// 删除最旧的条目
const oldestKey = cache.keys().next().value;
cache.delete(oldestKey);
}
cache.set(Date.now(), req.query.data);
res.json({ success: true });
});
  1. 事件监听器未移除
// ❌ 错误:事件监听器累积
app.get('/api/data', (req, res) => {
someEmitter.on('data', handler); // 每次请求都添加监听器
res.json({ success: true });
});
// ✅ 正确:使用 once 或移除监听器
app.get('/api/data', (req, res) => {
someEmitter.once('data', handler); // 只执行一次
res.json({ success: true });
});
  1. 定时器未清理
// ❌ 错误:定时器未清理
app.post('/api/schedule', (req, res) => {
setInterval(() => {
// 定时任务
}, 1000); // 每次请求都创建定时器
res.json({ success: true });
});
// ✅ 正确:清理定时器
const timers = new Set();
app.post('/api/schedule', (req, res) => {
const timer = setInterval(() => {
// 定时任务
}, 1000);
timers.add(timer);
// 在某个时机清理
// clearInterval(timer);
// timers.delete(timer);
res.json({ success: true });
});

12.5 性能分析#

使用 Node.js 性能分析器#

Terminal window
# 启动应用时启用性能分析
node --prof app.js
# 生成性能报告
node --prof-process isolate-*.log > profile.txt

使用 clinic.js#

Terminal window
npm install -g clinic
# 启动应用并分析
clinic doctor -- node app.js

使用 Chrome DevTools#

// 在 app.js 中添加
const inspector = require('inspector');
const fs = require('fs');
if (process.env.NODE_ENV === 'development') {
inspector.open(9229, '0.0.0.0');
console.log('调试器运行在 http://localhost:9229');
}

然后在 Chrome 浏览器中打开 chrome://inspect,点击 “Inspect” 进行调试。

12.6 日志管理#

使用 winston(专业日志库)#

Terminal window
npm install winston
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
// 错误日志
new winston.transports.File({
filename: 'error.log',
level: 'error'
}),
// 所有日志
new winston.transports.File({
filename: 'combined.log'
})
]
});
// 开发环境同时输出到控制台
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
// 使用日志
logger.info('服务器启动');
logger.error('数据库连接失败', { error: err });
logger.warn('内存使用率过高', { usage: '90%' });

12.7 调试最佳实践#

  1. 开发环境使用详细日志
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
}
  1. 生产环境使用简洁日志
if (process.env.NODE_ENV === 'production') {
app.use(morgan('combined', { stream: fs.createWriteStream('./access.log', { flags: 'a' }) }));
}
  1. 使用环境变量控制调试
const DEBUG = process.env.DEBUG === 'true';
if (DEBUG) {
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
console.log('Headers:', req.headers);
console.log('Body:', req.body);
next();
});
}
  1. 定期清理调试代码
// ❌ 不要在生产环境留下调试代码
console.log('调试信息');
// ✅ 使用条件判断
if (process.env.NODE_ENV === 'development') {
console.log('调试信息');
}

第十三步:RESTful API 设计#

RESTful API 是一种设计风格,使用 HTTP 方法和资源路径来构建 API。

🗺️ RESTful API 设计流程#

graph TD A[设计 RESTful API] --> B[识别资源<br/>用户, 文章, 评论] B --> C[定义资源路径<br/>/users, /posts, /comments] C --> D[选择 HTTP 方法<br/>GET, POST, PUT, DELETE] D --> E[设计端点<br/>/api/users, /api/posts] E --> F[定义请求/响应格式<br/>JSON Schema] F --> G[实现错误处理<br/>400, 404, 500] G --> H[添加认证授权<br/>JWT, OAuth] H --> I[编写 API 文档<br/>Swagger] I --> J[测试 API<br/>单元测试, 集成测试] style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#fff4e1 style D fill:#fff4e1 style E fill:#e8f5e9 style F fill:#e8f5e9 style G fill:#fce4ec style H fill:#ffccbc style I fill:#c8e6c9 style J fill:#c8e6c9

RESTful API 设计原则:

  • 资源导向:使用名词而非动词
  • 统一接口:使用标准 HTTP 方法
  • 无状态:每个请求包含所有必要信息
  • 可缓存:响应应该明确是否可缓存
  • 分层系统:客户端不需要知道是否直接连接服务器

13.1 RESTful 原则#

HTTP 方法操作示例路径
GET获取资源/api/users
POST创建资源/api/users
PUT更新资源/api/users/
PATCH部分更新/api/users/
DELETE删除资源/api/users/

💡 RESTful API 设计原则详解#

📚 什么是 RESTful API?#

RESTful API 是一种基于 REST(Representational State Transfer,表述性状态转移) 架构风格的 API 设计方式。它使用 HTTP 协议的标准方法来操作资源,使 API 更加简洁、统一和易于理解。

🏢 生活比喻:图书馆管理系统#

想象一个图书馆的图书管理系统:

操作传统方式RESTful 方式
查看所有书getAllBooks()GET /api/books
查看一本书getBookById(123)GET /api/books/123
借一本书borrowBook(123)POST /api/books/123/borrow
归还一本书returnBook(123)POST /api/books/123/return
添加新书addBook(bookInfo)POST /api/books
更新书信息updateBook(123, newInfo)PUT /api/books/123
删除一本书deleteBook(123)DELETE /api/books/123

RESTful 方式的优点:

  • 统一:所有操作都使用标准 HTTP 方法
  • 直观:URL 清晰地表达了资源和操作
  • 无状态:每个请求都包含所有必要信息
  • 可缓存:GET 请求可以被缓存

🎯 RESTful 设计的核心原则#

1. 资源导向(Resource-Oriented)#

使用名词而非动词来命名资源:

错误示例:

GET /getUsers
POST /createUser
PUT /updateUser/1
DELETE /deleteUser/1

正确示例:

GET /api/users
POST /api/users
PUT /api/users/1
DELETE /api/users/1

原则:URL 应该代表资源,而不是动作。

2. 统一接口(Uniform Interface)#

使用标准 HTTP 方法:

HTTP 方法操作幂等性安全性
GET获取资源✅ 是✅ 是
POST创建资源❌ 否❌ 否
PUT完整更新✅ 是❌ 否
PATCH部分更新❌ 否❌ 否
DELETE删除资源✅ 是❌ 否

幂等性:多次执行相同操作,结果不变 安全性:操作不会改变服务器状态

3. 无状态(Stateless)#

每个请求都包含所有必要信息,服务器不保存客户端状态:

错误示例(有状态):

# 第一次请求
POST /api/login
{ username: "alice", password: "123456" }
# 第二次请求(依赖第一次请求的状态)
GET /api/profile # 服务器需要知道当前用户是谁

正确示例(无状态):

# 第一次请求
POST /api/login
{ username: "alice", password: "123456" }
# 返回:{ token: "abc123" }
# 第二次请求(携带 token)
GET /api/profile
Authorization: Bearer abc123
4. 可缓存(Cacheable)#

GET 请求应该可以被缓存:

// 设置缓存头
app.get('/api/posts', (req, res) => {
res.set('Cache-Control', 'public, max-age=3600'); // 缓存 1 小时
res.json(posts);
});
5. 分层系统(Layered System)#

客户端不需要知道是否直接连接服务器:

客户端 → 负载均衡器 → API 网关 → 应用服务器 → 数据库

📋 RESTful API 设计最佳实践#

1. 使用复数形式命名资源#

正确:

GET /api/users
GET /api/posts
GET /api/comments

错误:

GET /api/user
GET /api/post
GET /api/comment
2. 使用小写字母和连字符#

正确:

GET /api/user-profiles
GET /api/blog-posts

错误:

GET /api/userProfiles # 驼峰命名
GET /api/user_profiles # 下划线
GET /api/UserProfiles # 大写
3. 使用嵌套资源表示关系#

正确:

GET /api/users/123/posts # 获取用户的文章
GET /api/posts/456/comments # 获取文章的评论
POST /api/users/123/posts # 为用户创建文章

错误:

GET /api/posts?userId=123 # 使用查询参数
GET /api/user/123/post # 使用单数
4. 合理使用查询参数#

正确:

GET /api/posts?page=2&limit=10 # 分页
GET /api/posts?category=tech # 筛选
GET /api/posts?sort=date&order=desc # 排序
GET /api/posts?q=nodejs # 搜索
5. 使用标准状态码#
状态码含义使用场景
200OK请求成功
201Created资源创建成功
204No Content删除成功(无返回内容)
400Bad Request请求参数错误
401Unauthorized未认证
403Forbidden无权限
404Not Found资源不存在
409Conflict资源冲突(如重复创建)
500Internal Server Error服务器错误
6. 统一的响应格式#
[{
"success": true,
"data": {
"id": 1,
"name": "张三"
}
},
{
"success": false,
"error": {
"code": "USER_NOT_FOUND",
"message": "用户不存在",
"details": {}
}
}]
7. 版本控制#

正确:

GET /api/v1/users
GET /api/v2/users

错误:

GET /api/users/v1
GET /api/users?version=1

🚫 常见错误示例#

错误 1:在 URL 中使用动词#

错误:

GET /api/getUsers
POST /api/createUser
PUT /api/updateUser/1

正确:

GET /api/users
POST /api/users
PUT /api/users/1
错误 2:返回不一致的数据结构#

错误:

# 成功时
{ "id": 1, "name": "张三" }
# 失败时
{ "error": "用户不存在" }

正确:

# 成功时
{
"success": true,
"data": { "id": 1, "name": "张三" }
}
# 失败时
{
"success": false,
"error": {
"code": "USER_NOT_FOUND",
"message": "用户不存在"
}
}
错误 3:不使用正确的 HTTP 状态码#

错误:

# 资源不存在
GET /api/users/999
# 返回 200 OK
{ "error": "用户不存在" }

正确:

# 资源不存在
GET /api/users/999
# 返回 404 Not Found
{
"success": false,
"error": {
"code": "USER_NOT_FOUND",
"message": "用户不存在"
}
}

13.2 实现用户 API#

// 模拟数据库
let users = [
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' }
];
// 获取所有用户
app.get('/api/users', (req, res) => {
res.json(users);
});
// 获取单个用户
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: '用户不存在' });
}
res.json(user);
});
// 创建用户
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
const newUser = {
id: users.length + 1,
name,
email
};
users.push(newUser);
res.status(201).json(newUser);
});
// 更新用户
app.put('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: '用户不存在' });
}
user.name = req.body.name || user.name;
user.email = req.body.email || user.email;
res.json(user);
});
// 删除用户
app.delete('/api/users/:id', (req, res) => {
const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));
if (userIndex === -1) {
return res.status(404).json({ error: '用户不存在' });
}
users.splice(userIndex, 1);
res.status(204).send();
});

第十四步:数据库集成#

💡 数据库设计思路与注意事项#

🏗️ 数据库设计的重要性#

数据库设计就像建筑的地基,设计得好,后续开发会事半功倍;设计得不好,后期维护会非常痛苦。

📋 数据库设计的基本原则#

1. 理解业务需求#

在开始设计之前,先问自己这些问题:

  • 🤔 需要存储什么数据?(用户、文章、评论、订单…)
  • 🤔 数据之间有什么关系?(用户-文章、文章-评论…)
  • 🤔 数据的访问模式是什么?(频繁查询、偶尔更新…)
  • 🤔 数据量有多大?(几千条、几百万条…)
2. 选择合适的数据库#
数据库类型适用场景优点缺点
MongoDB文档型数据、快速迭代灵活、易扩展、无模式事务支持较弱
MySQL关系型数据、事务要求高成熟、稳定、事务支持强扩展性相对较差
PostgreSQL复杂查询、数据分析功能强大、支持 JSON配置相对复杂
Redis缓存、会话、计数器极快、支持多种数据结构数据不能太大

生活比喻:

  • MongoDB 就像文件夹,可以放任何类型的文件
  • MySQL 就像Excel 表格,结构固定,关系明确
  • Redis 就像白板,快速读写,但容量有限
3. 数据库设计原则#

🎯 MongoDB 设计原则#

原则 1:嵌入 vs 引用

嵌入(Embedding):把相关数据放在同一个文档中

适用场景:

  • 数据量小(通常 < 1000 条)
  • 数据经常一起查询
  • 数据很少单独更新
{
"_id": "...",
"name": "张三",
"email": "zhangsan@example.com",
"addresses": [
{
"type": "home",
"street": "长安街 1 号",
"city": "北京"
},
{
"type": "work",
"street": "科技路 2 号",
"city": "上海"
}
]
}

引用(Referencing):使用 ID 引用其他文档

适用场景:

  • 数据量大
  • 数据需要单独查询和更新
  • 数据之间关系复杂
[{
"_id": "...",
"name": "张三",
"email": "zhangsan@example.com"
},
{
"_id": "...",
"title": "我的第一篇文章",
"content": "...",
"author": "..."
}]

生活比喻:

  • 嵌入:就像把照片直接贴在相册里
  • 引用:就像在相册里写”见第 3 页”

原则 2:避免过深的嵌套

错误示例:

{
"title": "文章标题",
"comments": [
{
"text": "评论1",
"replies": [
{
"text": "回复1",
"replies": [
{
"text": "回复的回复",
"replies": []
}
]
}
]
}
]
}

正确示例:

[{
"_id": "...",
"title": "文章标题",
"comments": ["...", "..."]
},
{
"_id": "...",
"text": "评论1",
"articleId": "...",
"parentId": null
},
{
"_id": "...",
"text": "回复1",
"articleId": "...",
"parentId": "..."
}]

🎯 MySQL 设计原则#

原则 1:规范化(Normalization)

规范化是为了减少数据冗余,避免数据不一致。

第一范式(1NF):每个字段都是不可分割的

错误示例:

-- 用户字段包含多个值
CREATE TABLE users (
id INT,
name VARCHAR(100),
phones VARCHAR(200) -- "13800000001,13800000002"
);

正确示例:

-- 分开存储
CREATE TABLE users (
id INT,
name VARCHAR(100)
);
CREATE TABLE user_phones (
id INT,
user_id INT,
phone VARCHAR(20)
);

第二范式(2NF):非主键字段完全依赖于主键

错误示例:

-- 订单表中包含商品信息
CREATE TABLE orders (
id INT,
user_id INT,
product_name VARCHAR(100), -- 依赖于商品,不依赖于订单
product_price DECIMAL(10,2)
);

正确示例:

-- 订单表
CREATE TABLE orders (
id INT,
user_id INT,
created_at DATETIME
);
-- 订单详情表
CREATE TABLE order_items (
id INT,
order_id INT,
product_id INT,
quantity INT,
price DECIMAL(10,2)
);
-- 商品表
CREATE TABLE products (
id INT,
name VARCHAR(100),
price DECIMAL(10,2)
);

原则 2:合理使用索引

索引就像书的目录,可以快速找到内容。

应该创建索引的字段:

  • 经常用于查询的字段(WHERE)
  • 经常用于排序的字段(ORDER BY)
  • 经常用于连接的字段(JOIN)
  • 唯一性字段(UNIQUE)
-- 为常用查询字段创建索引
CREATE INDEX idx_user_email ON users(email);
CREATE INDEX idx_post_author ON posts(author_id);
CREATE INDEX idx_post_created ON posts(created_at DESC);

不应该创建索引的字段:

  • 很少查询的字段
  • 数据量很小的表
  • 频繁更新的字段(索引会降低更新速度)

原则 3:使用外键约束

外键可以保证数据的一致性。

-- 创建外键约束
CREATE TABLE posts (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(200),
author_id INT,
FOREIGN KEY (author_id) REFERENCES users(id)
ON DELETE CASCADE -- 删除用户时,同时删除其文章
ON UPDATE CASCADE -- 更新用户 ID 时,同时更新文章
);

⚠️ 常见错误和注意事项#

错误 1:过度设计#

错误: 为了”完美”,创建了太多的表和关系

正确: 根据实际需求设计,够用就好

错误 2:缺少索引#

错误: 所有查询都是全表扫描

正确: 为常用查询字段创建索引

错误 3:不使用事务#

错误: 转账操作没有事务保护

// 危险!如果第二步失败,钱已经扣了但没到账
// 假设这是在 async 函数中
await db.query('UPDATE accounts SET balance = balance - 100 WHERE id = 1');
await db.query('UPDATE accounts SET balance = balance + 100 WHERE id = 2');

正确: 使用事务确保数据一致性

// 安全!要么全部成功,要么全部失败
// 假设这是在 async 函数中
await db.beginTransaction();
try {
await db.query('UPDATE accounts SET balance = balance - 100 WHERE id = 1');
await db.query('UPDATE accounts SET balance = balance + 100 WHERE id = 2');
await db.commit();
} catch (error) {
await db.rollback();
throw error;
}
错误 4:不处理数据库错误#

错误: 数据库错误直接抛出,暴露敏感信息

// 假设这是在 async 函数中
const users = await db.query('SELECT * FROM users');
// 如果数据库连接失败,会暴露数据库配置信息

正确: 捕获并处理错误,返回友好的错误信息

// 假设这是在 async 函数中
try {
const users = await db.query('SELECT * FROM users');
res.json(users);
} catch (error) {
console.error('数据库错误:', error);
res.status(500).json({
error: '获取用户列表失败'
});
}
错误 5:N+1 查询问题#

错误: 在循环中查询数据库

// 假设这是在 async 函数中
// 获取所有文章
const posts = await Post.find();
// 为每篇文章获取作者信息(N+1 查询)
for (const post of posts) {
post.author = await User.findById(post.authorId);
}

正确: 使用关联查询或批量查询

// 假设这是在 async 函数中
// 使用 populate(MongoDB)
const posts = await Post.find().populate('author');
// 或者批量查询
const posts = await Post.find();
const authorIds = posts.map(p => p.authorId);
const authors = await User.find({ _id: { $in: authorIds } });

📊 数据库性能优化建议#

  1. 使用连接池:避免频繁创建和销毁连接
  2. 合理使用缓存:将热点数据缓存到 Redis
  3. 分页查询:避免一次性加载大量数据
  4. 定期维护:清理无用数据,优化索引
  5. 监控性能:使用工具监控慢查询

14.1 使用 MongoDB#

Terminal window
npm install mongoose
const mongoose = require('mongoose');
// 连接数据库
mongoose.connect('mongodb://localhost:27017/myapp', {
useNewUrlParser: true,
useUnifiedTopology: true
});
// 定义模型
const User = mongoose.model('User', {
name: String,
email: String
});
// 创建用户
app.post('/api/users', async (req, res) => {
try {
const user = await User.create(req.body);
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// 获取所有用户
app.get('/api/users', async (req, res) => {
try {
const users = await User.find();
res.json(users);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
WARNING

⚠️ 数据库连接错误处理:在实际应用中,数据库连接可能会失败(如数据库服务未启动、网络问题等)。务必添加数据库连接的错误处理,否则应用会在启动时就崩溃。建议在 connect() 后添加 .catch() 处理连接错误。

14.2 使用 MySQL#

Terminal window
npm install mysql2
const mysql = require('mysql2/promise');
// 创建连接池
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'myapp',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// 查询用户
app.get('/api/users', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM users');
res.json(rows);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

第十五步:测试基础#

测试是保证代码质量的重要手段,可以帮助你发现和修复错误,提高代码的可靠性。

💡 为什么需要测试?#

生活比喻:

  • 不测试代码:就像盖房子不检查地基,随时可能倒塌
  • 测试代码:就像开车前检查刹车,确保安全

测试的好处:

  • 发现错误:在开发阶段发现错误,而不是在生产环境
  • 保证质量:确保代码按预期工作
  • 重构信心:修改代码时不用担心破坏现有功能
  • 文档作用:测试代码本身就是最好的文档
  • 提高效率:快速验证功能,减少手动测试时间

🗺️ 测试类型#

graph TD A[测试] --> B[单元测试<br/>测试单个函数/组件] A --> C[集成测试<br/>测试模块之间的交互] A --> D[端到端测试<br/>测试完整流程] B --> B1[Jest<br/>Mocha] C --> C1[Supertest<br/>Jest] D --> D1[Cypress<br/>Puppeteer] style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#fff9c4 style D fill:#e8f5e9 style B1 fill:#c8e6c9 style C1 fill:#c8e6c9 style D1 fill:#c8e6c9

15.1 单元测试#

单元测试是测试代码中最小的可测试部分(通常是函数或方法)。

📦 安装 Jest#

Jest 是一个流行的 JavaScript 测试框架。

Terminal window
npm install --save-dev jest

package.json 中添加测试脚本:

{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}

🎯 编写第一个单元测试#

创建一个简单的工具函数 utils.js

utils.js
/**
* 计算两个数的和
* @param {number} a - 第一个数
* @param {number} b - 第二个数
* @returns {number} 两数之和
*/
function add(a, b) {
return a + b;
}
/**
* 格式化日期
* @param {Date} date - 日期对象
* @returns {string} 格式化后的日期字符串
*/
function formatDate(date) {
return date.toISOString().split('T')[0];
}
/**
* 验证邮箱格式
* @param {string} email - 邮箱地址
* @returns {boolean} 是否有效
*/
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
module.exports = {
add,
formatDate,
isValidEmail
};

创建测试文件 utils.test.js

utils.test.js
const { add, formatDate, isValidEmail } = require('./utils');
describe('add 函数测试', () => {
test('应该正确计算两个正数的和', () => {
expect(add(1, 2)).toBe(3);
});
test('应该正确计算负数的和', () => {
expect(add(-1, -2)).toBe(-3);
});
test('应该正确处理零', () => {
expect(add(0, 5)).toBe(5);
expect(add(5, 0)).toBe(5);
});
test('应该正确处理小数', () => {
expect(add(0.1, 0.2)).toBeCloseTo(0.3);
});
});
describe('formatDate 函数测试', () => {
test('应该正确格式化日期', () => {
const date = new Date('2024-01-15');
expect(formatDate(date)).toBe('2024-01-15');
});
test('应该处理不同时区的日期', () => {
const date = new Date('2024-01-15T00:00:00Z');
const result = formatDate(date);
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
});
});
describe('isValidEmail 函数测试', () => {
test('应该验证有效的邮箱', () => {
expect(isValidEmail('test@example.com')).toBe(true);
expect(isValidEmail('user.name+tag@domain.co.uk')).toBe(true);
});
test('应该拒绝无效的邮箱', () => {
expect(isValidEmail('invalid')).toBe(false);
expect(isValidEmail('invalid@')).toBe(false);
expect(isValidEmail('@example.com')).toBe(false);
expect(isValidEmail('test@example')).toBe(false);
});
});

运行测试:

Terminal window
npm test

📚 Jest 常用断言#

断言说明示例
toBe()严格相等expect(1 + 1).toBe(2)
toEqual()深度相等expect({a: 1}).toEqual({a: 1})
toBeCloseTo()浮点数近似expect(0.1 + 0.2).toBeCloseTo(0.3)
toMatch()正则匹配expect('hello').toMatch(/ell/)
toContain()包含元素expect([1, 2, 3]).toContain(2)
toThrow()抛出异常expect(() => fn()).toThrow()
toBeTruthy()真值expect(true).toBeTruthy()
toBeFalsy()假值expect(false).toBeFalsy()
toBeNull()nullexpect(null).toBeNull()
toBeUndefined()undefinedexpect(undefined).toBeUndefined()

15.2 集成测试#

集成测试是测试多个模块或组件之间的交互。

📦 安装 Supertest#

Supertest 是一个专门用于测试 HTTP 服务器的库。

Terminal window
npm install --save-dev supertest

🎯 测试 Express.js 路由#

创建一个简单的 Express 应用 app.js

const express = require('express');
const app = express();
app.use(express.json());
// 模拟数据库
let users = [
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' }
];
// 获取所有用户
app.get('/api/users', (req, res) => {
res.json(users);
});
// 获取单个用户
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: '用户不存在' });
}
res.json(user);
});
// 创建用户
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: '姓名和邮箱不能为空' });
}
const newUser = {
id: users.length + 1,
name,
email
};
users.push(newUser);
res.status(201).json(newUser);
});
// 更新用户
app.put('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: '用户不存在' });
}
user.name = req.body.name || user.name;
user.email = req.body.email || user.email;
res.json(user);
});
// 删除用户
app.delete('/api/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: '用户不存在' });
}
users.splice(index, 1);
res.status(204).send();
});
module.exports = app;

创建测试文件 app.test.js

const request = require('supertest');
const app = require('./app');
describe('用户 API 测试', () => {
describe('GET /api/users', () => {
test('应该返回所有用户', async () => {
const response = await request(app)
.get('/api/users')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toBeInstanceOf(Array);
expect(response.body.length).toBeGreaterThan(0);
expect(response.body[0]).toHaveProperty('id');
expect(response.body[0]).toHaveProperty('name');
expect(response.body[0]).toHaveProperty('email');
});
});
describe('GET /api/users/:id', () => {
test('应该返回指定用户', async () => {
const response = await request(app)
.get('/api/users/1')
.expect(200);
expect(response.body).toHaveProperty('id', 1);
expect(response.body).toHaveProperty('name', '张三');
});
test('用户不存在时应该返回 404', async () => {
const response = await request(app)
.get('/api/users/999')
.expect(404);
expect(response.body).toHaveProperty('error', '用户不存在');
});
});
describe('POST /api/users', () => {
test('应该创建新用户', async () => {
const newUser = {
name: '王五',
email: 'wangwu@example.com'
};
const response = await request(app)
.post('/api/users')
.send(newUser)
.expect('Content-Type', /json/)
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe(newUser.name);
expect(response.body.email).toBe(newUser.email);
});
test('缺少必要字段时应该返回 400', async () => {
const invalidUser = {
name: '赵六'
// 缺少 email
};
const response = await request(app)
.post('/api/users')
.send(invalidUser)
.expect(400);
expect(response.body).toHaveProperty('error', '姓名和邮箱不能为空');
});
test('邮箱格式无效时应该返回 400', async () => {
const invalidUser = {
name: '赵六',
email: 'invalid-email'
};
const response = await request(app)
.post('/api/users')
.send(invalidUser)
.expect(400);
});
});
describe('PUT /api/users/:id', () => {
test('应该更新用户信息', async () => {
const updatedData = {
name: '张三(更新)'
};
const response = await request(app)
.put('/api/users/1')
.send(updatedData)
.expect(200);
expect(response.body.name).toBe(updatedData.name);
});
test('用户不存在时应该返回 404', async () => {
const response = await request(app)
.put('/api/users/999')
.send({ name: '测试' })
.expect(404);
});
});
describe('DELETE /api/users/:id', () => {
test('应该删除用户', async () => {
await request(app)
.delete('/api/users/2')
.expect(204);
// 验证用户已被删除
await request(app)
.get('/api/users/2')
.expect(404);
});
test('用户不存在时应该返回 404', async () => {
await request(app)
.delete('/api/users/999')
.expect(404);
});
});
});

15.3 测试数据库操作#

🎯 使用测试数据库#

在生产环境中,应该使用独立的测试数据库,避免影响生产数据。

app.test.js
const mongoose = require('mongoose');
const User = require('./models/User');
describe('用户模型测试', () => {
// 在所有测试之前连接数据库
beforeAll(async () => {
await mongoose.connect('mongodb://localhost:27017/test_db', {
useNewUrlParser: true,
useUnifiedTopology: true
});
});
// 在所有测试之后断开连接
afterAll(async () => {
await mongoose.connection.close();
});
// 在每个测试之前清空数据库
beforeEach(async () => {
await User.deleteMany({});
});
test('应该创建用户', async () => {
const userData = {
name: '张三',
email: 'zhangsan@example.com'
};
const user = await User.create(userData);
expect(user).toHaveProperty('_id');
expect(user.name).toBe(userData.name);
expect(user.email).toBe(userData.email);
});
test('应该查找用户', async () => {
const userData = {
name: '李四',
email: 'lisi@example.com'
};
await User.create(userData);
const user = await User.findOne({ email: userData.email });
expect(user).not.toBeNull();
expect(user.name).toBe(userData.name);
});
test('应该更新用户', async () => {
const user = await User.create({
name: '王五',
email: 'wangwu@example.com'
});
user.name = '王五(更新)';
await user.save();
const updatedUser = await User.findById(user._id);
expect(updatedUser.name).toBe('王五(更新)');
});
test('应该删除用户', async () => {
const user = await User.create({
name: '赵六',
email: 'zhaoliu@example.com'
});
await User.findByIdAndDelete(user._id);
const deletedUser = await User.findById(user._id);
expect(deletedUser).toBeNull();
});
});

15.4 测试异步代码#

🎯 测试异步函数#

asyncFunctions.js
/**
* 模拟异步获取用户数据
* @param {number} userId - 用户 ID
* @returns {Promise<Object>} 用户数据
*/
async function fetchUser(userId) {
// 模拟 API 调用
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId > 0) {
resolve({
id: userId,
name: `用户${userId}`,
email: `user${userId}@example.com`
});
} else {
reject(new Error('无效的用户 ID'));
}
}, 100);
});
}
/**
* 模拟异步保存用户数据
* @param {Object} userData - 用户数据
* @returns {Promise<Object>} 保存后的用户数据
*/
async function saveUser(userData) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
...userData,
id: Date.now(),
createdAt: new Date()
});
}, 100);
});
}
module.exports = {
fetchUser,
saveUser
};

测试文件:

asyncFunctions.test.js
const { fetchUser, saveUser } = require('./asyncFunctions');
describe('fetchUser 函数测试', () => {
test('应该成功获取用户数据', async () => {
const user = await fetchUser(1);
expect(user).toHaveProperty('id', 1);
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('email');
});
test('无效 ID 时应该抛出错误', async () => {
await expect(fetchUser(-1)).rejects.toThrow('无效的用户 ID');
});
test('应该处理超时情况', async () => {
// 设置超时时间
await expect(fetchUser(1)).resolves.toBeDefined();
}, 1000); // 1 秒超时
});
describe('saveUser 函数测试', () => {
test('应该成功保存用户数据', async () => {
const userData = {
name: '张三',
email: 'zhangsan@example.com'
};
const savedUser = await saveUser(userData);
expect(savedUser).toHaveProperty('id');
expect(savedUser).toHaveProperty('createdAt');
expect(savedUser.name).toBe(userData.name);
});
});

15.5 测试覆盖率#

测试覆盖率衡量代码被测试的程度。

📊 生成覆盖率报告#

Terminal window
npm run test:coverage

Jest 会生成一个覆盖率报告,通常包含:

  • 语句覆盖率(Statements):有多少语句被执行
  • 分支覆盖率(Branches):有多少分支被测试
  • 函数覆盖率(Functions):有多少函数被调用
  • 行覆盖率(Lines):有多少行代码被执行

🎯 配置覆盖率阈值#

package.json 中配置:

{
"jest": {
"collectCoverage": true,
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}

15.6 测试最佳实践#

✅ DO(应该做的)#

  1. 测试应该独立
// ✅ 每个测试独立运行
test('应该创建用户', async () => {
const user = await User.create({ name: '张三' });
expect(user.name).toBe('张三');
});
test('应该删除用户', async () => {
// 不依赖前面的测试
const user = await User.create({ name: '李四' });
await User.findByIdAndDelete(user._id);
const deleted = await User.findById(user._id);
expect(deleted).toBeNull();
});
  1. 使用描述性的测试名称
// ✅ 清晰的测试名称
test('当用户不存在时,应该返回 404', async () => {
// ...
});
// ❌ 不清晰的测试名称
test('测试 1', async () => {
// ...
});
  1. 遵循 AAA 模式(Arrange-Act-Assert)
test('应该更新用户信息', async () => {
// Arrange(准备)
const user = await User.create({ name: '张三' });
const updatedData = { name: '张三(更新)' };
// Act(执行)
user.name = updatedData.name;
await user.save();
// Assert(断言)
const updatedUser = await User.findById(user._id);
expect(updatedUser.name).toBe(updatedData.name);
});
  1. 使用 beforeEach 和 afterEach
describe('用户 API 测试', () => {
beforeEach(async () => {
// 每个测试前执行
await User.deleteMany({});
});
afterEach(async () => {
// 每个测试后执行
await User.deleteMany({});
});
test('应该创建用户', async () => {
// 测试代码
});
});

❌ DON’T(不应该做的)#

  1. 不要测试第三方库
// ❌ 不要测试 Express.js 本身
test('Express 应该能处理 GET 请求', async () => {
// 这是 Express.js 的责任,不是你的
});
// ✅ 测试你的业务逻辑
test('应该返回用户列表', async () => {
const response = await request(app).get('/api/users');
expect(response.body).toBeInstanceOf(Array);
});
  1. 不要在测试中硬编码数据
// ❌ 硬编码
test('应该返回用户', async () => {
const user = await User.findById(1);
expect(user.name).toBe('张三');
});
// ✅ 动态创建
test('应该返回用户', async () => {
const user = await User.create({ name: '张三' });
const found = await User.findById(user._id);
expect(found.name).toBe('张三');
});
  1. 不要忽略异步操作
// ❌ 忘记 await
test('应该创建用户', async () => {
User.create({ name: '张三' });
expect(User.count()).toBe(1); // 可能失败
});
// ✅ 正确处理异步
test('应该创建用户', async () => {
await User.create({ name: '张三' });
expect(await User.count()).toBe(1);
});

15.7 常见测试场景#

场景 1:测试身份验证#

describe('身份验证测试', () => {
let token;
beforeEach(async () => {
// 登录获取 token
const response = await request(app)
.post('/api/login')
.send({ username: 'admin', password: 'password' });
token = response.body.token;
});
test('应该拒绝未认证的请求', async () => {
await request(app)
.get('/api/protected')
.expect(401);
});
test('应该接受有效的 token', async () => {
await request(app)
.get('/api/protected')
.set('Authorization', `Bearer ${token}`)
.expect(200);
});
test('应该拒绝无效的 token', async () => {
await request(app)
.get('/api/protected')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
});

场景 2:测试文件上传#

describe('文件上传测试', () => {
test('应该成功上传文件', async () => {
const response = await request(app)
.post('/api/upload')
.attach('file', './test/fixtures/test.png')
.expect(200);
expect(response.body).toHaveProperty('filename');
expect(response.body).toHaveProperty('path');
});
test('应该拒绝不支持的文件类型', async () => {
await request(app)
.post('/api/upload')
.attach('file', './test/fixtures/test.exe')
.expect(400);
});
});

场景 3:测试错误处理#

describe('错误处理测试', () => {
test('应该捕获数据库错误', async () => {
// 模拟数据库错误
jest.spyOn(User, 'create').mockRejectedValue(new Error('数据库错误'));
await request(app)
.post('/api/users')
.send({ name: '张三', email: 'zhangsan@example.com' })
.expect(500);
});
test('应该返回友好的错误消息', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: '张三' }) // 缺少 email
.expect(400);
expect(response.body).toHaveProperty('error');
expect(response.body.error).toContain('邮箱');
});
});

15.8 持续集成中的测试#

在 CI/CD 流程中自动运行测试。

📝 GitHub Actions 示例#

创建 .github/workflows/test.yml

name: 运行测试
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
steps:
- uses: actions/checkout@v2
- name: 使用 Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: 安装依赖
run: npm ci
- name: 运行测试
run: npm test
- name: 生成覆盖率报告
run: npm run test:coverage
- name: 上传覆盖率报告
uses: codecov/codecov-action@v2

15.9 测试工具推荐#

工具用途特点
Jest单元测试、集成测试零配置、断言丰富、覆盖率报告
SupertestHTTP 测试专门测试 Express.js 应用
Mocha单元测试灵活、可扩展
Chai断言库可读性强
SinonMock/Stub模拟函数和对象
Cypress端到端测试真实浏览器环境
Istanbul覆盖率工具详细的覆盖率报告

第十六步:实战练习#

TIP

💡 实践是最好的学习方式:阅读文档只能让你”知道”,动手实践才能让你”真正掌握”。请务必完成这些练习,不要只是看代码!遇到问题时,尝试自己解决,这是提升编程能力的最佳途径。

16.1 练习 1:创建待办事项 API#

let todos = [];
// 获取所有待办事项
app.get('/api/todos', (req, res) => {
res.json(todos);
});
// 创建待办事项
app.post('/api/todos', (req, res) => {
const { text } = req.body;
const todo = {
id: Date.now(),
text,
completed: false
};
todos.push(todo);
res.status(201).json(todo);
});
// 更新待办事项
app.put('/api/todos/:id', (req, res) => {
const todo = todos.find(t => t.id === parseInt(req.params.id));
if (!todo) {
return res.status(404).json({ error: '待办事项不存在' });
}
todo.text = req.body.text || todo.text;
todo.completed = req.body.completed !== undefined ? req.body.completed : todo.completed;
res.json(todo);
});
// 删除待办事项
app.delete('/api/todos/:id', (req, res) => {
const index = todos.findIndex(t => t.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: '待办事项不存在' });
}
todos.splice(index, 1);
res.status(204).send();
});

16.2 练习 2:创建博客 API#

let posts = [];
// 获取所有文章
app.get('/api/posts', (req, res) => {
const { category } = req.query;
let filteredPosts = posts;
if (category) {
filteredPosts = posts.filter(p => p.category === category);
}
res.json(filteredPosts);
});
// 获取单篇文章
app.get('/api/posts/:id', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) {
return res.status(404).json({ error: '文章不存在' });
}
res.json(post);
});
// 创建文章
app.post('/api/posts', (req, res) => {
const { title, content, category } = req.body;
const post = {
id: Date.now(),
title,
content,
category,
createdAt: new Date(),
updatedAt: new Date()
};
posts.push(post);
res.status(201).json(post);
});
// 更新文章
app.put('/api/posts/:id', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) {
return res.status(404).json({ error: '文章不存在' });
}
post.title = req.body.title || post.title;
post.content = req.body.content || post.content;
post.category = req.body.category || post.category;
post.updatedAt = new Date();
res.json(post);
});
// 删除文章
app.delete('/api/posts/:id', (req, res) => {
const index = posts.findIndex(p => p.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: '文章不存在' });
}
posts.splice(index, 1);
res.status(204).send();
});

第十七步:项目结构最佳实践#

🗺️ Express.js 应用架构#

graph TD A[Express.js 应用] --> B[客户端<br/>浏览器/移动端] A --> C[API 网关] C --> D[路由层<br/>routes目录] D --> D1[index.js] D --> D2[users.js] D --> D3[posts.js] D1 --> E[中间件层<br/>middleware目录] D2 --> E D3 --> E E --> E1[auth.js<br/>身份验证] E --> E2[error.js<br/>错误处理] E --> E3[logger.js<br/>日志记录] E1 --> F[控制器层<br/>controllers目录] E2 --> F E3 --> F F --> F1[用户控制器] F --> F2[文章控制器] F1 --> G[服务层<br/>services目录] F2 --> G G --> G1[用户服务] G --> G2[文章服务] G1 --> H[数据访问层<br/>models目录] G2 --> H H --> H1[User.js<br/>MongoDB Schema] H --> H2[Post.js<br/>MongoDB Schema] H1 --> I[数据库<br/>MongoDB/MySQL] H2 --> I A --> J[静态文件<br/>public目录] A --> K[模板文件<br/>views目录] A --> L[配置文件<br/>config目录] style A fill:#e1f5ff style B fill:#c8e6c9 style C fill:#fff4e1 style D fill:#fff9c4 style E fill:#fce4ec style F fill:#e8f5e9 style G fill:#fff4e1 style H fill:#fce4ec style I fill:#ffccbc style J fill:#c8e6c9 style K fill:#c8e6c9 style L fill:#c8e6c9

分层架构说明:

  • 路由层:定义 API 端点,处理请求路由
  • 中间件层:处理身份验证、错误处理、日志等
  • 控制器层:处理业务逻辑,协调服务层
  • 服务层:封装业务逻辑,处理复杂操作
  • 数据访问层:定义数据模型,与数据库交互

17.1 推荐的项目结构#

my-express-app/
├── bin/
│ └── www # 应用启动脚本
├── public/ # 静态文件
│ ├── images/
│ ├── javascripts/
│ └── stylesheets/
├── routes/ # 路由文件
│ ├── index.js
│ ├── users.js
│ └── posts.js
├── views/ # 模板文件
│ ├── index.ejs
│ └── error.ejs
├── models/ # 数据模型
│ ├── User.js
│ └── Post.js
├── middleware/ # 自定义中间件
│ ├── auth.js
│ └── error.js
├── config/ # 配置文件
│ ├── database.js
│ └── config.js
├── app.js # 应用主文件
└── package.json

17.2 环境变量#

Terminal window
npm install dotenv

创建 .env 文件:

PORT=3000
NODE_ENV=development
DB_URI=mongodb://localhost:27017/myapp

在应用中使用:

require('dotenv').config();
const PORT = process.env.PORT || 3000;
const DB_URI = process.env.DB_URI;
app.listen(PORT, () => {
console.log(`服务器运行在端口 ${PORT}`);
});

第十八步:部署上线#

TIP

💡 部署前的准备工作:在部署应用之前,请确保:

  1. ✅ 已完成所有功能测试
  2. ✅ 已修复所有已知 bug
  3. ✅ 已配置环境变量(数据库连接、密钥等)
  4. ✅ 已设置错误日志和监控
  5. ✅ 已配置 HTTPS(生产环境)
  6. ✅ 已准备好回滚方案

18.1 准备部署#

Terminal window
# 安装生产依赖
npm install --production
# 设置环境变量
export NODE_ENV=production
export PORT=80

18.2 使用 PM2 管理进程#

Terminal window
# 全局安装 PM2
npm install -g pm2
# 启动应用
pm2 start app.js --name my-app
# 查看状态
pm2 status
# 查看日志
pm2 logs
# 重启应用
pm2 restart my-app
# 停止应用
pm2 stop my-app
# 设置开机自启动
pm2 startup
pm2 save

18.3 使用 Docker#

创建 Dockerfile

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]

构建和运行:

Terminal window
# 构建镜像
docker build -t my-express-app .
# 运行容器
docker run -p 3000:3000 my-express-app

18.4 部署到云平台#

Vercel#

Terminal window
npm install -g vercel
vercel

Heroku#

Terminal window
# 安装 Heroku CLI
npm install -g heroku
# 登录
heroku login
# 创建应用
heroku create
# 部署
git push heroku main

第十九步:学习资源推荐#

官方资源#

  1. Express.js 官方文档

  2. Node.js 官方文档

教程推荐#

  1. Express.js 教程(菜鸟教程)

  2. Express.js 视频教程

    • B站搜索”Express.js 入门”

实践项目#

  1. 简单的博客系统
  2. 待办事项应用
  3. RESTful API 服务
  4. 实时聊天应用(使用 Socket.io)

第二十步:常见问题#

Q1: Express.js 和 Koa.js 有什么区别?#

A:

  • Express.js:更成熟,中间件生态丰富,适合快速开发
  • Koa.js:更轻量,使用 async/await,更灵活,适合有经验的开发者

Q2: 如何处理跨域问题?#

A: 使用 cors 中间件:

const cors = require('cors');
app.use(cors());

Q3: 如何实现用户认证?#

A: 使用 jsonwebtokenbcrypt

Terminal window
npm install jsonwebtoken bcrypt

Q4: 如何优化 Express.js 应用性能?#

A:

  • 使用 gzip 压缩
  • 启用缓存
  • 使用集群模式
  • 优化数据库查询
  • 使用 CDN

Q5: Express.js 适合大型项目吗?#

A: Express.js 本身是轻量级的,但通过合理的项目结构和中间件组合,完全可以用于大型项目。对于复杂项目,可以考虑使用基于 Express 的框架,如 NestJS。


第二十一步:性能优化#

NOTE

📝 性能优化的时机不要过早优化!在应用功能完善之前,不要花太多时间优化性能。先让应用跑起来,然后通过性能分析工具找出真正的瓶颈,再有针对性地进行优化。过早优化往往是浪费时间。

性能优化是构建高性能 Express.js 应用的关键。通过合理的优化策略,可以显著提升应用的响应速度和并发处理能力。

11.1 响应时间优化#

使用 gzip 压缩#

Terminal window
npm install compression
const compression = require('compression');
// 启用 gzip 压缩
app.use(compression());
// 配置压缩选项
app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) {
// 不压缩请求头包含 x-no-compression 的请求
return false;
}
return compression.filter(req, res);
},
threshold: 1024 // 只压缩大于 1KB 的响应
}));

减少响应数据量#

// ❌ 不好的做法:返回所有字段
app.get('/api/users', async (req, res) => {
const users = await User.find();
res.json(users);
});
// ✅ 好的做法:只返回需要的字段
app.get('/api/users', async (req, res) => {
const users = await User.find({}, { name: 1, email: 1 });
res.json(users);
});
// ✅ 更好的做法:使用分页
app.get('/api/users', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
const [users, total] = await Promise.all([
User.find().skip(skip).limit(limit),
User.countDocuments()
]);
res.json({
data: users,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
});
});

11.2 内存优化#

使用流式处理大文件#

const fs = require('fs');
// ❌ 不好的做法:一次性读取整个文件到内存
app.get('/download', (req, res) => {
const data = fs.readFileSync('large-file.pdf');
res.send(data);
});
// ✅ 好的做法:使用流式处理
app.get('/download', (req, res) => {
const fileStream = fs.createReadStream('large-file.pdf');
fileStream.pipe(res);
});

及时释放资源#

// ❌ 不好的做法:缓存所有数据
const cache = {};
app.get('/api/data', async (req, res) => {
const key = req.params.id;
if (!cache[key]) {
cache[key] = await fetchData(key);
}
res.json(cache[key]);
});
// ✅ 好的做法:使用 LRU 缓存,限制缓存大小
const LRU = require('lru-cache');
const cache = new LRU({
max: 500, // 最多缓存 500 个项目
maxAge: 1000 * 60 * 5 // 5 分钟后过期
});
app.get('/api/data', async (req, res) => {
const key = req.params.id;
if (cache.has(key)) {
return res.json(cache.get(key));
}
const data = await fetchData(key);
cache.set(key, data);
res.json(data);
});

11.3 并发处理#

使用 Node.js 集群模式#

const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`主进程 ${process.pid} 正在运行`);
// 衍生工作进程
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 已退出`);
console.log('正在重启工作进程...');
cluster.fork();
});
} else {
// 工作进程可以共享同一个端口
const app = require('./app');
app.listen(3000, () => {
console.log(`工作进程 ${process.pid} 已启动`);
});
}

使用 PM2 集群模式#

Terminal window
# 使用 PM2 启动集群模式
pm2 start app.js -i max
# 查看集群状态
pm2 status

11.4 缓存策略#

使用 Redis 缓存#

Terminal window
npm install redis
const redis = require('redis');
const client = redis.createClient();
// 缓存中间件
const cache = (duration) => {
return async (req, res, next) => {
const key = `cache:${req.originalUrl || req.url}`;
try {
// 尝试从缓存获取
const cached = await client.get(key);
if (cached) {
return res.json(JSON.parse(cached));
}
// 缓存未命中,继续处理请求
res.sendResponse = res.json;
res.json = (body) => {
// 将响应存入缓存
client.setex(key, duration, JSON.stringify(body));
res.sendResponse(body);
};
next();
} catch (error) {
next();
}
};
};
// 使用缓存中间件
app.get('/api/users', cache(60), async (req, res) => {
const users = await User.find();
res.json(users);
});

HTTP 缓存头#

// 设置缓存头
app.get('/api/data', (req, res) => {
const data = fetchData();
// 设置缓存头(1 小时)
res.set('Cache-Control', 'public, max-age=3600');
res.set('ETag', generateETag(data));
// 检查 ETag
if (req.headers['if-none-match'] === res.get('ETag')) {
return res.status(304).end();
}
res.json(data);
});
function generateETag(data) {
return require('crypto')
.createHash('md5')
.update(JSON.stringify(data))
.digest('hex');
}

内存缓存#

// 使用 Node.js 内置 Map 实现简单缓存
const memoryCache = new Map();
// 缓存中间件
const memoryCacheMiddleware = (duration = 60) => {
return (req, res, next) => {
const key = req.originalUrl || req.url;
// 检查缓存
if (memoryCache.has(key)) {
const cached = memoryCache.get(key);
if (Date.now() < cached.expiresAt) {
return res.json(cached.data);
}
// 缓存过期,删除
memoryCache.delete(key);
}
// 缓存响应
res.sendResponse = res.json;
res.json = (data) => {
memoryCache.set(key, {
data,
expiresAt: Date.now() + duration * 1000
});
res.sendResponse(data);
};
next();
};
};
// 使用内存缓存
app.get('/api/users', memoryCacheMiddleware(60), async (req, res) => {
const users = await User.find();
res.json(users);
});
// 清除缓存
app.delete('/api/cache', (req, res) => {
memoryCache.clear();
res.json({ message: '缓存已清除' });
});

缓存失效策略#

// 缓存失效策略
const CacheStrategy = {
// 基于时间的失效(TTL)
TTL: 'ttl',
// 基于事件的失效(数据更新时)
EVENT: 'event',
// 基于计数的失效(LRU)
LRU: 'lru'
};
// 事件驱动的缓存失效
class EventDrivenCache {
constructor() {
this.cache = new Map();
this.subscribers = new Map();
}
// 订阅数据变化
subscribe(key, callback) {
if (!this.subscribers.has(key)) {
this.subscribers.set(key, []);
}
this.subscribers.get(key).push(callback);
}
// 发布数据变化
publish(key, data) {
// 清除缓存
this.cache.delete(key);
// 通知订阅者
if (this.subscribers.has(key)) {
this.subscribers.get(key).forEach(callback => callback(data));
}
}
// 获取缓存
get(key) {
return this.cache.get(key);
}
// 设置缓存
set(key, value) {
this.cache.set(key, value);
}
}
const eventCache = new EventDrivenCache();
// 订阅用户数据变化
eventCache.subscribe('user:123', (userData) => {
console.log('用户数据已更新:', userData);
});
// 更新用户数据时发布事件
app.put('/api/users/:id', async (req, res) => {
const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true });
// 发布数据变化事件
eventCache.publish(`user:${user._id}`, user);
res.json(user);
});
// 缓存预热(应用启动时加载热点数据)
async function warmUpCache() {
console.log('开始缓存预热...');
const popularPosts = await Post.find().sort({ views: -1 }).limit(100);
popularPosts.forEach(post => {
eventCache.set(`post:${post._id}`, post);
});
console.log('缓存预热完成');
}
// 应用启动时执行缓存预热
warmUpCache();

11.5 数据库查询优化#

使用索引#

// MongoDB 索引
const userSchema = new mongoose.Schema({
email: { type: String, unique: true },
name: String,
age: Number
});
// 为常用查询字段添加索引
userSchema.index({ email: 1 });
userSchema.index({ age: 1 });
userSchema.index({ name: 1, age: 1 }); // 复合索引
const User = mongoose.model('User', userSchema);

优化查询#

// ❌ 不好的做法:N+1 查询问题
app.get('/api/posts', async (req, res) => {
const posts = await Post.find();
for (const post of posts) {
post.author = await User.findById(post.authorId); // N+1 查询
}
res.json(posts);
});
// ✅ 好的做法:使用 populate
app.get('/api/posts', async (req, res) => {
const posts = await Post.find().populate('authorId', 'name email');
res.json(posts);
});
// ✅ 更好的做法:只查询需要的字段
app.get('/api/posts', async (req, res) => {
const posts = await Post.find()
.select('title content createdAt')
.populate('authorId', 'name')
.sort({ createdAt: -1 })
.limit(20);
res.json(posts);
});

使用连接池#

// MySQL 连接池配置
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'myapp',
waitForConnections: true,
connectionLimit: 10, // 最大连接数
queueLimit: 0
});
// 使用连接池查询
app.get('/api/users', async (req, res) => {
try {
const [rows] = await pool.query('SELECT * FROM users LIMIT 10');
res.json(rows);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

11.6 静态资源优化#

使用 CDN#

// 开发环境使用本地静态文件
if (process.env.NODE_ENV === 'development') {
app.use(express.static('public'));
} else {
// 生产环境使用 CDN
app.use((req, res, next) => {
if (req.url.startsWith('/static/')) {
return res.redirect(`https://cdn.example.com${req.url}`);
}
next();
});
}

压缩静态资源#

Terminal window
npm install shrink-ray-current
const shrinkRay = require('shrink-ray-current');
// 压缩 HTML、CSS、JavaScript
app.use(shrinkRay());

11.7 日志与监控#

日志管理#

Terminal window
npm install winston morgan
const winston = require('winston');
const morgan = require('morgan');
// 配置 Winston 日志
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'express-app' },
transports: [
// 写入所有日志到 combined.log
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' })
]
});
// 生产环境不输出到控制台
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
// 使用 Morgan 记录 HTTP 请求
app.use(morgan('combined', {
stream: {
write: (message) => logger.info(message.trim())
}
}));
// 在应用中使用日志
app.get('/api/test', (req, res) => {
logger.info('测试接口被调用', { ip: req.ip, userAgent: req.headers['user-agent'] });
try {
const data = fetchData();
logger.info('数据获取成功', { count: data.length });
res.json(data);
} catch (error) {
logger.error('数据获取失败', { error: error.message, stack: error.stack });
res.status(500).json({ error: '服务器错误' });
}
});

性能监控#

Terminal window
npm install express-status-monitor prom-client
const statusMonitor = require('express-status-monitor');
const promClient = require('prom-client');
// 添加状态监控页面
app.use(statusMonitor({
title: 'Express Status Monitor',
path: '/status',
spans: [
{ interval: 1, retention: 60 },
{ interval: 5, retention: 60 },
{ interval: 15, retention: 60 }
]
}));
// Prometheus 指标收集
const httpRequestDurationMicroseconds = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'code'],
buckets: [0.1, 0.5, 1, 1.5, 2, 5]
});
// 请求监控中间件
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
httpRequestDurationMicroseconds.observe(
{
method: req.method,
route: req.route ? req.route.path : req.path,
code: res.statusCode
},
duration / 1000
);
});
next();
});
// 暴露 Prometheus 指标
app.get('/metrics', (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(promClient.register.metrics());
});
// 访问 http://localhost:3000/status 查看性能监控
// 访问 http://localhost:3000/metrics 查看 Prometheus 指标

错误追踪#

Terminal window
npm install @sentry/node
const Sentry = require('@sentry/node');
// 配置 Sentry
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0,
});
// Sentry 错误处理中间件
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());
// 在路由中使用 Sentry
app.get('/api/test', async (req, res) => {
try {
const data = await fetchData();
res.json(data);
} catch (error) {
// 发送错误到 Sentry
Sentry.captureException(error);
res.status(500).json({ error: '服务器错误' });
}
});
// Sentry 错误处理(必须在所有路由之后)
app.use(Sentry.Handlers.errorHandler());
// 自定义错误处理
app.use((err, req, res, next) => {
// 记录错误
logger.error('未捕获的错误', {
error: err.message,
stack: err.stack,
url: req.url,
method: req.method,
ip: req.ip
});
// 发送到 Sentry
Sentry.captureException(err);
res.status(500).json({ error: '服务器错误' });
});

告警系统#

const nodemailer = require('nodemailer');
// 配置邮件发送器
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
secure: false,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
// 告警规则
const alertRules = {
// 错误率告警
errorRate: {
threshold: 0.1, // 10% 错误率
window: 60000, // 1 分钟窗口
count: 0,
total: 0
},
// 响应时间告警
responseTime: {
threshold: 3000, // 3 秒
violations: 0,
maxViolations: 5
}
};
// 发送告警邮件
async function sendAlert(type, message) {
const mailOptions = {
from: process.env.ALERT_FROM,
to: process.env.ALERT_TO,
subject: `[告警] ${type}`,
text: message
};
try {
await transporter.sendMail(mailOptions);
logger.info('告警邮件已发送', { type });
} catch (error) {
logger.error('发送告警邮件失败', { error: error.message });
}
}
// 监控错误率
app.use((req, res, next) => {
const originalSend = res.send;
res.send = function (data) {
alertRules.errorRate.total++;
if (res.statusCode >= 400) {
alertRules.errorRate.count++;
}
// 检查错误率
const errorRate = alertRules.errorRate.count / alertRules.errorRate.total;
if (errorRate > alertRules.errorRate.threshold) {
sendAlert('错误率过高', `错误率: ${(errorRate * 100).toFixed(2)}%`);
}
originalSend.call(this, data);
};
next();
});
// 定期重置计数器
setInterval(() => {
alertRules.errorRate.count = 0;
alertRules.errorRate.total = 0;
}, alertRules.errorRate.window);
// 健康检查端点
app.get('/health', (req, res) => {
const health = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
errorRate: alertRules.errorRate.count / alertRules.errorRate.total
};
res.json(health);
});

使用 PM2 监控#

Terminal window
# 实时监控
pm2 monit
# 查看日志
pm2 logs
# 查看详细信息
pm2 show app-name
# 设置告警
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 7

11.8 性能优化总结#

优化策略工具/技术预期效果
响应时间优化gzip 压缩、减少数据量减少 50-70% 响应时间
内存优化流式处理、LRU 缓存减少 30-50% 内存使用
并发处理集群模式、PM2提高 2-4 倍并发能力
缓存策略Redis、内存缓存、HTTP 缓存减少 80-90% 数据库查询
数据库优化索引、连接池、查询优化提高 3-10 倍查询速度
静态资源优化CDN、压缩减少 40-60% 加载时间
日志与监控Winston、Sentry、Prometheus实时监控,快速定位问题
Terminal window
npm install express-status-monitor
const statusMonitor = require('express-status-monitor');
// 添加状态监控页面
app.use(statusMonitor({
title: 'Express Status Monitor',
path: '/status',
spans: [
{ interval: 1, retention: 60 }, // 1 分钟间隔,保留 60 个数据点
{ interval: 5, retention: 60 },
{ interval: 15, retention: 60 }
]
}));
// 访问 http://localhost:3000/status 查看性能监控

使用 PM2 监控#

Terminal window
# 实时监控
pm2 monit
# 查看日志
pm2 logs
# 查看详细信息
pm2 show app-name

第二十二步:安全最佳实践#

🚨 安全是 Web 应用的生命线:一旦应用上线,就会面临各种安全威胁。不要等到被攻击后才重视安全!在开发阶段就应该遵循安全最佳实践,定期进行安全审计,保护用户数据和系统安全。

安全是 Web 应用开发中不可忽视的重要环节。本节将介绍常见的安全威胁及其防护措施。

12.1 常见安全威胁#

威胁类型描述影响
SQL 注入在查询中注入恶意 SQL 代码数据泄露、数据篡改
XSS(跨站脚本)在网页中注入恶意脚本用户信息泄露、会话劫持
CSRF(跨站请求伪造)伪造用户请求未授权操作
DDoS 攻击大量请求导致服务瘫痪服务不可用
暴力破解尝试大量密码组合账户被盗

12.2 SQL 注入防护#

使用参数化查询#

// ❌ 不好的做法:直接拼接 SQL
app.get('/api/users/:id', async (req, res) => {
const userId = req.params.id;
const query = `SELECT * FROM users WHERE id = ${userId}`; // 危险!
const [rows] = await pool.query(query);
res.json(rows);
});
// ✅ 好的做法:使用参数化查询
app.get('/api/users/:id', async (req, res) => {
const userId = req.params.id;
const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [userId]);
res.json(rows);
});
// ✅ 更好的做法:使用 ORM
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user);
});

🚨 永远不要信任用户输入:用户输入可能包含恶意代码,永远不要直接拼接 SQL 语句。使用参数化查询或 ORM 可以有效防止 SQL 注入攻击。这是一个必须遵守的安全原则

使用 Mongoose 的验证#

const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
minlength: 2,
maxlength: 50
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true,
validate: {
validator: function(v) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
},
message: '请输入有效的邮箱地址'
}
}
});

12.3 XSS 防护#

使用 helmet#

Terminal window
npm install helmet
const helmet = require('helmet');
// 启用 helmet 中间件
app.use(helmet());
// 配置 Content Security Policy
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
imgSrc: ["'self'", "data:", "https:"],
}
}));

转义用户输入#

const xss = require('xss');
// 转义用户输入
app.post('/api/comments', async (req, res) => {
const { content } = req.body;
// 转义 HTML 标签
const sanitizedContent = xss(content);
const comment = await Comment.create({ content: sanitizedContent });
res.json(comment);
});

12.4 CSRF 防护#

使用 csurf#

Terminal window
npm install csurf cookie-parser
const cookieParser = require('cookie-parser');
const csrf = require('csurf');
app.use(cookieParser());
app.use(csrf({ cookie: true }));
// 提供 CSRF Token
app.get('/api/csrf-token', (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// 验证 CSRF Token
app.post('/api/posts', (req, res) => {
res.json({ message: 'CSRF 验证通过' });
});

前端使用 CSRF Token#

// 获取 CSRF Token
fetch('/api/csrf-token')
.then(res => res.json())
.then(data => {
const csrfToken = data.csrfToken;
// 在请求头中发送 CSRF Token
fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ title: '新文章' })
});
});

12.5 速率限制#

使用 express-rate-limit#

Terminal window
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
// 基本速率限制
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100 // 最多 100 次请求
});
app.use('/api/', limiter);
// 登录接口更严格的限制
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 5, // 最多 5 次尝试
message: '登录尝试次数过多,请稍后再试'
});
app.post('/api/login', loginLimiter, (req, res) => {
// 登录逻辑
});

IP 黑名单#

const ipBlacklist = ['192.168.1.100', '10.0.0.1'];
app.use((req, res, next) => {
const clientIp = req.ip;
if (ipBlacklist.includes(clientIp)) {
return res.status(403).json({ error: 'IP 被封禁' });
}
next();
});

12.6 安全头设置#

使用 helmet 设置安全头#

app.use(helmet());
// 自定义安全头
app.use(helmet({
frameguard: {
action: 'deny' // 防止点击劫持
},
hsts: {
maxAge: 31536000, // 1 年
includeSubDomains: true,
preload: true
},
noSniff: true, // 防止 MIME 类型嗅探
xssFilter: true // 启用 XSS 过滤器
}));

自定义安全头#

app.use((req, res, next) => {
// X-Content-Type-Options
res.set('X-Content-Type-Options', 'nosniff');
// X-Frame-Options
res.set('X-Frame-Options', 'DENY');
// X-XSS-Protection
res.set('X-XSS-Protection', '1; mode=block');
// Strict-Transport-Security (HTTPS)
if (process.env.NODE_ENV === 'production') {
res.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
next();
});

12.7 敏感数据保护#

🚨 敏感信息泄露是严重的安全漏洞永远不要在代码中硬编码数据库密码、API 密钥、JWT 密钥等敏感信息!这些信息一旦泄露,攻击者就可以完全控制你的应用。务必使用环境变量或密钥管理服务来存储敏感信息。

使用环境变量#

Terminal window
npm install dotenv

创建 .env 文件(不提交到 Git):

DB_URI=mongodb://localhost:27017/myapp
JWT_SECRET=your-secret-key
API_KEY=your-api-key

在应用中使用:

require('dotenv').config();
const DB_URI = process.env.DB_URI;
const JWT_SECRET = process.env.JWT_SECRET;
// 不要在代码中硬编码敏感信息
console.log(DB_URI); // ✅ 从环境变量读取
// console.log('mongodb://localhost:27017/myapp'); // ❌ 不好的做法

密码加密#

Terminal window
npm install bcrypt
const bcrypt = require('bcrypt');
// 注册时加密密码
app.post('/api/register', async (req, res) => {
const { username, password } = req.body;
// 加密密码
const hashedPassword = await bcrypt.hash(password, 10);
const user = await User.create({
username,
password: hashedPassword
});
res.json({ message: '注册成功' });
});
// 登录时验证密码
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username });
if (!user) {
return res.status(401).json({ error: '用户名或密码错误' });
}
// 验证密码
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: '用户名或密码错误' });
}
res.json({ message: '登录成功' });
});

使用 JWT 认证#

Terminal window
npm install jsonwebtoken
const jwt = require('jsonwebtoken');
// 生成 JWT Token
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// 验证用户...
// 生成 Token
const token = jwt.sign(
{ userId: user._id, username: user.username },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ token });
});
// 验证 JWT Token 中间件
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: '未提供认证令牌' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: '无效的认证令牌' });
}
req.user = user;
next();
});
};
// 使用认证中间件
app.get('/api/profile', authenticateToken, (req, res) => {
res.json({ user: req.user });
});

Session 管理#

Terminal window
npm install express-session
const session = require('express-session');
// 配置 Session 中间件
app.use(session({
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // 生产环境使用 HTTPS
httpOnly: true, // 防止 XSS 攻击
maxAge: 24 * 60 * 60 * 1000 // 24 小时
},
store: new (require('connect-mongo')(session))({
url: process.env.DB_URI,
ttl: 24 * 60 * 60 // Session 过期时间(秒)
})
}));
// 设置 Session
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
// 验证用户...
req.session.userId = user._id;
req.session.username = user.username;
res.json({ message: '登录成功' });
});
// 检查 Session
app.get('/api/profile', (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ error: '未登录' });
}
res.json({ userId: req.session.userId, username: req.session.username });
});
// 销毁 Session
app.post('/api/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: '登出失败' });
}
res.json({ message: '登出成功' });
});
});

OAuth 2.0 集成#

Terminal window
npm install passport passport-google-oauth20
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
// 配置 Google OAuth 2.0
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback'
}, async (accessToken, refreshToken, profile, done) => {
// 查找或创建用户
let user = await User.findOne({ googleId: profile.id });
if (!user) {
user = await User.create({
googleId: profile.id,
name: profile.displayName,
email: profile.emails[0].value
});
}
return done(null, user);
}));
// 序列化和反序列化用户
passport.serializeUser((user, done) => done(null, user._id));
passport.deserializeUser(async (id, done) => {
const user = await User.findById(id);
done(null, user);
});
// Google 登录路由
app.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
// 登录成功,重定向到首页
res.redirect('/');
}
);
// 检查认证状态
app.get('/api/auth/status', (req, res) => {
if (req.isAuthenticated()) {
res.json({ authenticated: true, user: req.user });
} else {
res.json({ authenticated: false });
}
});

权限控制#

// 角色枚举
const Role = {
ADMIN: 'admin',
USER: 'user',
GUEST: 'guest'
};
// 权限检查中间件
const checkRole = (...allowedRoles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: '未登录' });
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({ error: '权限不足' });
}
next();
};
};
// 资源所有权检查
const checkOwnership = (Model) => {
return async (req, res, next) => {
try {
const resource = await Model.findById(req.params.id);
if (!resource) {
return res.status(404).json({ error: '资源不存在' });
}
// 管理员可以访问所有资源
if (req.user.role === Role.ADMIN) {
return next();
}
// 检查是否是资源所有者
if (resource.userId.toString() !== req.user.userId) {
return res.status(403).json({ error: '无权访问此资源' });
}
req.resource = resource;
next();
} catch (error) {
res.status(500).json({ error: error.message });
}
};
};
// 使用权限中间件
app.get('/api/admin/users', authenticateToken, checkRole(Role.ADMIN), async (req, res) => {
const users = await User.find();
res.json(users);
});
// 更新自己的文章
app.put('/api/posts/:id', authenticateToken, checkOwnership(Post), async (req, res) => {
const post = req.resource;
Object.assign(post, req.body);
await post.save();
res.json(post);
});

12.8 安全检查清单#

在部署应用之前,请检查以下安全措施:

基础安全#

  • 使用 helmet 设置安全头
  • 启用 HTTPS(生产环境)
  • 配置 CORS 策略
  • 设置速率限制
  • 使用环境变量存储敏感信息

认证和授权#

  • 密码加密存储(bcrypt)
  • 使用 JWT 或 Session 认证
  • 实现 CSRF 防护
  • 设置合理的会话过期时间

数据安全#

  • 使用参数化查询防止 SQL 注入
  • 转义用户输入防止 XSS
  • 验证和清理用户输入
  • 使用 HTTPS 传输敏感数据

日志和监控#

  • 记录安全事件
  • 监控异常请求
  • 定期审计日志
  • 设置安全告警

定期维护#

  • 定期更新依赖包
  • 定期备份数据
  • 定期进行安全审计
  • 制定应急响应计划

第二十三步:实时通信#

实时通信是现代 Web 应用的重要特性,让服务器能够主动向客户端推送数据,实现即时更新和双向通信。

23.1 WebSocket 基础#

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它允许服务器和客户端之间进行实时数据交换。

WebSocket vs HTTP#

特性HTTPWebSocket
通信模式请求-响应双向通信
连接每次请求新建连接持久连接
延迟较高极低
服务器推送不支持原生支持
开销较大较小

使用原生 WebSocket#

const WebSocket = require('ws');
// 创建 WebSocket 服务器
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
console.log('客户端已连接');
// 向客户端发送消息
ws.send('欢迎连接到 WebSocket 服务器!');
// 接收客户端消息
ws.on('message', (message) => {
console.log('收到消息:', message.toString());
// 广播消息给所有客户端
wss.clients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
});
// 客户端断开连接
ws.on('close', () => {
console.log('客户端已断开连接');
});
// 处理错误
ws.on('error', (error) => {
console.error('WebSocket 错误:', error);
});
});
console.log('WebSocket 服务器运行在 ws://localhost:8080');

23.2 Socket.io#

NOTE

📝 为什么选择 Socket.io?:虽然可以使用原生 WebSocket,但 Socket.io 提供了以下优势:

  • 自动降级:在不支持 WebSocket 的浏览器中自动降级到长轮询
  • 房间功能:轻松实现分组通信
  • 自动重连:连接断开时自动重连
  • 心跳检测:自动检测连接状态
  • 更简单的 API:比原生 WebSocket 更易用

Socket.io 是一个基于 WebSocket 的库,提供了更简单易用的 API 和自动降级功能。

安装 Socket.io#

Terminal window
npm install socket.io

服务器端实现#

const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = socketIo(server);
// Socket.io 连接处理
io.on('connection', (socket) => {
console.log('用户已连接:', socket.id);
// 加入房间
socket.on('join', (room) => {
socket.join(room);
socket.to(room).emit('message', `用户 ${socket.id} 加入了房间 ${room}`);
});
// 接收消息
socket.on('message', (data) => {
console.log('收到消息:', data);
// 广播消息给所有客户端
io.emit('message', {
id: socket.id,
text: data.text,
timestamp: new Date()
});
});
// 私聊
socket.on('private', (data) => {
const { targetId, message } = data;
io.to(targetId).emit('private', {
from: socket.id,
message
});
});
// 断开连接
socket.on('disconnect', () => {
console.log('用户已断开连接:', socket.id);
io.emit('message', `用户 ${socket.id} 已离开`);
});
});
server.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
});

客户端实现#

<!DOCTYPE html>
<html>
<head>
<title>Socket.io 聊天室</title>
</head>
<body>
<div id="messages"></div>
<input type="text" id="messageInput" placeholder="输入消息...">
<button onclick="sendMessage()">发送</button>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
// 接收消息
socket.on('message', (data) => {
const messagesDiv = document.getElementById('messages');
const messageElement = document.createElement('div');
messageElement.textContent = data.text || data;
messagesDiv.appendChild(messageElement);
});
// 发送消息
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value;
if (message) {
socket.emit('message', { text: message });
input.value = '';
}
}
// 加入房间
socket.emit('join', 'chat-room');
</script>
</body>
</html>

23.3 实时聊天应用#

完整的聊天应用示例#

const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const mongoose = require('mongoose');
const app = express();
const server = http.createServer(app);
const io = socketIo(server);
// 连接数据库
mongoose.connect('mongodb://localhost:27017/chat-app');
// 消息模型
const Message = mongoose.model('Message', {
username: String,
text: String,
room: String,
timestamp: { type: Date, default: Date.now }
});
// 获取历史消息
app.get('/api/messages/:room', async (req, res) => {
const messages = await Message.find({ room: req.params.room })
.sort({ timestamp: -1 })
.limit(50);
res.json(messages.reverse());
});
// Socket.io 连接
io.on('connection', (socket) => {
console.log('用户已连接:', socket.id);
// 用户加入
socket.on('join', ({ username, room }) => {
socket.join(room);
socket.username = username;
socket.room = room;
// 通知其他用户
socket.to(room).emit('notification', {
message: `${username} 加入了聊天室`,
type: 'join'
});
});
// 发送消息
socket.on('sendMessage', async (message) => {
const newMessage = new Message({
username: socket.username,
text: message,
room: socket.room
});
await newMessage.save();
// 广播消息到房间
io.to(socket.room).emit('message', {
username: socket.username,
text: message,
timestamp: newMessage.timestamp
});
});
// 用户离开
socket.on('disconnect', () => {
if (socket.username && socket.room) {
socket.to(socket.room).emit('notification', {
message: `${socket.username} 离开了聊天室`,
type: 'leave'
});
}
});
});
server.listen(3000, () => {
console.log('聊天应用运行在 http://localhost:3000');
});

第二十四步:GraphQL 入门#

NOTE

📝 GraphQL vs REST:GraphQL 和 REST 各有优劣:

  • REST:简单、成熟、缓存友好,适合大多数场景
  • GraphQL:灵活、高效、减少请求次数,适合复杂的前端需求

建议:如果你是初学者,建议先掌握 RESTful API。在遇到 REST 难以解决的问题时,再考虑使用 GraphQL。

GraphQL 是一种用于 API 的查询语言,它提供了更灵活、高效的数据获取方式。

24.1 GraphQL 基础#

GraphQL vs REST#

特性RESTGraphQL
数据获取多个端点单个端点
过度获取常见避免
不足获取需要多次请求一次请求
类型系统强类型
自描述需要 OpenAPI内置

GraphQL 基本概念#

# 查询(Query)
query {
user(id: "1") {
name
email
posts {
title
content
}
}
}
# 变更(Mutation)
mutation {
createPost(input: { title: "新文章", content: "内容" }) {
id
title
author {
name
}
}
}
# 订阅(Subscription)
subscription {
messageAdded {
id
text
author {
name
}
}
}

24.2 Apollo Server#

安装 Apollo Server#

Terminal window
npm install @apollo/server graphql

创建 Apollo Server#

const { ApolloServer, gql } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
// 定义 GraphQL Schema
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
posts: [Post!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
createPost(title: String!, content: String!, authorId: ID!): Post!
}
`;
// 模拟数据
const users = [
{ id: '1', name: '张三', email: 'zhangsan@example.com' },
{ id: '2', name: '李四', email: 'lisi@example.com' }
];
const posts = [
{ id: '1', title: '第一篇文章', content: '内容...', authorId: '1' },
{ id: '2', title: '第二篇文章', content: '内容...', authorId: '2' }
];
// 定义 Resolvers
const resolvers = {
Query: {
user: (_, { id }) => users.find(user => user.id === id),
users: () => users,
post: (_, { id }) => posts.find(post => post.id === id),
posts: () => posts
},
Mutation: {
createUser: (_, { name, email }) => {
const newUser = {
id: String(users.length + 1),
name,
email
};
users.push(newUser);
return newUser;
},
createPost: (_, { title, content, authorId }) => {
const newPost = {
id: String(posts.length + 1),
title,
content,
authorId
};
posts.push(newPost);
return newPost;
}
},
User: {
posts: (user) => posts.filter(post => post.authorId === user.id)
},
Post: {
author: (post) => users.find(user => user.id === post.authorId)
}
};
// 创建 Apollo Server
const server = new ApolloServer({
typeDefs,
resolvers
});
// 启动服务器
startStandaloneServer(server, {
listen: { port: 4000 }
}).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

24.3 与 Express 集成#

安装依赖#

Terminal window
npm install @apollo/server express body-parser

集成 Apollo Server 到 Express#

const express = require('express');
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const bodyParser = require('body-parser');
const app = express();
// GraphQL Schema 和 Resolvers
const typeDefs = gql`
type Query {
hello: String!
}
`;
const resolvers = {
Query: {
hello: () => 'Hello, World!'
}
};
// 创建 Apollo Server
const server = new ApolloServer({
typeDefs,
resolvers
});
// 启动 Apollo Server
await server.start();
// 集成到 Express
app.use('/graphql', bodyParser.json(), expressMiddleware(server));
// 添加 REST API
app.get('/api/hello', (req, res) => {
res.json({ message: 'Hello from REST API!' });
});
app.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
console.log('GraphQL Playground: http://localhost:3000/graphql');
});

使用数据库#

const mongoose = require('mongoose');
const { ApolloServer, gql } = require('@apollo/server');
// 连接数据库
mongoose.connect('mongodb://localhost:27017/myapp');
// 定义 Mongoose 模型
const User = mongoose.model('User', {
name: String,
email: String
});
const Post = mongoose.model('Post', {
title: String,
content: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }
});
// GraphQL Schema
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Query {
users: [User!]!
posts: [Post!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
createPost(title: String!, content: String!, authorId: ID!): Post!
}
`;
// Resolvers
const resolvers = {
Query: {
users: async () => await User.find(),
posts: async () => await Post.find().populate('author')
},
Mutation: {
createUser: async (_, { name, email }) => {
const user = new User({ name, email });
await user.save();
return user;
},
createPost: async (_, { title, content, authorId }) => {
const post = new Post({ title, content, author: authorId });
await post.save();
return post.populate('author');
}
},
User: {
posts: async (user) => await Post.find({ author: user._id })
}
};
// 创建 Apollo Server
const server = new ApolloServer({ typeDefs, resolvers });

第二十五步:微服务架构#

WARNING

⚠️ 微服务不是银弹:微服务架构虽然有很多优点,但也带来了额外的复杂度:

  • 📡 服务间通信:需要处理网络延迟、故障等问题
  • 🔧 运维复杂度:需要部署和管理多个服务
  • 📊 分布式追踪:调试和监控变得更加困难
  • 💾 数据一致性:需要处理分布式事务

建议:对于小型应用,单体架构更合适。只有当应用变得复杂、团队规模扩大时,才考虑微服务架构。

微服务架构是一种将应用拆分为多个小型、独立服务的设计模式,每个服务专注于特定的业务功能。

25.1 微服务概念#

微服务 vs 单体应用#

特性单体应用微服务
部署整体部署独立部署
扩展整体扩展按需扩展
技术栈统一多样化
故障隔离影响整体隔离故障
复杂度较低较高

微服务架构图#

graph TD A[客户端] --> B[API 网关] B --> C[用户服务<br/>端口: 3001] B --> D[文章服务<br/>端口: 3002] B --> E[评论服务<br/>端口: 3003] B --> F[通知服务<br/>端口: 3004] C --> G[用户数据库<br/>MongoDB] D --> H[文章数据库<br/>MongoDB] E --> I[评论数据库<br/>MongoDB] F --> J[消息队列<br/>Redis] C -.->|服务间通信| D D -.->|服务间通信| E E -.->|服务间通信| F style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#e8f5e9 style D fill:#e8f5e9 style E fill:#e8f5e9 style F fill:#e8f5e9 style G fill:#fce4ec style H fill:#fce4ec style I fill:#fce4ec style J fill:#ffccbc

25.2 服务间通信#

HTTP 通信#

// 服务 A 调用服务 B
const axios = require('axios');
class UserService {
async getUserPosts(userId) {
try {
const response = await axios.get(`http://localhost:3002/api/posts/${userId}`);
return response.data;
} catch (error) {
console.error('调用文章服务失败:', error.message);
throw error;
}
}
}
module.exports = new UserService();

使用服务发现#

Terminal window
npm install consul
const Consul = require('consul');
const consul = new Consul();
// 注册服务
async function registerService() {
await consul.agent.service.register({
name: 'user-service',
port: 3001,
check: {
http: 'http://localhost:3001/health',
interval: '10s'
}
});
console.log('服务已注册到 Consul');
}
// 发现服务
async function discoverService(serviceName) {
const services = await consul.agent.service.list();
const service = services[serviceName];
if (!service) {
throw new Error(`服务 ${serviceName} 未找到`);
}
return service;
}
// 调用服务
async function callService(serviceName, endpoint) {
const service = await discoverService(serviceName);
const url = `http://${service.Address}:${service.Port}${endpoint}`;
const response = await axios.get(url);
return response.data;
}

25.3 API 网关#

使用 Express 作为 API 网关#

const express = require('express');
const httpProxy = require('http-proxy-middleware');
const axios = require('axios');
const app = express();
// 服务配置
const services = {
users: 'http://localhost:3001',
posts: 'http://localhost:3002',
comments: 'http://localhost:3003'
};
// 代理中间件
const createProxy = (target) => {
return httpProxy.createProxyMiddleware({
target,
changeOrigin: true,
pathRewrite: {
[`^/api/${target.split(':')[2].replace('//', '')}`]: ''
}
});
};
// 路由到用户服务
app.use('/api/users', createProxy(services.users));
// 路由到文章服务
app.use('/api/posts', createProxy(services.posts));
// 路由到评论服务
app.use('/api/comments', createProxy(services.comments));
// 聚合接口
app.get('/api/dashboard/:userId', async (req, res) => {
try {
const { userId } = req.params;
// 并行调用多个服务
const [user, posts, comments] = await Promise.all([
axios.get(`${services.users}/api/users/${userId}`),
axios.get(`${services.posts}/api/posts?userId=${userId}`),
axios.get(`${services.comments}/api/comments?userId=${userId}`)
]);
res.json({
user: user.data,
posts: posts.data,
comments: comments.data
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 健康检查
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date() });
});
app.listen(3000, () => {
console.log('API 网关运行在 http://localhost:3000');
});

使用 Nginx 作为 API 网关#

upstream user_service {
server localhost:3001;
}
upstream post_service {
server localhost:3002;
}
upstream comment_service {
server localhost:3003;
}
server {
listen 80;
server_name api.example.com;
# 用户服务
location /api/users {
proxy_pass http://user_service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 文章服务
location /api/posts {
proxy_pass http://post_service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 评论服务
location /api/comments {
proxy_pass http://comment_service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

完成本指南的学习后,您可以:

🎯 实践项目#

  1. 完成教程中的实战练习,巩固所学知识
  2. 尝试开发自己的第一个 Express.js 应用
  3. 开发一个完整的博客系统,包含用户认证、文章管理、评论功能
  4. 开发一个实时聊天应用,使用 WebSocket 或 Socket.io
  5. 开发一个电商平台,包含商品管理、购物车、订单系统

📚 深入学习#

  1. 继续深入学习 Express.js 的高级特性
  2. 学习数据库集成(MongoDB、MySQL、PostgreSQL)
  3. 学习用户认证和授权(JWT、Session、OAuth 2.0)
  4. 学习 WebSocket 实时通信
  5. 学习 GraphQL,构建灵活的 API
  6. 学习微服务架构,构建可扩展的应用

🔧 工具和框架#

  1. 学习测试(单元测试、集成测试、E2E 测试)
  2. 学习 CI/CD(GitHub Actions、GitLab CI、Jenkins)
  3. 学习容器化(Docker、Kubernetes)
  4. 学习云服务(AWS、Azure、Google Cloud)
  5. 学习监控和日志(Prometheus、Grafana、ELK Stack)

🌐 进阶主题#

  1. 性能优化:缓存策略、负载均衡、CDN
  2. 安全最佳实践:HTTPS、安全头、漏洞扫描
  3. API 设计:RESTful API、GraphQL、gRPC
  4. 实时应用:WebSocket、Server-Sent Events、WebRTC
  5. 微服务:服务发现、API 网关、分布式追踪

📖 推荐学习路径#

graph TD A[Express.js 基础<br/>当前阶段] --> B[进阶主题] A --> C[实战项目] B --> B1[实时通信<br/>WebSocket/Socket.io] B --> B2[GraphQL<br/>Apollo Server] B --> B3[微服务架构<br/>服务拆分] C --> C1[博客系统] C --> C2[聊天应用] C --> C3[电商平台] C --> C4[社交应用] B1 --> D[高级主题] B2 --> D B3 --> D C1 --> D C2 --> D C3 --> D C4 --> D D --> D1[性能优化] D --> D2[安全加固] D --> D3[CI/CD] D --> D4[云部署] style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#e8f5e9 style D fill:#fce4ec style D1 fill:#c8e6c9 style D2 fill:#c8e6c9 style D3 fill:#c8e6c9 style D4 fill:#c8e6c9

🎓 学习资源#

官方资源#

推荐书籍#

  • 《Node.js 实战(第2版)》
  • 《Node.js 设计模式》
  • 《深入浅出 GraphQL》
  • 《微服务架构设计模式》

在线课程#

  • Udemy: Node.js - The Complete Guide
  • Coursera: Server-side Development with Node.js
  • Pluralsight: Building Web Apps with Node.js and Express
  • Egghead.io: GraphQL Fundamentals

社区资源#

💡 学习建议#

  1. 循序渐进:不要急于求成,按照文档的顺序学习
  2. 多动手实践:理论知识要结合实际项目
  3. 阅读源码:学习优秀开源项目的代码
  4. 参与社区:加入技术社区,与其他开发者交流
  5. 持续学习:技术更新快,保持学习的热情

🚀 职业发展#

掌握 Express.js 后,您可以:

  • 成为全栈开发工程师
  • 成为后端开发工程师
  • 成为 API 开发专家
  • 成为系统架构师
  • 创业开发自己的产品

📚 附录:完整知识体系#

Express.js 知识图谱#

Express.js
├── 基础
│ ├── 安装和配置
│ ├── 路由
│ ├── 中间件
│ ├── 请求处理
│ └── 响应处理
├── 进阶
│ ├── 错误处理
│ ├── 模板引擎
│ ├── 静态文件
│ ├── RESTful API
│ └── 调试技巧
├── 数据库
│ ├── MongoDB
│ ├── MySQL
│ ├── PostgreSQL
│ └── Redis
├── 认证授权
│ ├── JWT
│ ├── Session
│ ├── OAuth 2.0
│ └── 权限控制
├── 性能优化
│ ├── 缓存策略
│ ├── 数据库优化
│ ├── 并发处理
│ └── 监控告警
├── 安全实践
│ ├── SQL 注入防护
│ ├── XSS 防护
│ ├── CSRF 防护
│ └── 速率限制
├── 实时通信
│ ├── WebSocket
│ └── Socket.io
├── GraphQL
│ ├── Schema 定义
│ ├── Resolvers
│ └── Apollo Server
├── 微服务
│ ├── 服务拆分
│ ├── 服务通信
│ └── API 网关
└── 部署运维
├── Docker
├── CI/CD
├── 监控
└── 日志

技能树#

初级(1-3 个月)
├── Express.js 基础
├── RESTful API
├── MongoDB/MySQL
└── 基础认证
中级(3-6 个月)
├── 性能优化
├── 安全实践
├── WebSocket
├── 测试
└── Docker
高级(6-12 个月)
├── GraphQL
├── 微服务
├── TypeScript
├── CI/CD
└── 系统架构
专家(1 年以上)
├── 分布式系统
├── 高并发处理
├── 云原生
└── 技术领导

祝您学习愉快!🎉

如果您在学习过程中遇到问题,欢迎查阅官方文档、搜索技术社区或参与开源项目。记住,编程是一个持续学习的过程,保持好奇心和学习的热情,您一定能成为一名优秀的 Express.js 开发者!

最后更新:2026-01-15


祝您学习愉快!🎉

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Express.js 零基础入门指南
https://blog.mizhoubaobei.top/posts/node/express-js-beginner-guide/
作者
祁筱欣
发布于
2026-01-15
许可协议
CC BY 4.0

评论区

目录