一篇面向实战的后端博客:从 原生 HTTP 静态托管 到 Express 路由、中间件、模块化路由 ,并以 新闻列表 串联完整项目。示例可独立运行,不依赖外部讲义路径。
目录
- 导读:知识架构与权威参考
- [一、HTTP 服务基础](#一、HTTP 服务基础)
- [1.1 HTTP 协议核心概念](#1.1 HTTP 协议核心概念)
- [1.2 原生 Node.js 实现 HTTP 服务](#1.2 原生 Node.js 实现 HTTP 服务)
- [1.3 实现静态资源托管服务](#1.3 实现静态资源托管服务)
- [1.4 Node.js 事件循环与非阻塞 I/O 深度解析](#1.4 Node.js 事件循环与非阻塞 I/O 深度解析)
- [二、Express 框架深度解析](#二、Express 框架深度解析)
- [2.1 Express 框架概述](#2.1 Express 框架概述)
- [2.2 Express 安装与基本配置](#2.2 Express 安装与基本配置)
- [2.3 创建第一个 Express 应用](#2.3 创建第一个 Express 应用)
- [2.4 Express 静态资源服务详解](#2.4 Express 静态资源服务详解)
- 三、路由系统详解
- [3.1 路由概念解析](#3.1 路由概念解析)
- [3.2 Express 支持的 HTTP 方法](#3.2 Express 支持的 HTTP 方法)
- [3.3 路径匹配模式](#3.3 路径匹配模式)
- [3.4 路由处理器](#3.4 路由处理器)
- [3.5 app.route() 方法](#3.5 app.route() 方法)
- [3.6 404 错误处理](#3.6 404 错误处理)
- 四、请求与响应对象
- [4.1 请求对象(Request)详解](#4.1 请求对象(Request)详解)
- [4.2 获取请求体数据](#4.2 获取请求体数据)
- [4.3 响应对象(Response)详解](#4.3 响应对象(Response)详解)
- 五、中间件机制
- [5.1 中间件概念解析](#5.1 中间件概念解析)
- [5.2 应用级中间件](#5.2 应用级中间件)
- [5.3 路由级中间件](#5.3 路由级中间件)
- [5.4 错误处理中间件](#5.4 错误处理中间件)
- [5.5 内置中间件](#5.5 内置中间件)
- [5.6 第三方中间件](#5.6 第三方中间件)
- [5.7 实战:JWT 身份认证中间件](#5.7 实战:JWT 身份认证中间件)
- 六、模块化路由设计
- [6.1 为什么需要模块化路由](#6.1 为什么需要模块化路由)
- [6.2 创建路由模块](#6.2 创建路由模块)
- [6.3 在主应用中挂载路由](#6.3 在主应用中挂载路由)
- [6.4 项目结构示例](#6.4 项目结构示例)
- [6.5 路由模块的最佳实践](#6.5 路由模块的最佳实践)
- 七、实战案例:新闻列表应用
- [7.1 项目概述](#7.1 项目概述)
- [7.2 项目结构](#7.2 项目结构)
- [7.3 数据准备](#7.3 数据准备)
- [7.4 应用实现](#7.4 应用实现)
- [7.5 前端模板实现](#7.5 前端模板实现)
- [7.6 应用功能流程图](#7.6 应用功能流程图)
- [7.7 性能优化建议](#7.7 性能优化建议)
- 八、性能优化与最佳实践
- [8.1 性能优化策略](#8.1 性能优化策略)
- [8.2 安全最佳实践](#8.2 安全最佳实践)
- [8.3 错误处理模式](#8.3 错误处理模式)
- [8.4 日志记录](#8.4 日志记录)
- [8.5 环境配置](#8.5 环境配置)
- [8.6 数据库集成模式](#8.6 数据库集成模式)
- [8.7 优雅关闭(Graceful Shutdown)](#8.7 优雅关闭(Graceful Shutdown))
- 九、总结与进阶
- [9.1 核心知识点总结](#9.1 核心知识点总结)
- [9.2 学习路径建议](#9.2 学习路径建议)
- [9.3 进阶学习方向](#9.3 进阶学习方向)
- [9.4 常见问题与解决方案](#9.4 常见问题与解决方案)
- [9.5 实用代码片段](#9.5 实用代码片段)
- [9.6 项目模板](#9.6 项目模板)
- 十、核心案例速查与知识点归纳
- [10.1 课堂案例学习路线](#10.1 课堂案例学习路线)
- [10.2 GET vs POST 速查(考试常考)](#10.2 GET vs POST 速查(考试常考))
- [10.3 Express API 速查](#10.3 Express API 速查)
- [10.4 中间件三句话](#10.4 中间件三句话)
- [10.5 最小新闻列表(课堂精简版)](#10.5 最小新闻列表(课堂精简版))
- [10.6 可运行 HTML:调用 Express JSON API](#10.6 可运行 HTML:调用 Express JSON API)
- [10.7 常见坑](#10.7 常见坑)
- 结语
导读:知识架构与权威参考
本文解决什么问题
| 阶段 | 能力 | 产出 |
|---|---|---|
| 原生 HTTP | 路径解析、MIME、fs 读文件 |
静态资源服务器 |
| Express 入门 | app、listen、express.static |
多页面 + 静态站 |
| 路由 | GET/POST、参数、模糊匹配、app.route |
REST 风格 URL |
| 请求/响应 | query、params、body、res.json |
表单、API |
| 中间件 | app.use、next、错误处理 |
日志、鉴权、解析体 |
| 实战 | 新闻列表 + 详情 | 小型内容站原型 |
知识脉络(Mermaid)
#mermaid-svg-eoUWMXNtt1PtxTji{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eoUWMXNtt1PtxTji .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eoUWMXNtt1PtxTji .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eoUWMXNtt1PtxTji .error-icon{fill:#552222;}#mermaid-svg-eoUWMXNtt1PtxTji .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eoUWMXNtt1PtxTji .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eoUWMXNtt1PtxTji .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eoUWMXNtt1PtxTji .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eoUWMXNtt1PtxTji .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eoUWMXNtt1PtxTji .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eoUWMXNtt1PtxTji .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eoUWMXNtt1PtxTji .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eoUWMXNtt1PtxTji .marker.cross{stroke:#333333;}#mermaid-svg-eoUWMXNtt1PtxTji svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eoUWMXNtt1PtxTji p{margin:0;}#mermaid-svg-eoUWMXNtt1PtxTji .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eoUWMXNtt1PtxTji .cluster-label text{fill:#333;}#mermaid-svg-eoUWMXNtt1PtxTji .cluster-label span{color:#333;}#mermaid-svg-eoUWMXNtt1PtxTji .cluster-label span p{background-color:transparent;}#mermaid-svg-eoUWMXNtt1PtxTji .label text,#mermaid-svg-eoUWMXNtt1PtxTji span{fill:#333;color:#333;}#mermaid-svg-eoUWMXNtt1PtxTji .node rect,#mermaid-svg-eoUWMXNtt1PtxTji .node circle,#mermaid-svg-eoUWMXNtt1PtxTji .node ellipse,#mermaid-svg-eoUWMXNtt1PtxTji .node polygon,#mermaid-svg-eoUWMXNtt1PtxTji .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eoUWMXNtt1PtxTji .rough-node .label text,#mermaid-svg-eoUWMXNtt1PtxTji .node .label text,#mermaid-svg-eoUWMXNtt1PtxTji .image-shape .label,#mermaid-svg-eoUWMXNtt1PtxTji .icon-shape .label{text-anchor:middle;}#mermaid-svg-eoUWMXNtt1PtxTji .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eoUWMXNtt1PtxTji .rough-node .label,#mermaid-svg-eoUWMXNtt1PtxTji .node .label,#mermaid-svg-eoUWMXNtt1PtxTji .image-shape .label,#mermaid-svg-eoUWMXNtt1PtxTji .icon-shape .label{text-align:center;}#mermaid-svg-eoUWMXNtt1PtxTji .node.clickable{cursor:pointer;}#mermaid-svg-eoUWMXNtt1PtxTji .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eoUWMXNtt1PtxTji .arrowheadPath{fill:#333333;}#mermaid-svg-eoUWMXNtt1PtxTji .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eoUWMXNtt1PtxTji .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eoUWMXNtt1PtxTji .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eoUWMXNtt1PtxTji .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eoUWMXNtt1PtxTji .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eoUWMXNtt1PtxTji .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eoUWMXNtt1PtxTji .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eoUWMXNtt1PtxTji .cluster text{fill:#333;}#mermaid-svg-eoUWMXNtt1PtxTji .cluster span{color:#333;}#mermaid-svg-eoUWMXNtt1PtxTji div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eoUWMXNtt1PtxTji .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eoUWMXNtt1PtxTji rect.text{fill:none;stroke-width:0;}#mermaid-svg-eoUWMXNtt1PtxTji .icon-shape,#mermaid-svg-eoUWMXNtt1PtxTji .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eoUWMXNtt1PtxTji .icon-shape p,#mermaid-svg-eoUWMXNtt1PtxTji .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eoUWMXNtt1PtxTji .icon-shape .label rect,#mermaid-svg-eoUWMXNtt1PtxTji .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eoUWMXNtt1PtxTji .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eoUWMXNtt1PtxTji .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eoUWMXNtt1PtxTji :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 原生 http 静态托管
Express 应用
路由 app.get/post
中间件链
req / res API
express.Router 模块化
新闻列表实战
权威文档
| 主题 | 链接 |
|---|---|
| Express 官网 | expressjs.com |
| Express 中文 | expressjs.com.cn |
| Node.js http | nodejs.org/api/http.html |
| MDN HTTP | MDN --- HTTP |
| MDN MIME | MDN --- MIME 类型 |
行业落点(技术向)
- Next.js / Nuxt:SSR 底层仍有 Node HTTP 服务;Express 是理解中间件链的入门阶梯。
- API 网关:路由匹配、限流、CORS 与 Express 中间件思想一致。
- 微服务 BFF:Express 作聚合层,向前端提供统一 JSON API。
一、HTTP 服务基础
1.1 HTTP 协议核心概念
HTTP(HyperText Transfer Protocol)是应用层协议,定义了客户端与服务器之间通信的规范。理解 HTTP 协议是构建 Web 服务的基础。
请求与响应流程
数据存储 Web服务器 客户端浏览器 数据存储 Web服务器 客户端浏览器 #mermaid-svg-55ZPi3s0UZ1YdGzt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-55ZPi3s0UZ1YdGzt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-55ZPi3s0UZ1YdGzt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-55ZPi3s0UZ1YdGzt .error-icon{fill:#552222;}#mermaid-svg-55ZPi3s0UZ1YdGzt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-55ZPi3s0UZ1YdGzt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-55ZPi3s0UZ1YdGzt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-55ZPi3s0UZ1YdGzt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-55ZPi3s0UZ1YdGzt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-55ZPi3s0UZ1YdGzt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-55ZPi3s0UZ1YdGzt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-55ZPi3s0UZ1YdGzt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-55ZPi3s0UZ1YdGzt .marker.cross{stroke:#333333;}#mermaid-svg-55ZPi3s0UZ1YdGzt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-55ZPi3s0UZ1YdGzt p{margin:0;}#mermaid-svg-55ZPi3s0UZ1YdGzt .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-55ZPi3s0UZ1YdGzt text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-55ZPi3s0UZ1YdGzt .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-55ZPi3s0UZ1YdGzt .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-55ZPi3s0UZ1YdGzt .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-55ZPi3s0UZ1YdGzt .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-55ZPi3s0UZ1YdGzt #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-55ZPi3s0UZ1YdGzt .sequenceNumber{fill:white;}#mermaid-svg-55ZPi3s0UZ1YdGzt #sequencenumber{fill:#333;}#mermaid-svg-55ZPi3s0UZ1YdGzt #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-55ZPi3s0UZ1YdGzt .messageText{fill:#333;stroke:none;}#mermaid-svg-55ZPi3s0UZ1YdGzt .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-55ZPi3s0UZ1YdGzt .labelText,#mermaid-svg-55ZPi3s0UZ1YdGzt .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-55ZPi3s0UZ1YdGzt .loopText,#mermaid-svg-55ZPi3s0UZ1YdGzt .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-55ZPi3s0UZ1YdGzt .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-55ZPi3s0UZ1YdGzt .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-55ZPi3s0UZ1YdGzt .noteText,#mermaid-svg-55ZPi3s0UZ1YdGzt .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-55ZPi3s0UZ1YdGzt .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-55ZPi3s0UZ1YdGzt .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-55ZPi3s0UZ1YdGzt .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-55ZPi3s0UZ1YdGzt .actorPopupMenu{position:absolute;}#mermaid-svg-55ZPi3s0UZ1YdGzt .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-55ZPi3s0UZ1YdGzt .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-55ZPi3s0UZ1YdGzt .actor-man circle,#mermaid-svg-55ZPi3s0UZ1YdGzt line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-55ZPi3s0UZ1YdGzt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求报文包含: 请求行、请求头、请求体 响应报文包含: 状态行、响应头、响应体 发送HTTP请求 处理请求逻辑 读取/写入数据 返回数据 返回HTTP响应 渲染响应内容
GET 与 POST 方法对比
| 特性 | GET 方法 | POST 方法 |
|---|---|---|
| 主要用途 | 从服务器获取数据 | 向服务器提交数据 |
| 请求体 | 无请求体 | 包含请求体 |
| 安全性 | 数据暴露在 URL 中 | 数据在请求体中,相对安全 |
| 数据容量 | 受 URL 长度限制(约 2KB) | 理论上无限制 |
| 缓存性 | 可被缓存 | 默认不被缓存 |
| 幂等性 | 幂等操作 | 非幂等操作 |
名词解释:幂等性
幂等性是指多次执行同一操作产生的结果与执行一次相同。GET 方法是幂等的,因为多次获取不会改变服务器状态;POST 方法通常不是幂等的,因为每次提交可能创建新资源。
1.2 原生 Node.js 实现 HTTP 服务
javascript
const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
const server = http.createServer((req, res) => {
const pathname = url.parse(req.url).pathname;
console.log(`收到请求:${pathname}`);
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>欢迎使用 Node.js HTTP 服务</h1>');
});
server.listen(8080, () => {
console.log('服务器运行在 http://localhost:8080');
});
【代码注释】
- 与 Day09 原生
http相同:createServer每个请求触发一次回调,req读请求、res写响应。 url.parse(req.url).pathname得到路径(如/、/index.html),不含?后的查询串(查询用url.parse(req.url, true).query)。- 本示例对所有路径返回同一段 HTML;真实项目需
if/switch或路由表分支(Express 即封装这一步)。 - 学完本章后应理解:Express 底层仍是 Node HTTP,只是用
app.get('/path')代替手写pathname判断。
1.3 实现静态资源托管服务
静态资源托管是 Web 服务的基础功能,用于服务 HTML、CSS、JavaScript、图片等静态文件。
javascript
// 【代码注释】导入所需模块
const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
// 【代码注释】MIME 类型映射表
// MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展
// 用于标识文档、文件或字节流的性质和格式
const mimeTypes = {
'html': 'text/html',
'css': 'text/css',
'js': 'text/javascript',
'json': 'application/json',
'png': 'image/png',
'jpg': 'image/jpeg',
'gif': 'image/gif',
'svg': 'image/svg+xml',
'ico': 'image/x-icon'
};
// 【代码注释】创建服务器
const server = http.createServer((req, res) => {
// 1. 解析请求路径
const pathname = url.parse(req.url).pathname;
// 2. 获取文件扩展名
const extname = pathname.slice(pathname.lastIndexOf('.') + 1);
// 3. 构建文件绝对路径
// decodeURI 用于解码 URL 编码的中文字符
let filename = path.join(__dirname, 'public', pathname);
filename = decodeURI(filename);
// 4. 检查文件是否存在
fs.access(filename, err => {
if (err) {
// 文件不存在,返回 404
res.writeHead(404, 'Not Found', {
'Content-Type': 'text/html; charset=utf-8'
});
res.end('<h1>404 - 页面不存在</h1>');
return;
}
// 5. 读取并返回文件内容
fs.readFile(filename, (err, data) => {
if (err) {
// 读取失败,返回 500
res.writeHead(500, 'Internal Server Error', {
'Content-Type': 'text/html; charset=utf-8'
});
res.end('<h1>500 - 服务器内部错误</h1>');
return;
}
// 设置正确的 Content-Type 响应头
res.setHeader('Content-Type', mimeTypes[extname] || 'application/octet-stream');
res.end(data);
});
});
});
// 【代码注释】启动服务器
server.listen(8080, () => {
console.log('静态资源服务器运行在 http://localhost:8080');
});
【代码注释】
- 四步流程 :
pathname→path.join(__dirname, 'public', pathname)→fs.access判断存在 →readFile+ 正确Content-Type。 decodeURI(filename):浏览器请求中文文件名时 URL 会编码,不解码会ENOENT。mimeTypes[extname]:.css必须是text/css,否则页面无样式;与课堂mimes.json方案等价。fs.access异步检查优于同步existsSync,不阻塞事件循环;失败分支分别返回 404 (无文件)与 500(读盘错误)。- 访问
/时pathname为/,需业务上映射到index.html(Express 的express.static已内置该逻辑)。
静态资源服务的工作流程
#mermaid-svg-pze7uVmQYmNo7OB9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pze7uVmQYmNo7OB9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pze7uVmQYmNo7OB9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pze7uVmQYmNo7OB9 .error-icon{fill:#552222;}#mermaid-svg-pze7uVmQYmNo7OB9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pze7uVmQYmNo7OB9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pze7uVmQYmNo7OB9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pze7uVmQYmNo7OB9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pze7uVmQYmNo7OB9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pze7uVmQYmNo7OB9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pze7uVmQYmNo7OB9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pze7uVmQYmNo7OB9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pze7uVmQYmNo7OB9 .marker.cross{stroke:#333333;}#mermaid-svg-pze7uVmQYmNo7OB9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pze7uVmQYmNo7OB9 p{margin:0;}#mermaid-svg-pze7uVmQYmNo7OB9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-pze7uVmQYmNo7OB9 .cluster-label text{fill:#333;}#mermaid-svg-pze7uVmQYmNo7OB9 .cluster-label span{color:#333;}#mermaid-svg-pze7uVmQYmNo7OB9 .cluster-label span p{background-color:transparent;}#mermaid-svg-pze7uVmQYmNo7OB9 .label text,#mermaid-svg-pze7uVmQYmNo7OB9 span{fill:#333;color:#333;}#mermaid-svg-pze7uVmQYmNo7OB9 .node rect,#mermaid-svg-pze7uVmQYmNo7OB9 .node circle,#mermaid-svg-pze7uVmQYmNo7OB9 .node ellipse,#mermaid-svg-pze7uVmQYmNo7OB9 .node polygon,#mermaid-svg-pze7uVmQYmNo7OB9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pze7uVmQYmNo7OB9 .rough-node .label text,#mermaid-svg-pze7uVmQYmNo7OB9 .node .label text,#mermaid-svg-pze7uVmQYmNo7OB9 .image-shape .label,#mermaid-svg-pze7uVmQYmNo7OB9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-pze7uVmQYmNo7OB9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pze7uVmQYmNo7OB9 .rough-node .label,#mermaid-svg-pze7uVmQYmNo7OB9 .node .label,#mermaid-svg-pze7uVmQYmNo7OB9 .image-shape .label,#mermaid-svg-pze7uVmQYmNo7OB9 .icon-shape .label{text-align:center;}#mermaid-svg-pze7uVmQYmNo7OB9 .node.clickable{cursor:pointer;}#mermaid-svg-pze7uVmQYmNo7OB9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pze7uVmQYmNo7OB9 .arrowheadPath{fill:#333333;}#mermaid-svg-pze7uVmQYmNo7OB9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pze7uVmQYmNo7OB9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pze7uVmQYmNo7OB9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pze7uVmQYmNo7OB9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pze7uVmQYmNo7OB9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pze7uVmQYmNo7OB9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pze7uVmQYmNo7OB9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pze7uVmQYmNo7OB9 .cluster text{fill:#333;}#mermaid-svg-pze7uVmQYmNo7OB9 .cluster span{color:#333;}#mermaid-svg-pze7uVmQYmNo7OB9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pze7uVmQYmNo7OB9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pze7uVmQYmNo7OB9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-pze7uVmQYmNo7OB9 .icon-shape,#mermaid-svg-pze7uVmQYmNo7OB9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pze7uVmQYmNo7OB9 .icon-shape p,#mermaid-svg-pze7uVmQYmNo7OB9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pze7uVmQYmNo7OB9 .icon-shape .label rect,#mermaid-svg-pze7uVmQYmNo7OB9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pze7uVmQYmNo7OB9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pze7uVmQYmNo7OB9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pze7uVmQYmNo7OB9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
接收请求
解析URL路径
构建文件路径
解码URL编码
文件是否存在?
返回404
读取文件内容
读取成功?
返回500
设置Content-Type
返回文件内容
真实应用场景
- 内容分发网络(CDN):如 Cloudflare、AWS CloudFront
- 静态网站托管:如 GitHub Pages、Netlify、Vercel
- 企业官网:产品展示、营销页面
- 前端应用部署:React/Vue 单页应用的构建产物
静态托管四步归纳
- 根据 URL 的
pathname拼接服务器上的文件路径(常加public前缀)。 - 用
fs.access/readFile读取;不存在 → 404 ,读失败 → 500。 - 中文路径需
decodeURI()解码浏览器编码。 - 响应头设置
Content-Type(MIME),否则 CSS/JS 无法正确渲染。
使用外部 MIME 映射表(可维护性更好):
javascript
const mimes = require('./mimes/mimes.json');
const extname = pathname.slice(pathname.lastIndexOf('.') + 1);
res.setHeader('Content-Type', mimes[extname] || 'application/octet-stream');
【代码注释】
- 课堂案例把 MIME 表抽到
mimes/mimes.json,require后以扩展名为键取值,开闭原则 :新增.webp、.woff2只改 JSON。 mimes[extname] || 'application/octet-stream':octet-stream表示未知二进制,浏览器可能触发下载而非渲染。- 与内联
mimeTypes对象相比,JSON 便于运维/前端同学维护,无需改 JS 逻辑。 - Express 中
express.static内部同样依赖 MIME 映射,理解原生实现有助于排查静态资源类型错误。
1.4 Node.js 事件循环与非阻塞 I/O 深度解析
理解事件循环是写出高性能 Node.js 服务的必要前提。Node.js 基于 V8 引擎 (JS 执行)和 libuv (跨平台异步 I/O),采用单线程事件循环 + 操作系统异步 I/O 的架构。
为什么单线程也能高并发?
传统多线程服务器(如 Java Tomcat)为每个连接创建一个线程------线程切换和内存开销随并发量线性增长。Node.js 用一个线程 的事件循环处理所有请求,将耗时的 I/O 操作(读文件、数据库查询、网络请求)交给操作系统或 libuv 线程池异步执行,主线程不阻塞等待,继续处理下一个请求。这是 Node.js 在 I/O 密集型场景下吞吐量碾压传统多线程的根本原因。
#mermaid-svg-yrc6YPFPTtdapIUz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yrc6YPFPTtdapIUz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yrc6YPFPTtdapIUz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yrc6YPFPTtdapIUz .error-icon{fill:#552222;}#mermaid-svg-yrc6YPFPTtdapIUz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yrc6YPFPTtdapIUz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yrc6YPFPTtdapIUz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yrc6YPFPTtdapIUz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yrc6YPFPTtdapIUz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yrc6YPFPTtdapIUz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yrc6YPFPTtdapIUz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yrc6YPFPTtdapIUz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yrc6YPFPTtdapIUz .marker.cross{stroke:#333333;}#mermaid-svg-yrc6YPFPTtdapIUz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yrc6YPFPTtdapIUz p{margin:0;}#mermaid-svg-yrc6YPFPTtdapIUz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yrc6YPFPTtdapIUz .cluster-label text{fill:#333;}#mermaid-svg-yrc6YPFPTtdapIUz .cluster-label span{color:#333;}#mermaid-svg-yrc6YPFPTtdapIUz .cluster-label span p{background-color:transparent;}#mermaid-svg-yrc6YPFPTtdapIUz .label text,#mermaid-svg-yrc6YPFPTtdapIUz span{fill:#333;color:#333;}#mermaid-svg-yrc6YPFPTtdapIUz .node rect,#mermaid-svg-yrc6YPFPTtdapIUz .node circle,#mermaid-svg-yrc6YPFPTtdapIUz .node ellipse,#mermaid-svg-yrc6YPFPTtdapIUz .node polygon,#mermaid-svg-yrc6YPFPTtdapIUz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yrc6YPFPTtdapIUz .rough-node .label text,#mermaid-svg-yrc6YPFPTtdapIUz .node .label text,#mermaid-svg-yrc6YPFPTtdapIUz .image-shape .label,#mermaid-svg-yrc6YPFPTtdapIUz .icon-shape .label{text-anchor:middle;}#mermaid-svg-yrc6YPFPTtdapIUz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yrc6YPFPTtdapIUz .rough-node .label,#mermaid-svg-yrc6YPFPTtdapIUz .node .label,#mermaid-svg-yrc6YPFPTtdapIUz .image-shape .label,#mermaid-svg-yrc6YPFPTtdapIUz .icon-shape .label{text-align:center;}#mermaid-svg-yrc6YPFPTtdapIUz .node.clickable{cursor:pointer;}#mermaid-svg-yrc6YPFPTtdapIUz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yrc6YPFPTtdapIUz .arrowheadPath{fill:#333333;}#mermaid-svg-yrc6YPFPTtdapIUz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yrc6YPFPTtdapIUz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yrc6YPFPTtdapIUz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yrc6YPFPTtdapIUz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yrc6YPFPTtdapIUz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yrc6YPFPTtdapIUz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yrc6YPFPTtdapIUz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yrc6YPFPTtdapIUz .cluster text{fill:#333;}#mermaid-svg-yrc6YPFPTtdapIUz .cluster span{color:#333;}#mermaid-svg-yrc6YPFPTtdapIUz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yrc6YPFPTtdapIUz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yrc6YPFPTtdapIUz rect.text{fill:none;stroke-width:0;}#mermaid-svg-yrc6YPFPTtdapIUz .icon-shape,#mermaid-svg-yrc6YPFPTtdapIUz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yrc6YPFPTtdapIUz .icon-shape p,#mermaid-svg-yrc6YPFPTtdapIUz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yrc6YPFPTtdapIUz .icon-shape .label rect,#mermaid-svg-yrc6YPFPTtdapIUz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yrc6YPFPTtdapIUz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yrc6YPFPTtdapIUz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yrc6YPFPTtdapIUz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 同步 JS
文件/网络 I/O
大量并发请求
事件队列 Event Queue
事件循环 Event Loop
单线程
任务类型
立即执行
libuv 线程池
或操作系统异步
I/O 完成
回调进入事件队列
响应客户端
事件循环的六个阶段
Node.js 事件循环按固定顺序轮询六个阶段,每个阶段有一个 FIFO 回调队列:
┌────────────────────────────────┐
│ timers │ ← setTimeout / setInterval 的到期回调
├────────────────────────────────┤
│ pending callbacks │ ← 上一轮遗留的 I/O 错误回调
├────────────────────────────────┤
│ idle, prepare │ ← 内部使用,开发者无需关心
├────────────────────────────────┤
│ poll │ ← 核心阶段:等待并执行新的 I/O 回调
├────────────────────────────────┤
│ check │ ← setImmediate 回调
├────────────────────────────────┤
│ close callbacks │ ← socket.on('close') 等关闭事件
└────────────────────────────────┘
↑_________________________↓ 每轮循环前清空 microtask 队列
(Promise.then / queueMicrotask)
javascript
// 示例:验证执行顺序
console.log('① 同步代码开始');
setTimeout(() => {
console.log('④ setTimeout ------ timers 阶段');
}, 0);
setImmediate(() => {
console.log('⑤ setImmediate ------ check 阶段');
});
// microtask(Promise)在每个阶段切换前优先清空
Promise.resolve().then(() => {
console.log('③ Promise.then ------ microtask 队列');
});
process.nextTick(() => {
// nextTick 优先级高于 Promise,在 microtask 中最先执行
console.log('② process.nextTick ------ nextTick 队列');
});
console.log('① 同步代码结束');
// 实际输出:① 开始 → ① 结束 → ② nextTick → ③ Promise → ④ setTimeout → ⑤ setImmediate
阻塞 vs 非阻塞:实战对比
javascript
const express = require('express');
const fs = require('fs');
const app = express();
// ❌ 错误做法:readFileSync 阻塞整个事件循环
// 读取期间,所有其他请求都被挂起,服务器吞吐量骤降
app.get('/bad-blocking', (req, res) => {
const data = fs.readFileSync('./large-file.txt', 'utf-8'); // 阻塞!
res.send(data);
});
// ✅ 正确做法:异步回调,不阻塞事件循环
app.get('/good-callback', (req, res) => {
fs.readFile('./large-file.txt', 'utf-8', (err, data) => {
if (err) return res.status(500).send('读取失败');
res.send(data); // 文件就绪后才执行,期间主线程可处理其他请求
});
});
// ✅ 最佳实践:async/await + fs.promises(代码更清晰)
app.get('/good-async', async (req, res) => {
try {
// await 让出执行权,事件循环可继续处理其他请求
const data = await fs.promises.readFile('./large-file.txt', 'utf-8');
res.send(data);
} catch (err) {
res.status(500).send('读取失败');
}
});
各场景的正确选择
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 读取配置文件(应用启动时) | readFileSync |
启动阶段一次性执行,不影响运行时 |
| 请求处理中的文件/数据库操作 | async/await + fs.promises |
不阻塞事件循环,保持高并发 |
| CPU 密集计算(图像处理、加密) | worker_threads |
单线程无法充分利用多核 CPU |
| 大量并发轻量请求 | Node.js 原生擅长 | I/O 密集型的最佳场景 |
核心结论 :Node.js 高并发的秘诀是"不等待,异步回调"。在 Express 路由处理函数中,凡是涉及 I/O 的操作,务必使用异步 API,否则一个慢请求会拖垮整个服务器。
二、Express 框架深度解析
2.1 Express 框架概述
Express 是基于 Node.js 平台的极简、灵活的 Web 应用开发框架,提供了强大的路由功能和中间件系统,极大简化了 Web 开发。
Express 核心特性
- 简洁的路由 API:灵活的 URL 路由定义
- 中间件支持:可堆叠的中间件系统
- 静态文件服务:内置静态资源托管
- 模板引擎集成:支持多种模板引擎
- 丰富的生态系统:大量第三方中间件
技术对比:原生 HTTP vs Express
| 特性 | 原生 HTTP 模块 | Express 框架 |
|---|---|---|
| 路由管理 | 手动 if-else 判断 | 优雅的路由 API |
| 中间件 | 需要自己实现 | 内置丰富中间件 |
| 静态文件 | 手动实现 fs 操作 | express.static() |
| 代码量 | 较多 | 简洁高效 |
| 学习曲线 | 较陡 | 相对平缓 |
| 适用场景 | 理解 HTTP 原理 | 快速开发应用 |
2.2 Express 安装与基本配置
项目初始化
bash
# 【代码注释】初始化项目
npm init -y
# 【代码注释】安装 Express
npm install express
# 【代码注释】安装常用中间件
npm install body-parser cookie-parser
2.3 创建第一个 Express 应用
javascript
const express = require('express');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.static(path.join(__dirname, 'public')));
app.get('/', (req, res) => {
res.send('<h1>欢迎访问 Express 应用</h1>');
});
app.get('/about', (req, res) => {
res.send('<h1>关于我们</h1>');
});
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
【代码注释】
express()返回应用实例app,不是直接http.createServer;app内部会创建 HTTP 服务器。app.use(express.static(...))注册中间件 :请求若匹配public下文件则直接返回,不再走后面路由(除非static未找到文件且未next------static 找不到会next())。app.get(path, handler)只匹配 GET + 路径;handler中res.send自动设置Content-Type并发送(字符串/HTML)。app.listen(PORT)等价于原生server.listen,默认监听0.0.0.0;process.env.PORT便于部署环境注入端口。- 课堂常用 8080 ;与
package.json的"scripts": { "start": "node index.js" }配合使用。
Express 应用生命周期
#mermaid-svg-qweVqPEo3l7nafmG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qweVqPEo3l7nafmG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qweVqPEo3l7nafmG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qweVqPEo3l7nafmG .error-icon{fill:#552222;}#mermaid-svg-qweVqPEo3l7nafmG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qweVqPEo3l7nafmG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qweVqPEo3l7nafmG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qweVqPEo3l7nafmG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qweVqPEo3l7nafmG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qweVqPEo3l7nafmG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qweVqPEo3l7nafmG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qweVqPEo3l7nafmG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qweVqPEo3l7nafmG .marker.cross{stroke:#333333;}#mermaid-svg-qweVqPEo3l7nafmG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qweVqPEo3l7nafmG p{margin:0;}#mermaid-svg-qweVqPEo3l7nafmG defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-qweVqPEo3l7nafmG g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-qweVqPEo3l7nafmG g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-qweVqPEo3l7nafmG g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-qweVqPEo3l7nafmG g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-qweVqPEo3l7nafmG g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-qweVqPEo3l7nafmG .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-qweVqPEo3l7nafmG .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-qweVqPEo3l7nafmG .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-qweVqPEo3l7nafmG .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-qweVqPEo3l7nafmG .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-qweVqPEo3l7nafmG .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-qweVqPEo3l7nafmG .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-qweVqPEo3l7nafmG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qweVqPEo3l7nafmG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qweVqPEo3l7nafmG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qweVqPEo3l7nafmG .edgeLabel .label text{fill:#333;}#mermaid-svg-qweVqPEo3l7nafmG .label div .edgeLabel{color:#333;}#mermaid-svg-qweVqPEo3l7nafmG .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-qweVqPEo3l7nafmG .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-qweVqPEo3l7nafmG .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-qweVqPEo3l7nafmG .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-qweVqPEo3l7nafmG .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-qweVqPEo3l7nafmG .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qweVqPEo3l7nafmG .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qweVqPEo3l7nafmG #statediagram-barbEnd{fill:#333333;}#mermaid-svg-qweVqPEo3l7nafmG .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qweVqPEo3l7nafmG .cluster-label,#mermaid-svg-qweVqPEo3l7nafmG .nodeLabel{color:#131300;}#mermaid-svg-qweVqPEo3l7nafmG .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-qweVqPEo3l7nafmG .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-qweVqPEo3l7nafmG .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-qweVqPEo3l7nafmG .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-qweVqPEo3l7nafmG .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-qweVqPEo3l7nafmG .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-qweVqPEo3l7nafmG .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-qweVqPEo3l7nafmG .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-qweVqPEo3l7nafmG .note-edge{stroke-dasharray:5;}#mermaid-svg-qweVqPEo3l7nafmG .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-qweVqPEo3l7nafmG .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-qweVqPEo3l7nafmG .statediagram-note text{fill:black;}#mermaid-svg-qweVqPEo3l7nafmG .statediagram-note .nodeLabel{color:black;}#mermaid-svg-qweVqPEo3l7nafmG .statediagram .edgeLabel{color:red;}#mermaid-svg-qweVqPEo3l7nafmG #dependencyStart,#mermaid-svg-qweVqPEo3l7nafmG #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-qweVqPEo3l7nafmG .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qweVqPEo3l7nafmG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} const app = express()
app.use()
app.get/post等
app.listen()
接收HTTP请求
路由匹配
res.send等
创建应用实例
配置中间件
定义路由
启动服务器
监听请求
处理请求
返回响应
2.4 Express 静态资源服务详解
javascript
const express = require('express');
const path = require('path');
const app = express();
// 【代码注释】方式一:单个静态目录
// 访问 http://localhost:3000/images/logo.png
// 实际路径:public/images/logo.png
app.use(express.static(path.join(__dirname, 'public')));
// 【代码注释】方式二:多个静态目录
// 为静态目录指定虚拟路径前缀
app.use('/static', express.static(path.join(__dirname, 'public')));
// 访问:http://localhost:3000/static/images/logo.png
// 【代码注释】方式三:设置缓存控制
const options = {
maxAge: '1d', // 缓存一天
etag: true, // 启用 ETag
lastModified: true, // 使用 Last-Modified 头
setHeaders: (res, path) => {
// 自定义响应头
if (path.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache');
}
}
};
app.use(express.static(path.join(__dirname, 'public'), options));
app.listen(3000);
【代码注释】
express.static(root):请求/css/app.css映射到public/css/app.css;根路径/自动尝试index.html。- 虚拟前缀:
app.use('/static', express.static('public'))则访问/static/css/app.css,HTML 里引用路径要一致。 options中maxAge设置Cache-Control缓存;etag/lastModified支持 304 协商缓存,减轻带宽。- 多个
app.use(express.static(...))按注册顺序查找,先匹配先返回。
静态资源优化策略
- 缓存控制:设置合适的 Cache-Control 头
- 文件压缩:使用 gzip/brotli 压缩
- CDN 加速:分发到全球节点
- 图片优化:使用 WebP/AVIF 格式
- 资源合并:减少 HTTP 请求数
三、路由系统详解
3.1 路由概念解析
路由是指确定应用程序如何响应客户端对特定端点的请求。路由由一个 HTTP 方法(GET、POST 等)和路径组成。
路由组成要素
渲染错误: Mermaid 渲染失败: Lexical error on line 6. Unrecognized text. ...路径PATH] --> G/users F --> H[/users -----------------------^
3.2 Express 支持的 HTTP 方法
javascript
const express = require('express');
const app = express();
// 【代码注释】GET 方法:用于获取资源
app.get('/users', (req, res) => {
res.json({ action: '获取用户列表' });
});
// 【代码注释】POST 方法:用于创建资源
app.post('/users', (req, res) => {
res.json({ action: '创建新用户' });
});
// 【代码注释】PUT 方法:用于更新整个资源
app.put('/users/:id', (req, res) => {
res.json({ action: '更新用户' });
});
// 【代码注释】PATCH 方法:用于部分更新资源
app.patch('/users/:id', (req, res) => {
res.json({ action: '部分更新用户' });
});
// 【代码注释】DELETE 方法:用于删除资源
app.delete('/users/:id', (req, res) => {
res.json({ action: '删除用户' });
});
// 【代码注释】all 方法:匹配所有 HTTP 方法
app.all('/test', (req, res) => {
res.json({
method: req.method,
message: '匹配所有HTTP方法'
});
});
app.listen(3000);
【代码注释】
- REST 风格:
GET读、POST建、PUT全量更新、PATCH部分更新、DELETE删;路径常带:id表示资源标识。 app.all('/test')对同一路径响应任意 HTTP 方法,调试时可用req.method区分;生产少用,易与安全策略冲突。- 同一路径不同方法会注册多条路由;Express 按 方法 + 路径 精确匹配,未命中则继续下一个中间件或 404。
- 课堂端口 8080 与文档示例
3000等价,改PORT常量即可统一。
3.3 路径匹配模式
精确匹配
javascript
// 【代码注释】精确匹配:只能匹配 /home/index
app.get('/home/index', (req, res) => {
res.send('首页');
});
// 【代码注释】匹配根路径
app.get('/', (req, res) => {
res.send('网站首页');
});
字符串模糊匹配
javascript
// 【代码注释】使用通配符进行模糊匹配
// ? 匹配前一个字符 0 次或 1 次
app.get('/index.html?', (req, res) => {
// 匹配:/index.html 和 /index
res.send('匹配成功');
});
// + 匹配前一个字符 1 次或多次
app.get('/user+', (req, res) => {
// 匹配:/user, /userr, /userrr 等
res.send('匹配成功');
});
// * 匹配任意字符任意次数
app.get('/admin/*', (req, res) => {
// 匹配:/admin/, /admin/index, /admin/user/list 等
res.send('管理后台');
});
// () 将字符作为整体匹配
app.get('/index(.html)?', (req, res) => {
// 匹配:/index 和 /index.html
res.send('首页');
});
正则表达式匹配
javascript
// 【代码注释】使用正则表达式进行复杂匹配
// 匹配所有以 .html 结尾的路径
app.get(/\.html$/, (req, res) => {
res.send('HTML 页面');
});
// 匹配所有包含 'item' 的路径
app.get(/item/, (req, res) => {
res.send('商品页面');
});
// 匹配特定格式的路径
app.get(/^\/users\/\d+$/, (req, res) => {
// 匹配:/users/123, /users/456 等
res.send('用户详情页');
});
动态路由参数
javascript
// 【代码注释】定义带参数的路由
// :id 是参数占位符,可以匹配任意值
app.get('/users/:id', (req, res) => {
// req.params 对象包含路由参数
const userId = req.params.id;
res.send(`用户ID:${userId}`);
});
// 【代码注释】多个参数
app.get('/users/:userId/posts/:postId', (req, res) => {
const { userId, postId } = req.params;
res.send(`用户:${userId},文章:${postId}`);
});
// 【代码注释】可选参数
app.get('/books/:year?', (req, res) => {
const year = req.params.year || '2024';
res.send(`图书年份:${year}`);
});
【代码注释】
req.params来自路径占位符:id;值为字符串,比较数字需parseInt或Number()。- 模糊路径
?+*是 Express 路径语法,不是正则字面量;复杂规则用app.get(/^\/users\/\d+$/)。 :id(\\d+)限制只匹配数字,避免/users/admin被当成 id。- 可选参数
:year?未传时req.params.year为undefined,需默认值。 - 新闻详情推荐
/news/:id(路径参数)而非/news/details?id=(查询串),利于分享链接与缓存。
3.4 路由处理器
单个回调函数
javascript
app.get('/api/data', (req, res) => {
res.json({ message: '数据获取成功' });
});
多个回调函数
javascript
// 【代码注释】多个回调函数形成处理链
// 第一个回调函数
app.get('/api/users', (req, res, next) => {
console.log('第一个处理函数');
// 【代码注释】next() 将控制权传递给下一个处理函数
next();
}, (req, res) => {
// 第二个处理函数
console.log('第二个处理函数');
res.json({ users: ['张三', '李四'] });
});
回调函数数组
javascript
// 【代码注释】定义中间件函数数组
const middleware1 = (req, res, next) => {
console.log('中间件1:身份验证');
// 在这里进行身份验证
next();
};
const middleware2 = (req, res, next) => {
console.log('中间件2:日志记录');
next();
};
const handler = (req, res) => {
res.json({ message: '请求处理完成' });
};
// 【代码注释】使用数组传递多个处理函数
app.get('/api/protected', [middleware1, middleware2], handler);
3.5 app.route() 方法
javascript
// 【代码注释】使用 app.route() 创建链式路由
// 可以为同一路径定义多个 HTTP 方法的处理
app.route('/login')
.get((req, res) => {
// GET /login - 显示登录表单
res.send('<h1>登录表单</h1>');
})
.post((req, res) => {
// POST /login - 处理登录请求
res.send('<h1>登录处理</h1>');
});
// 【代码注释】更复杂的示例
app.route('/articles')
.get((req, res) => {
// 获取文章列表
res.json({ action: '获取文章列表' });
})
.post((req, res) => {
// 创建新文章
res.json({ action: '创建文章' });
});
app.route('/articles/:id')
.get((req, res) => {
// 获取文章详情
res.json({ action: '获取文章详情', id: req.params.id });
})
.put((req, res) => {
// 更新文章
res.json({ action: '更新文章', id: req.params.id });
})
.delete((req, res) => {
// 删除文章
res.json({ action: '删除文章', id: req.params.id });
});
【代码注释】
app.route('/login')对同一路径 链式注册.get().post(),避免重复写/login。app.route('/articles/:id')可继续.get().put().delete(),REST 资源一目了然。- 与
router.route类似;大项目可在express.Router()上使用router.route('/:id')。 - 链中任一步可挂中间件:
app.route('/x').all(auth).get(...)(需 Express 支持 all 在链首)。
3.6 404 错误处理
javascript
// 【代码注释】在所有路由之后定义 404 处理
// 使用 app.all('*') 匹配所有未定义的路径
app.all('*', (req, res) => {
res.status(404).send(`
<!DOCTYPE html>
<html>
<head>
<title>404 - 页面不存在</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
padding: 50px;
}
h1 { font-size: 72px; margin: 0; }
p { color: #666; }
</style>
</head>
<body>
<h1>404</h1>
<p>抱歉,您访问的页面不存在。</p>
<a href="/">返回首页</a>
</body>
</html>
`);
});
【代码注释】
app.all('*', handler)必须放在所有业务路由之后,作为兜底;否则会拦截尚未注册的路由。- Express 4 也可用
app.use((req,res)=>{ res.status(404)... })(无路径的 use),效果类似。 - 返回 HTML 404 页适合浏览器;API 项目宜
res.status(404).json({ error: 'Not Found' })。 - 与中间件区别:404 处理器通常不 调用
next(),直接结束响应。
路由匹配流程
#mermaid-svg-vsYDCjrPNJQKb3TF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vsYDCjrPNJQKb3TF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vsYDCjrPNJQKb3TF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vsYDCjrPNJQKb3TF .error-icon{fill:#552222;}#mermaid-svg-vsYDCjrPNJQKb3TF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vsYDCjrPNJQKb3TF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vsYDCjrPNJQKb3TF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vsYDCjrPNJQKb3TF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vsYDCjrPNJQKb3TF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vsYDCjrPNJQKb3TF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vsYDCjrPNJQKb3TF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vsYDCjrPNJQKb3TF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vsYDCjrPNJQKb3TF .marker.cross{stroke:#333333;}#mermaid-svg-vsYDCjrPNJQKb3TF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vsYDCjrPNJQKb3TF p{margin:0;}#mermaid-svg-vsYDCjrPNJQKb3TF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vsYDCjrPNJQKb3TF .cluster-label text{fill:#333;}#mermaid-svg-vsYDCjrPNJQKb3TF .cluster-label span{color:#333;}#mermaid-svg-vsYDCjrPNJQKb3TF .cluster-label span p{background-color:transparent;}#mermaid-svg-vsYDCjrPNJQKb3TF .label text,#mermaid-svg-vsYDCjrPNJQKb3TF span{fill:#333;color:#333;}#mermaid-svg-vsYDCjrPNJQKb3TF .node rect,#mermaid-svg-vsYDCjrPNJQKb3TF .node circle,#mermaid-svg-vsYDCjrPNJQKb3TF .node ellipse,#mermaid-svg-vsYDCjrPNJQKb3TF .node polygon,#mermaid-svg-vsYDCjrPNJQKb3TF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vsYDCjrPNJQKb3TF .rough-node .label text,#mermaid-svg-vsYDCjrPNJQKb3TF .node .label text,#mermaid-svg-vsYDCjrPNJQKb3TF .image-shape .label,#mermaid-svg-vsYDCjrPNJQKb3TF .icon-shape .label{text-anchor:middle;}#mermaid-svg-vsYDCjrPNJQKb3TF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vsYDCjrPNJQKb3TF .rough-node .label,#mermaid-svg-vsYDCjrPNJQKb3TF .node .label,#mermaid-svg-vsYDCjrPNJQKb3TF .image-shape .label,#mermaid-svg-vsYDCjrPNJQKb3TF .icon-shape .label{text-align:center;}#mermaid-svg-vsYDCjrPNJQKb3TF .node.clickable{cursor:pointer;}#mermaid-svg-vsYDCjrPNJQKb3TF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vsYDCjrPNJQKb3TF .arrowheadPath{fill:#333333;}#mermaid-svg-vsYDCjrPNJQKb3TF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vsYDCjrPNJQKb3TF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vsYDCjrPNJQKb3TF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vsYDCjrPNJQKb3TF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vsYDCjrPNJQKb3TF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vsYDCjrPNJQKb3TF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vsYDCjrPNJQKb3TF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vsYDCjrPNJQKb3TF .cluster text{fill:#333;}#mermaid-svg-vsYDCjrPNJQKb3TF .cluster span{color:#333;}#mermaid-svg-vsYDCjrPNJQKb3TF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vsYDCjrPNJQKb3TF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vsYDCjrPNJQKb3TF rect.text{fill:none;stroke-width:0;}#mermaid-svg-vsYDCjrPNJQKb3TF .icon-shape,#mermaid-svg-vsYDCjrPNJQKb3TF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vsYDCjrPNJQKb3TF .icon-shape p,#mermaid-svg-vsYDCjrPNJQKb3TF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vsYDCjrPNJQKb3TF .icon-shape .label rect,#mermaid-svg-vsYDCjrPNJQKb3TF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vsYDCjrPNJQKb3TF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vsYDCjrPNJQKb3TF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vsYDCjrPNJQKb3TF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 精确匹配
模糊匹配
参数匹配
无匹配
接收请求
匹配路由
执行对应处理函数
执行处理函数
提取参数并处理
执行404处理
返回响应
四、请求与响应对象
4.1 请求对象(Request)详解
请求对象代表 HTTP 请求,包含请求的各种信息。
核心属性和方法
javascript
const express = require('express');
const app = express();
app.get('/demo', (req, res) => {
// 【代码注释】req.app - 保留对 Express 应用实例的引用
console.log('应用实例:', req.app);
// 【代码注释】req.url - 请求路径(包含查询字符串)
console.log('请求URL:', req.url);
// 【代码注释】req.method - HTTP 方法
console.log('请求方法:', req.method);
// 【代码注释】req.ip - 客户端 IP 地址
console.log('客户端IP:', req.ip);
// 【代码注释】req.hostname - 主机名(从 Host 头获取)
console.log('主机名:', req.hostname);
// 【代码注释】req.protocol - 请求协议(http 或 https)
console.log('协议:', req.protocol);
// 【代码注释】req.path - URL 路径部分(不含查询字符串)
console.log('路径:', req.path);
// 【代码注释】req.query - 查询字符串参数对象
console.log('查询参数:', req.query);
// 【代码注释】req.params - 路由参数对象
console.log('路由参数:', req.params);
res.send('查看控制台输出');
});
获取请求头
javascript
app.get('/headers', (req, res) => {
// 【代码注释】req.get() - 获取指定请求头
const userAgent = req.get('user-agent');
const acceptLanguage = req.get('accept-language');
const contentType = req.get('content-type');
// 【代码注释】req.headers - 所有请求头的对象
const allHeaders = req.headers;
res.json({
userAgent,
acceptLanguage,
contentType,
allHeaders
});
});
获取查询字符串参数
javascript
// 【代码注释】处理查询字符串:/search?keyword=nodejs&page=1
app.get('/search', (req, res) => {
const { keyword, page } = req.query;
res.send(`
<h1>搜索结果</h1>
<p>关键词:${keyword}</p>
<p>页码:${page}</p>
`);
});
// 【代码注释】复杂查询字符串:/filter?tag=nodejs&tag=express
app.get('/filter', (req, res) => {
// req.query.tag 会是字符串 'express'(最后一个值)
// 要获取所有值,需要手动解析
const tags = req.query.tag || [];
const tagArray = Array.isArray(tags) ? tags : [tags];
res.json({ tags: tagArray });
});
【代码注释】
req.query由查询串?key=value解析,值为字符串;page参与运算需parseInt(page, 10)。- 同名参数重复(
?tag=a&tag=b)时 Express 默认后者覆盖;要数组需extended配置或自行解析原始 URL。 req.path不含查询串;req.url含查询串;日志与鉴权常用req.path。req.get('user-agent')大小写不敏感,等价于读req.headers规范化后的键。
获取路由参数
javascript
// 【代码注释】定义带参数的路由
app.get('/articles/:category/:id(\\d+)', (req, res) => {
// req.params 包含所有路由参数
const { category, id } = req.params;
res.json({
message: '文章详情',
category,
id,
idType: typeof id
});
});
// 【代码注释】可选路由参数
app.get('/books/:title?', (req, res) => {
const title = req.params.title || '未指定';
res.json({ title });
});
4.2 获取请求体数据
安装和配置 body-parser
bash
# 【代码注释】安装 body-parser 中间件
npm install body-parser
javascript
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
// 【代码注释】配置 body-parser 中间件
// 解析 application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }));
// 【代码注释】解析 application/json
app.use(bodyParser.json());
// 【代码注释】处理表单提交
app.route('/contact')
.get((req, res) => {
// 显示表单
res.send(`
<form method="POST" action="/contact">
<input type="text" name="name" placeholder="姓名" required />
<input type="email" name="email" placeholder="邮箱" required />
<textarea name="message" placeholder="留言"></textarea>
<button type="submit">提交</button>
</form>
`);
})
.post((req, res) => {
// 获取表单数据
const { name, email, message } = req.body;
res.json({
message: '提交成功',
data: { name, email, message }
});
});
app.listen(3000);
Express 4.16+ 内置解析
javascript
const express = require('express');
const app = express();
// 【代码注释】Express 4.16+ 内置了 body-parser 功能
// 解析 JSON 格式请求体
app.use(express.json());
// 【代码注释】解析 URL 编码格式请求体
app.use(express.urlencoded({ extended: true }));
// 【代码注释】处理 API 请求
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
// 数据验证
if (!name || !email) {
return res.status(400).json({
error: '姓名和邮箱不能为空'
});
}
res.json({
message: '用户创建成功',
user: { id: Date.now(), name, email }
});
});
【代码注释】
express.json()/express.urlencoded()必须挂在路由之前 ,否则req.body为undefined。urlencoded({ extended: true })使用qs库,支持嵌套对象;false仅用querystring,更简单。- 表单
method="POST"+application/x-www-form-urlencoded对应urlencoded;AJAX 发 JSON 对应json()。 - 旧项目
body-parser与 Express 4.16+ 内置能力等价,新项目直接用express.json()。 - 校验失败应
return res.status(400).json(...),避免继续执行创建逻辑。
4.3 响应对象(Response)详解
设置响应状态码
javascript
const express = require('express');
const app = express();
// 【代码注释】设置不同的 HTTP 状态码
app.get('/ok', (req, res) => {
res.status(200).send('OK'); // 成功
});
app.get('/created', (req, res) => {
res.status(201).send('Created'); // 资源已创建
});
app.get('/not-found', (req, res) => {
res.status(404).send('Not Found'); // 资源未找到
});
app.get('/server-error', (req, res) => {
res.status(500).send('Server Error'); // 服务器错误
});
// 【代码注释】链式调用
app.get('/user', (req, res) => {
res.status(200)
.set('Content-Type', 'application/json')
.json({ name: '张三', age: 25 });
});
设置响应头
javascript
app.get('/custom-headers', (req, res) => {
// 【代码注释】设置单个响应头
res.set('Custom-Header', 'Custom-Value');
// 【代码注释】设置多个响应头
res.set({
'X-Powered-By': 'Express',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY'
});
res.send('响应头已设置');
});
// 【代码注释】设置 CORS 头
app.get('/api/data', (req, res) => {
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.set('Access-Control-Allow-Headers', 'Content-Type');
res.json({ data: '允许跨域访问的数据' });
});
发送响应内容
javascript
const express = require('express');
const path = require('path');
const app = express();
// 【代码注释】res.send() - 发送各种类型的响应
app.get('/send-text', (req, res) => {
res.send('纯文本响应');
});
app.get('/send-html', (req, res) => {
res.send('<h1>HTML响应</h1>');
});
app.get('/send-object', (req, res) => {
res.send({ key: 'value', number: 123 });
});
app.get('/send-array', (req, res) => {
res.send([1, 2, 3, 4, 5]);
});
// 【代码注释】res.json() - 发送 JSON 响应
app.get('/api/users', (req, res) => {
res.json({
success: true,
data: [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
]
});
});
// 【代码注释】res.sendFile() - 发送文件
app.get('/file', (req, res) => {
const filePath = path.join(__dirname, 'public', 'index.html');
res.sendFile(filePath);
});
// 【代码注释】res.download() - 触发文件下载
app.get('/download', (req, res) => {
const filePath = path.join(__dirname, 'files', 'document.pdf');
res.download(filePath, 'downloaded-file.pdf');
});
// 【代码注释】res.render() - 渲染模板
app.set('view engine', 'ejs');
app.get('/template', (req, res) => {
res.render('index', {
title: '首页',
message: '欢迎使用Express'
});
});
重定向
javascript
// 【代码注释】res.redirect() - 重定向到其他URL
app.get('/redirect-example', (req, res) => {
// 默认重定向(302 临时重定向)
res.redirect('/target');
});
app.get('/redirect-permanent', (req, res) => {
// 301 永久重定向
res.redirect(301, '/target');
});
app.get('/redirect-back', (req, res) => {
// 重定向到上一页
res.redirect('back');
});
app.get('/external', (req, res) => {
// 重定向到外部网站
res.redirect('https://www.example.com');
});
app.get('/target', (req, res) => {
res.send('这是重定向目标页面');
});
【代码注释】
res.status(200).json(obj)可链式设置状态码与 JSON;常见:200 成功、201 创建、400 参数错误、404 无资源、500 服务器错误。res.send根据类型自动设Content-Type;对象会 JSON 序列化,与res.json类似但json显式application/json。res.sendFile需绝对路径 ,常用path.join(__dirname, 'public', 'index.html')。res.download会设Content-Disposition: attachment,触发浏览器下载而非内联打开。res.redirect(301, url)永久重定向利于 SEO;302临时;redirect('back')依赖Referer头。- CORS 简单场景可
res.set('Access-Control-Allow-Origin', '*');复杂预检用cors中间件(见 §5.5)。
重定向流程图
服务器 客户端 服务器 客户端 #mermaid-svg-YIAUBF0iIkISp75k{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YIAUBF0iIkISp75k .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YIAUBF0iIkISp75k .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YIAUBF0iIkISp75k .error-icon{fill:#552222;}#mermaid-svg-YIAUBF0iIkISp75k .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YIAUBF0iIkISp75k .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YIAUBF0iIkISp75k .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YIAUBF0iIkISp75k .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YIAUBF0iIkISp75k .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YIAUBF0iIkISp75k .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YIAUBF0iIkISp75k .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YIAUBF0iIkISp75k .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YIAUBF0iIkISp75k .marker.cross{stroke:#333333;}#mermaid-svg-YIAUBF0iIkISp75k svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YIAUBF0iIkISp75k p{margin:0;}#mermaid-svg-YIAUBF0iIkISp75k .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-YIAUBF0iIkISp75k text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-YIAUBF0iIkISp75k .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-YIAUBF0iIkISp75k .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-YIAUBF0iIkISp75k .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-YIAUBF0iIkISp75k .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-YIAUBF0iIkISp75k #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-YIAUBF0iIkISp75k .sequenceNumber{fill:white;}#mermaid-svg-YIAUBF0iIkISp75k #sequencenumber{fill:#333;}#mermaid-svg-YIAUBF0iIkISp75k #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-YIAUBF0iIkISp75k .messageText{fill:#333;stroke:none;}#mermaid-svg-YIAUBF0iIkISp75k .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-YIAUBF0iIkISp75k .labelText,#mermaid-svg-YIAUBF0iIkISp75k .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-YIAUBF0iIkISp75k .loopText,#mermaid-svg-YIAUBF0iIkISp75k .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-YIAUBF0iIkISp75k .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-YIAUBF0iIkISp75k .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-YIAUBF0iIkISp75k .noteText,#mermaid-svg-YIAUBF0iIkISp75k .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-YIAUBF0iIkISp75k .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-YIAUBF0iIkISp75k .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-YIAUBF0iIkISp75k .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-YIAUBF0iIkISp75k .actorPopupMenu{position:absolute;}#mermaid-svg-YIAUBF0iIkISp75k .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-YIAUBF0iIkISp75k .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-YIAUBF0iIkISp75k .actor-man circle,#mermaid-svg-YIAUBF0iIkISp75k line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-YIAUBF0iIkISp75k :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GET /redirect-example 302 Found Location: /target GET /target 200 OK 目标页面内容
五、中间件机制
5.1 中间件概念解析
中间件是一个函数,它可以访问请求对象、响应对象和下一个中间件函数。
中间件执行流程
路由处理器 中间件2 中间件1 客户端 路由处理器 中间件2 中间件1 客户端 #mermaid-svg-Rf6ICpXWxEwOy5Dz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Rf6ICpXWxEwOy5Dz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .error-icon{fill:#552222;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .marker.cross{stroke:#333333;}#mermaid-svg-Rf6ICpXWxEwOy5Dz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Rf6ICpXWxEwOy5Dz p{margin:0;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Rf6ICpXWxEwOy5Dz text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Rf6ICpXWxEwOy5Dz .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Rf6ICpXWxEwOy5Dz #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .sequenceNumber{fill:white;}#mermaid-svg-Rf6ICpXWxEwOy5Dz #sequencenumber{fill:#333;}#mermaid-svg-Rf6ICpXWxEwOy5Dz #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .messageText{fill:#333;stroke:none;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .labelText,#mermaid-svg-Rf6ICpXWxEwOy5Dz .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .loopText,#mermaid-svg-Rf6ICpXWxEwOy5Dz .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Rf6ICpXWxEwOy5Dz .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .noteText,#mermaid-svg-Rf6ICpXWxEwOy5Dz .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .actorPopupMenu{position:absolute;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Rf6ICpXWxEwOy5Dz .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Rf6ICpXWxEwOy5Dz .actor-man circle,#mermaid-svg-Rf6ICpXWxEwOy5Dz line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Rf6ICpXWxEwOy5Dz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求 处理逻辑 next() 处理逻辑 next() 响应
中间件函数签名
javascript
// 【代码注释】中间件函数的基本结构
function middlewareFunction(request, response, next) {
// request - 请求对象
// response - 响应对象
// next - 下一个中间件函数
// 执行一些逻辑
// 调用 next() 将控制权传递给下一个中间件
next();
}
5.2 应用级中间件
javascript
const express = require('express');
const app = express();
// 【代码注释】日志中间件 - 记录每个请求的信息
app.use((req, res, next) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${req.method} ${req.url}`);
next();
});
// 【代码注释】请求计时中间件
app.use((req, res, next) => {
req.startTime = Date.now();
// 【代码注释】监听响应结束事件
res.on('finish', () => {
const duration = Date.now() - req.startTime;
console.log(`请求处理时间:${duration}ms`);
});
next();
});
// 【代码注释】认证中间件 - 特定路径
app.use('/admin', (req, res, next) => {
const isAuthenticated = false; // 模拟认证状态
if (isAuthenticated) {
next(); // 已认证,继续处理
} else {
res.status(401).send('未授权访问');
}
});
// 【代码注释】响应头中间件
app.use((req, res, next) => {
res.setHeader('X-Powered-By', 'MyApp');
res.setHeader('X-Content-Type-Options', 'nosniff');
next();
});
// 【代码注释】路由定义
app.get('/', (req, res) => {
res.send('首页');
});
app.get('/admin/dashboard', (req, res) => {
res.send('管理后台');
});
【代码注释】
- 应用级
app.use(fn):对所有请求生效(除非fn里根据req.path提前return);顺序 = 注册顺序。 - 未调用
next()且未res.send/res.end时,请求会挂起 ;未认证时res.status(401).send直接结束,不再进入后续路由。 app.use('/admin', fn)只匹配路径以/admin开头的请求(前缀匹配),常用于后台鉴权。res.on('finish')在响应发送完成后触发,可统计耗时,类似 APM 埋点雏形。- 路由定义应放在「日志、解析 body、CORS」等公共中间件之后 ,404 兜底放最后。
5.3 路由级中间件
javascript
const express = require('express');
const router = express.Router();
// 【代码注释】路由级中间件 - 只应用于特定路由
// 验证中间件
const validateUser = (req, res, next) => {
const userId = req.params.id;
if (!userId || isNaN(userId)) {
return res.status(400).json({ error: '无效的用户ID' });
}
next();
};
// 日志中间件
const logRoute = (req, res, next) => {
console.log(`访问路由:${req.originalUrl}`);
next();
};
// 【代码注释】应用中间件到路由
router.get('/users/:id', validateUser, logRoute, (req, res) => {
res.json({ userId: req.params.id });
});
// 【代码注释】多个中间件
router.post('/users',
(req, res, next) => {
// 数据验证中间件
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: '缺少必要字段' });
}
next();
},
(req, res, next) => {
// 数据处理中间件
req.body.createdAt = new Date();
next();
},
(req, res) => {
// 最终处理
res.json({
message: '用户创建成功',
user: req.body
});
}
);
module.exports = router;
5.4 错误处理中间件
javascript
const express = require('express');
const app = express();
// 【代码注释】404 错误处理中间件
app.use((req, res, next) => {
res.status(404).json({
error: 'Not Found',
message: '请求的资源不存在',
path: req.url
});
});
// 【代码注释】错误处理中间件 - 4个参数
app.use((err, req, res, next) => {
console.error('错误堆栈:', err.stack);
// 根据错误类型返回不同的状态码
const statusCode = err.statusCode || 500;
const message = err.message || '服务器内部错误';
res.status(statusCode).json({
error: message,
// 在开发环境返回错误堆栈
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});
// 【代码注释】自定义错误类
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// 【代码注释】在路由中抛出错误
app.get('/error', (req, res, next) => {
const error = new AppError('这是一个自定义错误', 400);
next(error);
});
// 【代码注释】异步错误处理
app.get('/async-error', async (req, res, next) => {
try {
// 模拟异步操作
await Promise.reject(new Error('异步操作失败'));
} catch (error) {
next(error);
}
});
【代码注释】
- 404 中间件 :无路由匹配时执行;必须放在所有
app.get/post之后,否则会把正常路由也变成 404。 - 错误处理中间件 :签名必须是
(err, req, res, next)四个参数,Express 据此识别为错误处理器;普通中间件 3 个参数。 - 路由里
next(error)或throw在 async 路由中需try/catch+next(err)(Express 5 对 async 错误有改进,课堂以显式next为准)。 AppError+statusCode:区分业务错误(400)与未知错误(500);生产环境勿在 JSON 里返回stack。- 开发环境
NODE_ENV=development可临时返回堆栈便于调试。
5.5 内置中间件
javascript
const express = require('express');
const path = require('path');
const app = express();
// 【代码注释】express.static() - 静态文件服务
app.use(express.static(path.join(__dirname, 'public')));
// 【代码注释】express.json() - 解析 JSON 请求体
app.use(express.json());
// 【代码注释】express.urlencoded() - 解析 URL 编码请求体
app.use(express.urlencoded({ extended: true }));
// 【代码注释】express.Router() - 创建路由模块
const apiRouter = express.Router();
apiRouter.get('/data', (req, res) => {
res.json({ message: 'API路由数据' });
});
app.use('/api', apiRouter);
app.listen(3000);
【代码注释】
- 推荐注册顺序:
static→json/urlencoded→ 业务Router→ 404 → 错误处理(四参数)。 express.Router()挂载app.use('/api', apiRouter)后,apiRouter.get('/data')对外路径为/api/data。- 静态与 API 可共存:未命中静态文件时继续
next()进入后续路由(static内部行为)。 - 勿在路由之后重复注册
json(),否则已结束响应的请求无意义且浪费解析。
5.6 第三方中间件
常用中间件推荐
| 中间件 | 功能 | 使用场景 |
|---|---|---|
| morgan | HTTP 请求日志 | 开发调试、生产监控 |
| helmet | 安全头设置 | 生产环境安全加固 |
| cors | 跨域资源共享 | API 服务 |
| compression | 响应压缩 | 提升性能 |
| cookie-parser | Cookie 解析 | 会话管理 |
| express-session | 会话管理 | 用户登录状态 |
安装和使用示例
javascript
const express = require('express');
const morgan = require('morgan');
const helmet = require('helmet');
const cors = require('cors');
const compression = require('compression');
const app = express();
// 【代码注释】morgan - 请求日志
app.use(morgan('combined')); // 详细日志格式
// app.use(morgan('dev')); // 开发环境格式
// app.use(morgan('short')); // 简洁格式
// 【代码注释】helmet - 安全增强
app.use(helmet());
// 【代码注释】cors - 跨域支持
app.use(cors()); // 允许所有来源
// 或者配置 CORS
app.use(cors({
origin: ['https://example.com', 'https://www.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// 【代码注释】compression - 响应压缩
app.use(compression());
// 【代码注释】测试压缩效果
app.get('/large-data', (req, res) => {
const largeData = 'x'.repeat(10000); // 10KB 数据
res.send(largeData);
});
app.listen(3000);
【代码注释】
morgan('dev')彩色简短日志适合开发;combined含 IP、User-Agent,接近 Apache 日志格式。helmet()设置 XSS、点击劫持等安全响应头,生产 API 建议默认开启。cors()无参允许任意来源*,生产应origin白名单;预检 OPTIONS 由cors自动处理。compression()对文本响应 gzip,配合大 JSON/HTML 降带宽;图片等已压缩格式收益小。- 第三方中间件同样遵守
next()链;compression通常放在路由之前、靠近响应出口一侧亦可(文档示意图为概念顺序)。
中间件执行顺序示意图
#mermaid-svg-Xk9KQmcYR4JTx3Wo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Xk9KQmcYR4JTx3Wo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .error-icon{fill:#552222;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .marker.cross{stroke:#333333;}#mermaid-svg-Xk9KQmcYR4JTx3Wo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Xk9KQmcYR4JTx3Wo p{margin:0;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .cluster-label text{fill:#333;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .cluster-label span{color:#333;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .cluster-label span p{background-color:transparent;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .label text,#mermaid-svg-Xk9KQmcYR4JTx3Wo span{fill:#333;color:#333;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .node rect,#mermaid-svg-Xk9KQmcYR4JTx3Wo .node circle,#mermaid-svg-Xk9KQmcYR4JTx3Wo .node ellipse,#mermaid-svg-Xk9KQmcYR4JTx3Wo .node polygon,#mermaid-svg-Xk9KQmcYR4JTx3Wo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .rough-node .label text,#mermaid-svg-Xk9KQmcYR4JTx3Wo .node .label text,#mermaid-svg-Xk9KQmcYR4JTx3Wo .image-shape .label,#mermaid-svg-Xk9KQmcYR4JTx3Wo .icon-shape .label{text-anchor:middle;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .rough-node .label,#mermaid-svg-Xk9KQmcYR4JTx3Wo .node .label,#mermaid-svg-Xk9KQmcYR4JTx3Wo .image-shape .label,#mermaid-svg-Xk9KQmcYR4JTx3Wo .icon-shape .label{text-align:center;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .node.clickable{cursor:pointer;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .arrowheadPath{fill:#333333;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Xk9KQmcYR4JTx3Wo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Xk9KQmcYR4JTx3Wo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Xk9KQmcYR4JTx3Wo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .cluster text{fill:#333;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .cluster span{color:#333;}#mermaid-svg-Xk9KQmcYR4JTx3Wo div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Xk9KQmcYR4JTx3Wo rect.text{fill:none;stroke-width:0;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .icon-shape,#mermaid-svg-Xk9KQmcYR4JTx3Wo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .icon-shape p,#mermaid-svg-Xk9KQmcYR4JTx3Wo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .icon-shape .label rect,#mermaid-svg-Xk9KQmcYR4JTx3Wo .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Xk9KQmcYR4JTx3Wo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Xk9KQmcYR4JTx3Wo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Xk9KQmcYR4JTx3Wo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求
日志记录
安全检查
CORS处理
请求体解析
身份验证
路由匹配
路由处理
响应压缩
响应
5.7 实战:JWT 身份认证中间件
JWT(JSON Web Token)是现代 RESTful API 最主流的无状态身份认证方案。服务器签发一个加密令牌给客户端,客户端每次请求携带该令牌,服务器验证后即可确认身份------无需维护服务端会话状态,天然支持横向扩展。
JWT 结构
JWT 由三段 Base64 编码字符串组成,以 . 分隔:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← Header(算法类型)
.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiJ9 ← Payload(用户数据,可解码但不可篡改)
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← Signature(用密钥签名,防篡改)
JWT 完整认证流程
数据库 Express 服务器 客户端 数据库 Express 服务器 客户端 #mermaid-svg-yTZRYWKEoQmxSQpO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yTZRYWKEoQmxSQpO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yTZRYWKEoQmxSQpO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yTZRYWKEoQmxSQpO .error-icon{fill:#552222;}#mermaid-svg-yTZRYWKEoQmxSQpO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yTZRYWKEoQmxSQpO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yTZRYWKEoQmxSQpO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yTZRYWKEoQmxSQpO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yTZRYWKEoQmxSQpO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yTZRYWKEoQmxSQpO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yTZRYWKEoQmxSQpO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yTZRYWKEoQmxSQpO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yTZRYWKEoQmxSQpO .marker.cross{stroke:#333333;}#mermaid-svg-yTZRYWKEoQmxSQpO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yTZRYWKEoQmxSQpO p{margin:0;}#mermaid-svg-yTZRYWKEoQmxSQpO .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yTZRYWKEoQmxSQpO text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-yTZRYWKEoQmxSQpO .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-yTZRYWKEoQmxSQpO .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-yTZRYWKEoQmxSQpO .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-yTZRYWKEoQmxSQpO .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-yTZRYWKEoQmxSQpO #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-yTZRYWKEoQmxSQpO .sequenceNumber{fill:white;}#mermaid-svg-yTZRYWKEoQmxSQpO #sequencenumber{fill:#333;}#mermaid-svg-yTZRYWKEoQmxSQpO #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-yTZRYWKEoQmxSQpO .messageText{fill:#333;stroke:none;}#mermaid-svg-yTZRYWKEoQmxSQpO .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yTZRYWKEoQmxSQpO .labelText,#mermaid-svg-yTZRYWKEoQmxSQpO .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-yTZRYWKEoQmxSQpO .loopText,#mermaid-svg-yTZRYWKEoQmxSQpO .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-yTZRYWKEoQmxSQpO .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-yTZRYWKEoQmxSQpO .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-yTZRYWKEoQmxSQpO .noteText,#mermaid-svg-yTZRYWKEoQmxSQpO .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-yTZRYWKEoQmxSQpO .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yTZRYWKEoQmxSQpO .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yTZRYWKEoQmxSQpO .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yTZRYWKEoQmxSQpO .actorPopupMenu{position:absolute;}#mermaid-svg-yTZRYWKEoQmxSQpO .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-yTZRYWKEoQmxSQpO .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yTZRYWKEoQmxSQpO .actor-man circle,#mermaid-svg-yTZRYWKEoQmxSQpO line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-yTZRYWKEoQmxSQpO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 将 token 存储在 localStorage 或 Cookie POST /auth/login {username, password} 查询用户,验证密码哈希 返回用户信息 jwt.sign({id, role}, SECRET) 生成 token { token: "eyJ..." } GET /api/profile Authorization: Bearer eyJ... jwt.verify(token, SECRET) 验证签名和过期 { user: {id, username, role} }
安装依赖
bash
# jsonwebtoken:生成和验证 JWT
# bcryptjs:密码哈希(永远不要明文存储密码!)
npm install jsonwebtoken bcryptjs
JWT 中间件实现
javascript
// middlewares/auth.js
const jwt = require('jsonwebtoken');
// 密钥从环境变量读取,生产环境使用 32 字节以上的随机字符串
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-must-change-in-production';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '24h';
// 生成 token:将用户信息编码进去(不要放密码等敏感字段)
const generateToken = (payload) =>
jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
// 认证中间件:验证 Authorization: Bearer <token>
const authenticateToken = (req, res, next) => {
const authHeader = req.headers.authorization;
// 提取 Bearer 后面的 token 字符串
const token = authHeader?.startsWith('Bearer ')
? authHeader.slice(7)
: null;
if (!token) {
return res.status(401).json({
success: false,
error: '未提供认证令牌,请先登录'
});
}
try {
// verify 同时验证签名合法性和 expiresIn 过期时间
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded; // 将解码结果挂到 req.user,后续路由直接使用
next();
} catch (err) {
// TokenExpiredError:token 已过期;JsonWebTokenError:token 被篡改或无效
const message = err.name === 'TokenExpiredError'
? '令牌已过期,请重新登录'
: '无效的令牌';
res.status(401).json({ success: false, error: message });
}
};
// 角色权限中间件工厂(必须在 authenticateToken 之后使用)
// 用法:requireRole('admin') 或 requireRole('admin', 'editor')
const requireRole = (...roles) => (req, res, next) => {
if (!roles.includes(req.user?.role)) {
return res.status(403).json({
error: `权限不足,需要 [${roles.join(' / ')}] 角色`
});
}
next();
};
module.exports = { generateToken, authenticateToken, requireRole };
登录注册路由
javascript
// routes/auth.js
const express = require('express');
const bcrypt = require('bcryptjs');
const { generateToken, authenticateToken } = require('../middlewares/auth');
const router = express.Router();
// 模拟用户数据库(生产项目替换为 MySQL/MongoDB 查询)
const users = [];
// POST /auth/register - 注册
router.post('/register', async (req, res) => {
const { username, password, email } = req.body;
if (!username || !password || !email) {
return res.status(400).json({ error: '用户名、密码和邮箱不能为空' });
}
if (users.find(u => u.username === username)) {
return res.status(409).json({ error: '用户名已存在' });
}
// bcrypt saltRounds=10 是安全与性能的平衡点(约 100ms)
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = {
id: Date.now(),
username,
email,
password: hashedPassword, // 数据库只存哈希值,原始密码不可逆
role: 'user',
createdAt: new Date()
};
users.push(newUser);
// 注册后直接签发 token,用户无需再次登录
const token = generateToken({ id: newUser.id, username, role: newUser.role });
res.status(201).json({ success: true, token });
});
// POST /auth/login - 登录
router.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
// 统一错误信息:不透露"用户不存在"还是"密码错误",防止用户枚举攻击
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: '用户名或密码错误' });
}
const token = generateToken({ id: user.id, username: user.username, role: user.role });
res.json({
success: true,
token,
user: { id: user.id, username: user.username, role: user.role }
});
});
// GET /auth/profile - 获取当前用户信息(受保护路由)
router.get('/profile', authenticateToken, (req, res) => {
// authenticateToken 已将解码后的用户信息挂到 req.user
res.json({ success: true, user: req.user });
});
module.exports = router;
在路由中使用认证
javascript
const { authenticateToken, requireRole } = require('./middlewares/auth');
const authRouter = require('./routes/auth');
// 挂载认证路由
app.use('/auth', authRouter);
// 保护单个路由:只需在 handler 前加 authenticateToken 中间件
app.get('/api/dashboard', authenticateToken, (req, res) => {
res.json({ message: `欢迎,${req.user.username}!你的角色是 ${req.user.role}` });
});
// 管理员专属路由:先认证,再鉴权
app.delete('/api/users/:id',
authenticateToken, // 第一步:验证 token 是否合法
requireRole('admin'), // 第二步:验证是否有 admin 角色
(req, res) => {
res.json({ message: `用户 ${req.params.id} 已删除` });
}
);
// 保护整个路由组:/api/admin/* 都要求 admin 权限
app.use('/api/admin',
authenticateToken,
requireRole('admin')
);
JWT vs Session 对比
| 特性 | JWT | Session + Cookie |
|---|---|---|
| 服务器状态 | 无状态,token 自包含 | 有状态,Session 存服务器 |
| 横向扩展 | 天然支持(无共享状态) | 需 Redis 共享 Session |
| token 主动吊销 | 困难(需维护黑名单) | 容易(删除服务端 Session) |
| 存储位置 | localStorage / HttpOnly Cookie | Cookie(存 SessionID) |
| 安全风险 | XSS(localStorage) | CSRF(Cookie 自动携带) |
| 适用场景 | 微服务、移动 APP、SPA 前后端分离 | 传统服务端渲染 Web 应用 |
生产建议 :将 JWT 存储在 HttpOnly Cookie(而非 localStorage)中防 XSS,并配合 CSRF Token 防 CSRF 攻击。永远不要在 JWT Payload 中存储密码等敏感信息。
六、模块化路由设计
6.1 为什么需要模块化路由
随着应用规模增长,将所有路由定义在单个文件中会导致代码难以维护。模块化路由设计可以:
- 提高代码可读性:相关路由组织在一起
- 便于团队协作:不同开发者负责不同模块
- 简化测试:独立模块便于单元测试
- 代码复用:通用逻辑可以抽象为中间件
6.2 创建路由模块
用户路由模块
javascript
// 【代码注释】routes/users.js - 用户相关路由
const express = require('express');
const router = express.Router();
// 【代码注释】用户数据存储(模拟数据库)
const users = [
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' }
];
// 【代码注释】获取用户列表
router.get('/', (req, res) => {
res.json({
success: true,
data: users
});
});
// 【代码注释】获取单个用户
router.get('/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({
success: false,
error: '用户不存在'
});
}
res.json({
success: true,
data: user
});
});
// 【代码注释】创建用户
router.post('/', (req, res) => {
const { name, email } = req.body;
// 验证数据
if (!name || !email) {
return res.status(400).json({
success: false,
error: '姓名和邮箱不能为空'
});
}
// 创建新用户
const newUser = {
id: users.length + 1,
name,
email
};
users.push(newUser);
res.status(201).json({
success: true,
data: newUser
});
});
// 【代码注释】更新用户
router.put('/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({
success: false,
error: '用户不存在'
});
}
const { name, email } = req.body;
// 更新用户信息
user.name = name || user.name;
user.email = email || user.email;
res.json({
success: true,
data: user
});
});
// 【代码注释】删除用户
router.delete('/:id', (req, res) => {
const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));
if (userIndex === -1) {
return res.status(404).json({
success: false,
error: '用户不存在'
});
}
users.splice(userIndex, 1);
res.json({
success: true,
message: '用户已删除'
});
});
module.exports = router;
【代码注释】
express.Router()创建子路由,与app接口相同(get/post/use),最后module.exports = router供主文件挂载。router.get('/:id')挂载到app.use('/users', router)后完整路径为/users/:id。router.get('/')列表、router.post('/')创建,注意 POST 与 GET 同路径 靠 HTTP 方法区分。parseInt(req.params.id)避免字符串与数字===比较失败;404 时统一{ success: false, error: '...' }便于前端处理。- 主文件需
app.use(express.json())才能读到req.body。
文章路由模块
javascript
// 【代码注释】routes/articles.js - 文章相关路由
const express = require('express');
const router = express.Router();
// 【代码注释】文章数据
const articles = [
{ id: 1, title: 'Node.js 入门', content: '...' },
{ id: 2, title: 'Express 框架', content: '...' }
];
// 【代码注释】中间件:验证文章ID
const validateArticleId = (req, res, next) => {
const id = parseInt(req.params.id);
const article = articles.find(a => a.id === id);
if (!article) {
return res.status(404).json({
success: false,
error: '文章不存在'
});
}
req.article = article; // 将文章对象附加到请求上
next();
};
// 【代码注释】获取文章列表
router.get('/', (req, res) => {
const { page = 1, limit = 10 } = req.query;
const startIndex = (page - 1) * limit;
const endIndex = startIndex + parseInt(limit);
const paginatedArticles = articles.slice(startIndex, endIndex);
res.json({
success: true,
data: paginatedArticles,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: articles.length
}
});
});
// 【代码注释】获取单篇文章(使用验证中间件)
router.get('/:id', validateArticleId, (req, res) => {
res.json({
success: true,
data: req.article
});
});
// 【代码注释】创建文章
router.post('/', (req, res) => {
const { title, content } = req.body;
if (!title || !content) {
return res.status(400).json({
success: false,
error: '标题和内容不能为空'
});
}
const newArticle = {
id: articles.length + 1,
title,
content,
createdAt: new Date()
};
articles.push(newArticle);
res.status(201).json({
success: true,
data: newArticle
});
});
module.exports = router;
6.3 在主应用中挂载路由
javascript
// 【代码注释】app.js - 主应用文件
const express = require('express');
const path = require('path');
// 【代码注释】导入路由模块
const usersRouter = require('./routes/users');
const articlesRouter = require('./routes/articles');
const app = express();
// 【代码注释】配置中间件
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
// 【代码注释】挂载路由模块
app.use('/api/users', usersRouter);
app.use('/api/articles', articlesRouter);
// 【代码注释】根路径
app.get('/', (req, res) => {
res.json({
message: 'API 服务',
endpoints: {
users: '/api/users',
articles: '/api/articles'
}
});
});
// 【代码注释】404 处理
app.use((req, res) => {
res.status(404).json({
success: false,
error: '端点不存在'
});
});
// 【代码注释】错误处理
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
success: false,
error: '服务器错误'
});
});
app.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
});
【代码注释】
app.use('/api/users', usersRouter)将子路由挂到前缀下,模块内router.get('/')即GET /api/users。- 全局中间件(
json、static)写在use(router)之前 ,保证子路由能读到req.body。 - 根路径返回
endpoints文档化 API,便于 Postman/前端联调。 - 404 用
app.use无路径;错误处理四参数放最后;next(err)从路由传入后由此统一返回 500 JSON。
6.4 项目结构示例
project/
├── app.js # 主应用文件
├── routes/ # 路由目录
│ ├── index.js # 路由聚合
│ ├── users.js # 用户路由
│ ├── articles.js # 文章路由
│ └── auth.js # 认证路由
├── controllers/ # 控制器(可选)
│ ├── userController.js
│ └── articleController.js
├── models/ # 数据模型(可选)
│ ├── User.js
│ └── Article.js
├── middlewares/ # 自定义中间件
│ ├── auth.js
│ ├── validation.js
│ └── errorHandler.js
├── public/ # 静态资源
│ ├── css/
│ ├── js/
│ └── images/
└── views/ # 模板文件
└── index.html
6.5 路由模块的最佳实践
javascript
// 【代码注释】routes/index.js - 路由聚合
const express = require('express');
const router = express.Router();
// 【代码注释】导入子路由
const usersRouter = require('./users');
const articlesRouter = require('./articles');
const authRouter = require('./auth');
// 【代码注释】挂载子路由
router.use('/users', usersRouter);
router.use('/articles', articlesRouter);
router.use('/auth', authRouter);
// 【代码注释】路由级中间件
router.use((req, res, next) => {
console.log(`API路由访问:${req.method} ${req.url}`);
next();
});
module.exports = router;
API 版本控制
javascript
// 【代码注释】实现 API 版本控制
const express = require('express');
const app = express();
// 【代码注释】v1 版本路由
const v1Router = require('./routes/v1');
app.use('/api/v1', v1Router);
// 【代码注释】v2 版本路由
const v2Router = require('./routes/v2');
app.use('/api/v2', v2Router);
// 【代码注释】默认使用最新版本
app.use('/api', v2Router);
app.listen(3000);
七、实战案例:新闻列表应用
7.1 项目概述
构建一个功能完整的新闻列表应用,包含新闻列表展示、新闻详情查看、分类筛选等功能。
7.2 项目结构
news-app/
├── app.js # 主应用文件
├── data.json # 新闻数据
├── public/ # 静态资源
│ ├── css/
│ │ └── style.css
│ ├── js/
│ │ └── main.js
│ └── images/
└── views/ # 模板文件
├── layout.html
├── list.html
└── detail.html
7.3 数据准备
javascript
// 【代码注释】data.json - 新闻数据
[
{
"id": "1",
"category": "科技",
"title": "Node.js 20 版本发布:带来重大性能提升",
"summary": "最新的 Node.js 20 版本引入了许多新特性和性能优化...",
"content": "详细的新闻内容...",
"author": "技术编辑部",
"publishTime": "2024-01-15 08:30:00",
"views": 1234,
"tags": ["Node.js", "JavaScript", "后端开发"]
},
{
"id": "2",
"category": "前端开发",
"title": "Express 框架最佳实践指南",
"summary": "了解如何使用 Express 框架构建高性能的 Web 应用...",
"content": "详细的新闻内容...",
"author": "前端专家组",
"publishTime": "2024-01-14 14:20:00",
"views": 892,
"tags": ["Express", "Node.js", "Web开发"]
},
{
"id": "3",
"category": "人工智能",
"title": "AI 技术在 Web 开发中的应用趋势",
"summary": "探索人工智能如何改变 Web 开发的未来...",
"content": "详细的新闻内容...",
"author": "AI研究组",
"publishTime": "2024-01-13 10:15:00",
"views": 2341,
"tags": ["AI", "机器学习", "Web开发"]
}
]
7.4 应用实现
javascript
// 【代码注释】app.js - 新闻应用主文件
const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();
const PORT = 3000;
// 【代码注释】加载数据
const newsData = JSON.parse(
fs.readFileSync(path.join(__dirname, 'data.json'), 'utf-8')
);
// 【代码注释】配置静态资源
app.use(express.static(path.join(__dirname, 'public')));
// 【代码注释】模板引擎配置
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'html');
app.engine('html', require('ejs').renderFile);
// 【代码注释】根路径重定向
app.get('/', (req, res) => {
res.redirect('/news');
});
// 【代码注释】新闻列表页
app.get('/news', (req, res) => {
// 【代码注释】获取分类和分页参数
const { category, page = 1, limit = 10 } = req.query;
// 【代码注释】过滤新闻
let filteredNews = newsData;
if (category) {
filteredNews = newsData.filter(news => news.category === category);
}
// 【代码注释】分页计算
const startIndex = (page - 1) * limit;
const endIndex = startIndex + parseInt(limit);
const paginatedNews = filteredNews.slice(startIndex, endIndex);
// 【代码注释】获取所有分类
const categories = [...new Set(newsData.map(news => news.category))];
// 【代码注释】渲染模板
res.render('list', {
news: paginatedNews,
categories,
currentCategory: category,
currentPage: parseInt(page),
totalPages: Math.ceil(filteredNews.length / limit),
totalNews: filteredNews.length
});
});
// 【代码注释】新闻详情页
app.get('/news/:id', (req, res) => {
const newsId = req.params.id;
// 【代码注释】查找新闻
const newsItem = newsData.find(news => news.id === newsId);
if (!newsItem) {
return res.status(404).render('error', {
message: '新闻不存在'
});
}
// 【代码注释】增加浏览量(实际应用中应该持久化)
newsItem.views += 1;
// 【代码注释】获取相关新闻
const relatedNews = newsData
.filter(news =>
news.id !== newsId &&
news.category === newsItem.category
)
.slice(0, 3);
res.render('detail', {
news: newsItem,
relatedNews
});
});
// 【代码注释】分类页面
app.get('/category/:name', (req, res) => {
const categoryName = req.params.name;
const categoryNews = newsData.filter(
news => news.category === categoryName
);
res.render('list', {
news: categoryNews,
currentCategory: categoryName,
categories: [...new Set(newsData.map(news => news.category))],
currentPage: 1,
totalPages: 1,
totalNews: categoryNews.length
});
});
// 【代码注释】搜索功能
app.get('/search', (req, res) => {
const { keyword } = req.query;
if (!keyword) {
return res.redirect('/news');
}
const searchResults = newsData.filter(news =>
news.title.includes(keyword) ||
news.content.includes(keyword) ||
news.tags.some(tag => tag.includes(keyword))
);
res.render('list', {
news: searchResults,
keyword,
categories: [...new Set(newsData.map(news => news.category))],
currentPage: 1,
totalPages: 1,
totalNews: searchResults.length
});
});
// 【代码注释】404 错误处理
app.use((req, res) => {
res.status(404).render('error', {
message: '页面不存在'
});
});
// 【代码注释】启动服务器
app.listen(PORT, () => {
console.log(`新闻应用运行在 http://localhost:${PORT}`);
});
【代码注释】
fs.readFileSync+JSON.parse课堂用内存数据;生产应数据库或异步readFile避免阻塞。app.engine('html', require('ejs').renderFile):视图文件扩展名为.html,语法仍是 EJS<% %>。- 列表页
req.query驱动 分类筛选 (category)与 分页 (page、limit);slice模拟分页,真实项目用 SQLLIMIT/OFFSET。 - 详情
req.params.id与find匹配;不存在时res.status(404).render('error')并return,防止继续执行。 newsItem.views += 1仅改内存,重启丢失;持久化需写库。- 搜索
GET /search?keyword=用includes做简单匹配;404 中间件放所有路由最后。 - 课堂精简版用
?id=查询串见 §10.5;完整版用/news/:id路径参数。
7.5 前端模板实现
html
<!-- 【代码注释】views/list.html - 新闻列表模板 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>新闻列表 - 新闻应用</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header class="header">
<div class="container">
<h1>新闻中心</h1>
<nav class="nav">
<a href="/news">首页</a>
<% categories.forEach(category => { %>
<a href="/category/<%= category %>"><%= category %></a>
<% }); %>
</nav>
</div>
</header>
<main class="main">
<div class="container">
<!-- 搜索框 -->
<div class="search-box">
<form action="/search" method="GET">
<input type="text" name="keyword" placeholder="搜索新闻..."
value="<%= keyword || '' %>">
<button type="submit">搜索</button>
</form>
</div>
<!-- 新闻列表 -->
<div class="news-list">
<% news.forEach(item => { %>
<article class="news-item">
<div class="news-meta">
<span class="category"><%= item.category %></span>
<span class="time"><%= item.publishTime %></span>
</div>
<h2 class="news-title">
<a href="/news/<%= item.id %>"><%= item.title %></a>
</h2>
<p class="news-summary"><%= item.summary %></p>
<div class="news-footer">
<span class="author">作者:<%= item.author %></span>
<span class="views">阅读:<%= item.views %></span>
<div class="tags">
<% item.tags.forEach(tag => { %>
<span class="tag"><%= tag %></span>
<% }); %>
</div>
</div>
</article>
<% }); %>
</div>
<!-- 分页 -->
<% if (totalPages > 1) { %>
<div class="pagination">
<% for (let i = 1; i <= totalPages; i++) { %>
<a href="/news?page=<%= i %><%= currentCategory ? '&category=' + currentCategory : '' %>"
class="<%= i === currentPage ? 'active' : '' %>">
<%= i %>
</a>
<% }; %>
</div>
<% }; %>
</div>
</main>
<footer class="footer">
<div class="container">
<p>© 2024 新闻应用. All rights reserved.</p>
</div>
</footer>
</body>
</html>
html
<!-- 【代码注释】views/detail.html - 新闻详情模板 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= news.title %> - 新闻应用</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header class="header">
<div class="container">
<h1><a href="/news">新闻中心</a></h1>
</div>
</header>
<main class="main">
<div class="container">
<article class="news-detail">
<div class="detail-header">
<h1 class="detail-title"><%= news.title %></h1>
<div class="detail-meta">
<span class="category"><%= news.category %></span>
<span class="author">作者:<%= news.author %></span>
<span class="time"><%= news.publishTime %></span>
<span class="views">阅读:<%= news.views %></span>
</div>
</div>
<div class="detail-content">
<%= news.content %>
</div>
<div class="detail-tags">
<% news.tags.forEach(tag => { %>
<span class="tag"><%= tag %></span>
<% }); %>
</div>
</article>
<!-- 相关新闻 -->
<% if (relatedNews.length > 0) { %>
<aside class="related-news">
<h3>相关新闻</h3>
<ul>
<% relatedNews.forEach(item => { %>
<li>
<a href="/news/<%= item.id %>"><%= item.title %></a>
<span class="views"><%= item.views %> 阅读</span>
</li>
<% }); %>
</ul>
</aside>
<% }; %>
</div>
</main>
</body>
</html>
7.6 应用功能流程图
#mermaid-svg-DkEqYeZ1eItBSich{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DkEqYeZ1eItBSich .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DkEqYeZ1eItBSich .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DkEqYeZ1eItBSich .error-icon{fill:#552222;}#mermaid-svg-DkEqYeZ1eItBSich .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DkEqYeZ1eItBSich .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DkEqYeZ1eItBSich .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DkEqYeZ1eItBSich .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DkEqYeZ1eItBSich .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DkEqYeZ1eItBSich .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DkEqYeZ1eItBSich .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DkEqYeZ1eItBSich .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DkEqYeZ1eItBSich .marker.cross{stroke:#333333;}#mermaid-svg-DkEqYeZ1eItBSich svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DkEqYeZ1eItBSich p{margin:0;}#mermaid-svg-DkEqYeZ1eItBSich .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DkEqYeZ1eItBSich .cluster-label text{fill:#333;}#mermaid-svg-DkEqYeZ1eItBSich .cluster-label span{color:#333;}#mermaid-svg-DkEqYeZ1eItBSich .cluster-label span p{background-color:transparent;}#mermaid-svg-DkEqYeZ1eItBSich .label text,#mermaid-svg-DkEqYeZ1eItBSich span{fill:#333;color:#333;}#mermaid-svg-DkEqYeZ1eItBSich .node rect,#mermaid-svg-DkEqYeZ1eItBSich .node circle,#mermaid-svg-DkEqYeZ1eItBSich .node ellipse,#mermaid-svg-DkEqYeZ1eItBSich .node polygon,#mermaid-svg-DkEqYeZ1eItBSich .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DkEqYeZ1eItBSich .rough-node .label text,#mermaid-svg-DkEqYeZ1eItBSich .node .label text,#mermaid-svg-DkEqYeZ1eItBSich .image-shape .label,#mermaid-svg-DkEqYeZ1eItBSich .icon-shape .label{text-anchor:middle;}#mermaid-svg-DkEqYeZ1eItBSich .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DkEqYeZ1eItBSich .rough-node .label,#mermaid-svg-DkEqYeZ1eItBSich .node .label,#mermaid-svg-DkEqYeZ1eItBSich .image-shape .label,#mermaid-svg-DkEqYeZ1eItBSich .icon-shape .label{text-align:center;}#mermaid-svg-DkEqYeZ1eItBSich .node.clickable{cursor:pointer;}#mermaid-svg-DkEqYeZ1eItBSich .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DkEqYeZ1eItBSich .arrowheadPath{fill:#333333;}#mermaid-svg-DkEqYeZ1eItBSich .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DkEqYeZ1eItBSich .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DkEqYeZ1eItBSich .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DkEqYeZ1eItBSich .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DkEqYeZ1eItBSich .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DkEqYeZ1eItBSich .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DkEqYeZ1eItBSich .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DkEqYeZ1eItBSich .cluster text{fill:#333;}#mermaid-svg-DkEqYeZ1eItBSich .cluster span{color:#333;}#mermaid-svg-DkEqYeZ1eItBSich div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DkEqYeZ1eItBSich .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DkEqYeZ1eItBSich rect.text{fill:none;stroke-width:0;}#mermaid-svg-DkEqYeZ1eItBSich .icon-shape,#mermaid-svg-DkEqYeZ1eItBSich .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DkEqYeZ1eItBSich .icon-shape p,#mermaid-svg-DkEqYeZ1eItBSich .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DkEqYeZ1eItBSich .icon-shape .label rect,#mermaid-svg-DkEqYeZ1eItBSich .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DkEqYeZ1eItBSich .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DkEqYeZ1eItBSich .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DkEqYeZ1eItBSich :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 首页
分类页
详情页
搜索
用户访问
访问类型
显示所有新闻
显示分类新闻
显示新闻详情
显示搜索结果
应用分类筛选
应用分页
增加阅读量
显示相关新闻
渲染列表页
渲染详情页
7.7 性能优化建议
- 数据缓存:使用内存缓存或 Redis 缓存新闻数据
- 分页优化:对于大数据集,使用数据库分页查询
- 静态资源压缩:启用 gzip 压缩
- CDN 加速:将静态资源托管到 CDN
- 图片优化:使用合适的图片格式和尺寸
八、性能优化与最佳实践
8.1 性能优化策略
启用压缩
javascript
const compression = require('compression');
// 【代码注释】启用 gzip 压缩
app.use(compression());
// 【代码注释】配置压缩选项
app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
},
threshold: 1024, // 只压缩大于 1KB 的响应
level: 6 // 压缩级别 (0-9)
}));
静态资源优化
javascript
// 【代码注释】设置静态缓存头
const staticOptions = {
maxAge: '1d', // 缓存一天
etag: true, // 启用 ETag
lastModified: true, // 使用 Last-Modified
setHeaders: (res, filePath) => {
// 根据文件类型设置不同的缓存策略
if (filePath.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache');
} else if (filePath.match(/\.(js|css)$/)) {
res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1年
}
}
};
app.use(express.static('public', staticOptions));
连接池配置
javascript
// 【代码注释】优化 HTTP 连接
const http = require('http');
const https = require('https');
// 【代码注释】增加连接池大小
http.globalAgent.maxSockets = 100;
https.globalAgent.maxSockets = 100;
// 【代码注释】设置连接超时
http.globalAgent.keepAlive = true;
http.globalAgent.keepAliveMsecs = 1000;
http.globalAgent.timeout = 60000;
8.2 安全最佳实践
使用 Helmet
javascript
const helmet = require('helmet');
// 【代码注释】应用 Helmet 安全中间件
app.use(helmet());
// 【代码注释】配置 Content Security Policy
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
},
}));
// 【代码注释】配置 HTTP Strict Transport Security
app.use(helmet.hsts({
maxAge: 31536000,
includeSubDomains: true,
preload: true
}));
输入验证
javascript
const { body, validationResult } = require('express-validator');
// 【代码注释】验证中间件
const validateUser = [
body('name')
.notEmpty().withMessage('姓名不能为空')
.isLength({ min: 2, max: 50 }).withMessage('姓名长度为2-50字符')
.trim(),
body('email')
.isEmail().withMessage('邮箱格式不正确')
.normalizeEmail(),
body('age')
.optional()
.isInt({ min: 1, max: 120 }).withMessage('年龄范围1-120')
];
// 【代码注释】在路由中使用验证
app.post('/users', validateUser, (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 处理有效的数据
res.json({ message: '用户创建成功' });
});
8.3 错误处理模式
javascript
// 【代码注释】统一错误处理中间件
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// 【代码注释】异步错误包装器
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// 【代码注释】使用示例
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new AppError('用户不存在', 404);
}
res.json(user);
}));
// 【代码注释】错误处理中间件
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
res.status(err.statusCode).json({
status: err.status,
message: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});
8.4 日志记录
javascript
const morgan = require('morgan');
const fs = require('fs');
const path = require('path');
// 【代码注释】创建日志文件写入流
const accessLogStream = fs.createWriteStream(
path.join(__dirname, 'access.log'),
{ flags: 'a' }
);
// 【代码注释】自定义日志格式
morgan.token('user-id', (req) => req.user?.id || 'anonymous');
const customFormat = ':user-id [:date[clf]] ":method :url" :status :res[content-length]';
// 【代码注释】应用日志中间件
if (process.env.NODE_ENV === 'production') {
app.use(morgan('combined', { stream: accessLogStream }));
} else {
app.use(morgan('dev'));
}
// 【代码注释】自定义错误日志
const logError = (err) => {
const logMessage = {
timestamp: new Date().toISOString(),
error: {
message: err.message,
stack: err.stack,
statusCode: err.statusCode
},
request: {
method: err.req?.method,
url: err.req?.url,
headers: err.req?.headers
}
};
fs.appendFileSync(
path.join(__dirname, 'error.log'),
JSON.stringify(logMessage) + '\n'
);
};
8.5 环境配置
javascript
// 【代码注释】使用 dotenv 管理环境变量
require('dotenv').config();
const app = express();
// 【代码注释】环境变量验证
const requiredEnvVars = ['PORT', 'DATABASE_URL', 'JWT_SECRET'];
requiredEnvVars.forEach(envVar => {
if (!process.env[envVar]) {
throw new Error(`缺少必需的环境变量: ${envVar}`);
}
});
// 【代码注释】根据环境加载配置
const config = {
development: {
port: process.env.PORT || 3000,
database: process.env.DEV_DATABASE_URL,
logging: true
},
production: {
port: process.env.PORT || 80,
database: process.env.DATABASE_URL,
logging: false
},
test: {
port: process.env.TEST_PORT || 3001,
database: process.env.TEST_DATABASE_URL,
logging: false
}
};
const envConfig = config[process.env.NODE_ENV || 'development'];
// 【代码注释】应用配置
app.set('port', envConfig.port);
app.set('database', envConfig.database);
module.exports = { app, envConfig };
8.6 数据库集成模式
Express 本身不绑定任何数据库,通过驱动库与数据库通信。以下给出两种最常见方案的生产级写法。
MySQL 集成(mysql2 + 连接池)
javascript
// 安装:npm install mysql2
// db/mysql.js ------ 统一管理连接池,避免每次请求新建连接
const mysql = require('mysql2/promise');
// createPool 而非 createConnection:连接池自动复用,并发安全
const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || 'myapp',
waitForConnections: true,
connectionLimit: 10, // 最大同时保持 10 条连接
queueLimit: 0 // 等待队列无限长(0 = 不限)
});
// 封装查询方法:pool.execute 自动参数化,防 SQL 注入
const query = async (sql, params = []) => {
const [rows] = await pool.execute(sql, params);
return rows;
};
module.exports = { pool, query };
javascript
// 在 Express 路由中使用
const { query } = require('./db/mysql');
const asyncHandler = require('./utils/asyncHandler');
// 获取用户列表(永远使用参数化查询,杜绝 SQL 注入)
app.get('/users', asyncHandler(async (req, res) => {
// 正确:参数化查询,? 占位符
const users = await query(
'SELECT id, name, email FROM users WHERE active = ? ORDER BY created_at DESC LIMIT ?',
[1, 20]
);
res.json({ success: true, data: users });
}));
// 创建用户
app.post('/users', asyncHandler(async (req, res) => {
const { name, email } = req.body;
try {
const result = await query(
'INSERT INTO users (name, email, created_at) VALUES (?, ?, NOW())',
[name, email]
);
res.status(201).json({
success: true,
data: { id: result.insertId, name, email }
});
} catch (err) {
// ER_DUP_ENTRY:唯一键冲突(如邮箱重复)
if (err.code === 'ER_DUP_ENTRY') {
return res.status(409).json({ error: '邮箱已被注册' });
}
throw err; // 其他错误继续向上抛给全局错误处理
}
}));
MongoDB 集成(Mongoose)
javascript
// 安装:npm install mongoose
// db/mongo.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGO_URI, {
serverSelectionTimeoutMS: 5000 // 5秒内无法连接则报错
});
console.log('MongoDB 连接成功');
} catch (err) {
console.error('MongoDB 连接失败:', err.message);
process.exit(1); // 数据库不可用则终止进程,避免服务以降级状态运行
}
};
module.exports = connectDB;
javascript
// models/Article.js ------ Schema 定义数据结构和约束
const mongoose = require('mongoose');
const articleSchema = new mongoose.Schema({
title: {
type: String,
required: [true, '标题不能为空'],
maxlength: [200, '标题不超过200字']
},
content: { type: String, required: true },
category: { type: String, enum: ['科技', '财经', '体育', '娱乐'], index: true },
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, // 关联 User 文档
views: { type: Number, default: 0 },
tags: [String]
}, { timestamps: true }); // 自动添加 createdAt / updatedAt 字段
// 复合索引:按分类 + 时间倒序查询时命中
articleSchema.index({ category: 1, createdAt: -1 });
// 全文索引:支持 $text 搜索
articleSchema.index({ title: 'text', content: 'text' });
module.exports = mongoose.model('Article', articleSchema);
javascript
// routes/articles.js ------ 使用 Mongoose Model 进行 CRUD
const Article = require('../models/Article');
const asyncHandler = require('../utils/asyncHandler');
const router = express.Router();
// GET /articles?page=1&limit=10&category=科技
// 分页 + 分类筛选,不返回正文(节省带宽),联表填充作者信息
router.get('/', asyncHandler(async (req, res) => {
const { page = 1, limit = 10, category } = req.query;
const filter = category ? { category } : {};
// Promise.all 并发执行两个查询,而非串行等待
const [articles, total] = await Promise.all([
Article.find(filter)
.sort({ createdAt: -1 }) // 最新文章排前
.skip((page - 1) * limit)
.limit(+limit)
.select('-content') // 列表页排除正文字段
.populate('author', 'name avatar'), // 填充作者名和头像
Article.countDocuments(filter)
]);
res.json({
success: true,
data: articles,
pagination: { page: +page, limit: +limit, total, pages: Math.ceil(total / limit) }
});
}));
// GET /articles/search?keyword=Node.js ------ 利用全文索引搜索
router.get('/search', asyncHandler(async (req, res) => {
const { keyword } = req.query;
if (!keyword?.trim()) return res.json({ success: true, data: [] });
const results = await Article
.find(
{ $text: { $search: keyword } }, // 使用全文索引
{ score: { $meta: 'textScore' } } // 计算相关性分数
)
.sort({ score: { $meta: 'textScore' } }) // 按相关性降序
.limit(20)
.select('title category author createdAt views');
res.json({ success: true, data: results });
}));
// POST /articles ------ 创建文章(需要登录)
router.post('/', authenticateToken, asyncHandler(async (req, res) => {
const article = await Article.create({
...req.body,
author: req.user.id // 从 JWT 中获取当前用户 ID
});
res.status(201).json({ success: true, data: article });
}));
module.exports = router;
asyncHandler 错误包装器(必备工具)
javascript
// utils/asyncHandler.js
// 消除每个 async 路由都必须写 try/catch 的样板代码
// Promise 内部的 throw 或 reject 会被 .catch(next) 捕获并传给全局错误处理中间件
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
module.exports = asyncHandler;
// 使用前:每个路由手写 try/catch,噪音多
app.get('/users', async (req, res, next) => {
try {
const users = await User.find();
res.json(users);
} catch (err) {
next(err);
}
});
// 使用后:只关注业务逻辑,异常自动转发
app.get('/users', asyncHandler(async (req, res) => {
const users = await User.find();
res.json(users);
}));
8.7 优雅关闭(Graceful Shutdown)
生产环境中,服务重启或容器销毁时不应强行中断正在处理的请求,需要实现优雅关闭。
javascript
const server = app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
// 监听进程终止信号(Docker stop、Kubernetes pod 停止等)
const shutdown = (signal) => {
console.log(`\n收到 ${signal},开始优雅关闭...`);
// server.close 停止接受新连接,等待现有连接处理完毕
server.close(() => {
console.log('HTTP 服务器已关闭');
// 关闭数据库连接
mongoose.connection.close(false, () => {
console.log('数据库连接已关闭');
process.exit(0);
});
});
// 超时强制退出,防止连接一直挂着
setTimeout(() => {
console.error('关闭超时,强制退出');
process.exit(1);
}, 10000);
};
process.on('SIGTERM', () => shutdown('SIGTERM')); // Kubernetes / Docker 发送
process.on('SIGINT', () => shutdown('SIGINT')); // Ctrl+C 发送
九、总结与进阶
9.1 核心知识点总结
HTTP 服务基础
- HTTP 协议:理解请求/响应循环、状态码、请求方法
- GET vs POST:掌握不同请求方法的适用场景
- 静态资源服务:实现文件托管和 MIME 类型设置
Express 框架
- 路由系统:精确匹配、模糊匹配、动态参数
- 中间件机制:应用级、路由级、错误处理中间件
- 请求处理:获取查询参数、路由参数、请求体
- 响应设置:状态码、响应头、重定向、文件下载
项目结构
- 模块化设计:路由模块、控制器、中间件分离
- 错误处理:统一错误处理机制
- 安全加固:Helmet、CORS、输入验证
9.2 学习路径建议
#mermaid-svg-ckFG3364eDc1jkFK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ckFG3364eDc1jkFK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ckFG3364eDc1jkFK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ckFG3364eDc1jkFK .error-icon{fill:#552222;}#mermaid-svg-ckFG3364eDc1jkFK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ckFG3364eDc1jkFK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ckFG3364eDc1jkFK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ckFG3364eDc1jkFK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ckFG3364eDc1jkFK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ckFG3364eDc1jkFK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ckFG3364eDc1jkFK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ckFG3364eDc1jkFK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ckFG3364eDc1jkFK .marker.cross{stroke:#333333;}#mermaid-svg-ckFG3364eDc1jkFK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ckFG3364eDc1jkFK p{margin:0;}#mermaid-svg-ckFG3364eDc1jkFK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ckFG3364eDc1jkFK .cluster-label text{fill:#333;}#mermaid-svg-ckFG3364eDc1jkFK .cluster-label span{color:#333;}#mermaid-svg-ckFG3364eDc1jkFK .cluster-label span p{background-color:transparent;}#mermaid-svg-ckFG3364eDc1jkFK .label text,#mermaid-svg-ckFG3364eDc1jkFK span{fill:#333;color:#333;}#mermaid-svg-ckFG3364eDc1jkFK .node rect,#mermaid-svg-ckFG3364eDc1jkFK .node circle,#mermaid-svg-ckFG3364eDc1jkFK .node ellipse,#mermaid-svg-ckFG3364eDc1jkFK .node polygon,#mermaid-svg-ckFG3364eDc1jkFK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ckFG3364eDc1jkFK .rough-node .label text,#mermaid-svg-ckFG3364eDc1jkFK .node .label text,#mermaid-svg-ckFG3364eDc1jkFK .image-shape .label,#mermaid-svg-ckFG3364eDc1jkFK .icon-shape .label{text-anchor:middle;}#mermaid-svg-ckFG3364eDc1jkFK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ckFG3364eDc1jkFK .rough-node .label,#mermaid-svg-ckFG3364eDc1jkFK .node .label,#mermaid-svg-ckFG3364eDc1jkFK .image-shape .label,#mermaid-svg-ckFG3364eDc1jkFK .icon-shape .label{text-align:center;}#mermaid-svg-ckFG3364eDc1jkFK .node.clickable{cursor:pointer;}#mermaid-svg-ckFG3364eDc1jkFK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ckFG3364eDc1jkFK .arrowheadPath{fill:#333333;}#mermaid-svg-ckFG3364eDc1jkFK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ckFG3364eDc1jkFK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ckFG3364eDc1jkFK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ckFG3364eDc1jkFK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ckFG3364eDc1jkFK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ckFG3364eDc1jkFK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ckFG3364eDc1jkFK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ckFG3364eDc1jkFK .cluster text{fill:#333;}#mermaid-svg-ckFG3364eDc1jkFK .cluster span{color:#333;}#mermaid-svg-ckFG3364eDc1jkFK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ckFG3364eDc1jkFK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ckFG3364eDc1jkFK rect.text{fill:none;stroke-width:0;}#mermaid-svg-ckFG3364eDc1jkFK .icon-shape,#mermaid-svg-ckFG3364eDc1jkFK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ckFG3364eDc1jkFK .icon-shape p,#mermaid-svg-ckFG3364eDc1jkFK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ckFG3364eDc1jkFK .icon-shape .label rect,#mermaid-svg-ckFG3364eDc1jkFK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ckFG3364eDc1jkFK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ckFG3364eDc1jkFK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ckFG3364eDc1jkFK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Node.js基础
Express框架
RESTful API
数据库集成
身份认证
性能优化
部署运维
9.3 进阶学习方向
-
数据库集成
- MongoDB + Mongoose
- MySQL/PostgreSQL + Sequelize
- Redis 缓存
-
身份认证与授权
- JWT (JSON Web Token)
- OAuth 2.0
- Session 管理
-
实时通信
- WebSocket
- Socket.IO
- Server-Sent Events
-
API 设计
- RESTful API 最佳实践
- GraphQL
- API 版本控制
-
测试与质量
- 单元测试 (Jest, Mocha)
- 集成测试
- API 测试
-
部署与运维
- Docker 容器化
- CI/CD 流程
- 云服务部署 (AWS, Azure, 阿里云)
9.4 常见问题与解决方案
跨域问题
javascript
const cors = require('cors');
// 【代码注释】允许所有来源
app.use(cors());
// 【代码注释】配置 CORS
app.use(cors({
origin: ['https://example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400 // 预检请求缓存24小时
}));
文件上传
javascript
const multer = require('multer');
// 【代码注释】配置存储
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix);
}
});
const upload = multer({
storage: storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
fileFilter: (req, file, cb) => {
const allowedTypes = /jpeg|jpg|png|gif/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (mimetype && extname) {
return cb(null, true);
} else {
cb(new Error('只支持图片文件'));
}
}
});
// 【代码注释】文件上传路由
app.post('/upload', upload.single('file'), (req, res) => {
res.json({
message: '文件上传成功',
file: req.file
});
});
Session 管理
javascript
const session = require('express-session');
// 【代码注释】配置 Session
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only
httpOnly: true, // 防止 XSS
maxAge: 24 * 60 * 60 * 1000 // 24小时
},
store: new RedisStore({ // 使用 Redis 存储
host: 'localhost',
port: 6379
})
}));
// 【代码注释】使用 Session
app.get('/login', (req, res) => {
req.session.userId = user.id;
req.session.userName = user.name;
res.json({ message: '登录成功' });
});
app.get('/profile', (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ error: '未登录' });
}
res.json({ userId: req.session.userId });
});
9.5 实用代码片段
URL 验证中间件
javascript
// 【代码注释】URL 参数验证
const validateUrlParams = (requiredParams) => {
return (req, res, next) => {
const missingParams = requiredParams.filter(
param => !req.params[param]
);
if (missingParams.length > 0) {
return res.status(400).json({
error: '缺少必需参数',
missing: missingParams
});
}
next();
};
};
// 【代码注释】使用验证中间件
app.get('/users/:id/:name',
validateUrlParams(['id', 'name']),
(req, res) => {
res.json({ message: '验证通过' });
}
);
响应时间中间件
javascript
// 【代码注释】响应时间计算
const responseTime = (req, res, next) => {
const startTime = Date.now();
res.on('finish', () => {
const duration = Date.now() - startTime;
console.log(`${req.method} ${req.url} - ${duration}ms`);
});
next();
};
app.use(responseTime);
请求限流
javascript
const rateLimit = require('express-rate-limit');
// 【代码注释】配置限流
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 限制100次请求
message: '请求过于频繁,请稍后再试',
standardHeaders: true,
legacyHeaders: false,
});
// 【代码注释】应用限流
app.use('/api', limiter);
9.6 项目模板
javascript
// 【代码注释】完整的项目模板结构
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const compression = require('compression');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
class ExpressApp {
constructor() {
this.app = express();
this.port = process.env.PORT || 3000;
this.setupMiddlewares();
this.setupRoutes();
this.setupErrorHandling();
}
setupMiddlewares() {
// 安全
this.app.use(helmet());
// CORS
this.app.use(cors());
// 压缩
this.app.use(compression());
// 日志
this.app.use(morgan('combined'));
// 解析
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
// 限流
this.app.use(rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
}));
}
setupRoutes() {
// 健康检查
this.app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: Date.now() });
});
// API 路由
this.app.use('/api', require('./routes'));
// 404
this.app.use((req, res) => {
res.status(404).json({ error: 'Not Found' });
});
}
setupErrorHandling() {
this.app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});
}
start() {
this.app.listen(this.port, () => {
console.log(`Server running on port ${this.port}`);
});
}
}
// 【代码注释】启动应用
if (require.main === module) {
new ExpressApp().start();
}
module.exports = ExpressApp;
十、核心案例速查与知识点归纳
10.1 课堂案例学习路线
| 序号 | 主题 | 关键文件/命令 | 端口 |
|---|---|---|---|
| ① | 原生静态托管 | http + mimes.json + decodeURI |
8080 |
| ② | Express 创建服务 | express() + app.listen |
8080 |
| ③ | 简单路由 + static | app.get + express.static |
8080 |
| ④ | 路由进阶 | 模糊匹配、params、app.route、404 |
8080 |
| ⑤ | 请求对象 | query、params、body |
8080 |
| ⑥ | 响应对象 | status、send、json、redirect |
8080 |
| ⑦ | 新闻列表 01/02 | /news、/news/details?id= |
8080 |
bash
cd 项目目录 && npm install && node index.js
10.2 GET vs POST 速查(考试常考)
| 对比项 | GET | POST |
|---|---|---|
| 用途 | 获取资源 | 提交数据 |
| 请求体 | 通常无 | 有 |
| 数据位置 | URL 查询串(可见) | 请求体(相对隐蔽) |
| 容量 | 受 URL 长度限制 | 理论更大 |
| 缓存 | 可缓存 | 默认不缓存 |
| 幂等 | 幂等 | 一般非幂等 |
【代码注释】
- GET 数据暴露在地址栏,适合幂等查询(列表、搜索);勿用 GET 提交密码或大量数据。
- POST 数据在请求体,需中间件解析;
Content-Type: application/x-www-form-urlencoded配express.urlencoded,application/json配express.json()。 - Express:
req.query对应 GET 查询串,req.body对应 POST/PUT 等请求体(解析后)。 fetch默认 GET;POST JSON 需method: 'POST'、headers: { 'Content-Type': 'application/json' }、body: JSON.stringify(data)。
10.3 Express API 速查
| 需求 | API |
|---|---|
| 静态目录 | app.use(express.static(dir)) |
| GET 路由 | app.get(path, handler) |
| 链式同路径 | app.route('/login').get().post() |
| 路径参数 | /news/:id → req.params.id |
| 查询串 | ?id=1 → req.query.id |
| JSON 体 | app.use(express.json()) |
| 表单体 | app.use(express.urlencoded({ extended: true })) |
| 发 HTML | res.send() / res.sendFile() |
| 发 JSON | res.json() |
| 404 | res.status(404).send(...) |
| 兜底路由 | app.all('*', ...) 放最后 |
10.4 中间件三句话
- 中间件是
(req, res, next) => {},可改 req/res、结束响应或调用next()。 app.use按注册顺序执行;路由回调本质也是中间件。- 错误处理中间件签名为
(err, req, res, next),必须 4 个参数。
路由处理 解析body 日志中间件 客户端 路由处理 解析body 日志中间件 客户端 #mermaid-svg-Gp37394JEAhnIEWz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Gp37394JEAhnIEWz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Gp37394JEAhnIEWz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Gp37394JEAhnIEWz .error-icon{fill:#552222;}#mermaid-svg-Gp37394JEAhnIEWz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Gp37394JEAhnIEWz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Gp37394JEAhnIEWz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Gp37394JEAhnIEWz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Gp37394JEAhnIEWz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Gp37394JEAhnIEWz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Gp37394JEAhnIEWz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Gp37394JEAhnIEWz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Gp37394JEAhnIEWz .marker.cross{stroke:#333333;}#mermaid-svg-Gp37394JEAhnIEWz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Gp37394JEAhnIEWz p{margin:0;}#mermaid-svg-Gp37394JEAhnIEWz .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Gp37394JEAhnIEWz text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Gp37394JEAhnIEWz .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Gp37394JEAhnIEWz .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Gp37394JEAhnIEWz .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Gp37394JEAhnIEWz .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Gp37394JEAhnIEWz #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Gp37394JEAhnIEWz .sequenceNumber{fill:white;}#mermaid-svg-Gp37394JEAhnIEWz #sequencenumber{fill:#333;}#mermaid-svg-Gp37394JEAhnIEWz #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Gp37394JEAhnIEWz .messageText{fill:#333;stroke:none;}#mermaid-svg-Gp37394JEAhnIEWz .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Gp37394JEAhnIEWz .labelText,#mermaid-svg-Gp37394JEAhnIEWz .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Gp37394JEAhnIEWz .loopText,#mermaid-svg-Gp37394JEAhnIEWz .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Gp37394JEAhnIEWz .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Gp37394JEAhnIEWz .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Gp37394JEAhnIEWz .noteText,#mermaid-svg-Gp37394JEAhnIEWz .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Gp37394JEAhnIEWz .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Gp37394JEAhnIEWz .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Gp37394JEAhnIEWz .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Gp37394JEAhnIEWz .actorPopupMenu{position:absolute;}#mermaid-svg-Gp37394JEAhnIEWz .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Gp37394JEAhnIEWz .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Gp37394JEAhnIEWz .actor-man circle,#mermaid-svg-Gp37394JEAhnIEWz line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Gp37394JEAhnIEWz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求 next() next() 响应
10.5 最小新闻列表(课堂精简版)
javascript
const express = require('express');
const newsData = require('./data.json');
const app = express();
app.get('/', (req, res) => res.redirect('/news'));
app.get('/news', (req, res) => {
const list = newsData
.map(item => `<li><a href="/news/details?id=${item.id}">${item.newsTitle}</a></li>`)
.join('');
res.send(`<h1>新闻列表</h1><ul>${list}</ul>`);
});
app.get('/news/details', (req, res) => {
const item = newsData.find(n => n.id === req.query.id);
if (!item) return res.status(404).send('<h1>新闻不存在</h1>');
res.send(`<h1>${item.newsTitle}</h1><p>${item.newsContent}</p>`);
});
app.listen(8080, () => console.log('http://localhost:8080'));
【代码注释】
app.get('/')重定向到/news,避免根路径空白。- 列表用模板字符串拼
<ul><li>,零依赖;完整项目用 EJS/res.render(见 §7)。 href="/news/details?id=${item.id}":查询参数 传 id;req.query.id为字符串,与data.json里id字段类型需一致(都用字符串或都转数字)。find(n => n.id === req.query.id)找不到时 404 +return,避免二次res.send报错。- 升级为
app.get('/news/:id')+req.params.id更符合 REST;列表链接改为/news/${item.id}。
10.6 可运行 HTML:调用 Express JSON API
先启动提供 JSON 的服务(示例):
javascript
const express = require('express');
const app = express();
app.get('/api/news', (req, res) => {
res.json([
{ id: '1', title: 'Express 入门' },
{ id: '2', title: '中间件机制' }
]);
});
app.listen(3000, () => console.log('API http://localhost:3000'));
浏览器打开 fetch-news.html:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>fetch 新闻 API</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 560px; margin: 2rem auto; padding: 0 1rem; }
ul { line-height: 1.8; }
.err { color: #c00; }
</style>
</head>
<body>
<h1>新闻 API 演示</h1>
<button type="button" id="load">加载 /api/news</button>
<ul id="list"></ul>
<p id="msg" class="err"></p>
<script>
document.getElementById('load').onclick = async () => {
const msg = document.getElementById('msg');
const list = document.getElementById('list');
msg.textContent = '';
list.innerHTML = '';
try {
const res = await fetch('http://localhost:3000/api/news');
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
list.innerHTML = data.map(n => '<li>' + n.title + '</li>').join('');
} catch (e) {
msg.textContent = '请求失败: ' + e.message + '(请先启动 Node 服务并处理 CORS)';
}
};
</script>
</body>
</html>
【代码注释】
fetch('http://localhost:3000/api/news')从file://或另一端口打开 HTML 会触发跨域 ;Node 需返回Access-Control-Allow-Origin或使用cors中间件。res.ok为 false 时(4xx/5xx)仍要throw或分支处理,否则res.json()可能解析错误页 HTML。- 同源方案:Express 同时
express.static托管fetch-news.html,浏览器访问http://localhost:3000/fetch-news.html则无 CORS。 - 先
node启动 API,再点按钮;错误信息提示检查服务与 CORS,便于课堂自查。
10.7 常见坑
| 现象 | 原因 | 处理 |
|---|---|---|
| CSS 不生效 | Content-Type 错误 |
检查 MIME / express.static |
req.body 为 undefined |
未挂 body 解析中间件 | app.use(express.urlencoded()) / express.json() 放路由前 |
| 路由不命中 | 顺序问题或 * 在前 |
404 兜底放最后 |
| 中文路径 404 | 未 decodeURI |
原生静态服务必做 |
| 端口占用 | EADDRINUSE | lsof -i :8080 找进程,换端口或 kill |
| async 路由错误未捕获 | async 函数 throw 未 next(err) |
包裹 asyncHandler 或 try/catch + next |
| CORS 预检(OPTIONS)失败 | 未处理 OPTIONS 方法 | 使用 cors() 中间件,它自动响应预检 |
| JWT 解析失败:invalid token | 前端未加 Bearer 前缀 |
Authorization: Bearer <token>,注意空格 |
| 静态资源路径在子路由下失效 | 虚拟前缀与 HTML 内引用路径不一致 | HTML 里用 /css/app.css(绝对路径),而非相对路径 |
| 服务重启丢失内存数据 | 数据只存在变量中 | 持久化到数据库或文件,内存仅做缓存 |
| Cannot set headers after they are sent | 路由中多次调用 res.send/json |
return res.json(...) 确保只发一次响应 |
| 上传文件大小超限 | 默认 body-parser 限制 100kb | multer 的 limits.fileSize 或 express.json({ limit: '10mb' }) |
结语
Node.js 与 Express 为构建高性能的 Web 应用提供了强大的工具和灵活的架构。通过本文的系统学习,你应该掌握了:
- HTTP 服务的基本原理和实现
- Express 框架的核心概念和使用
- 路由系统的设计和应用
- 中间件机制的理解和运用
- 模块化项目结构的组织
- 实际项目开发的最佳实践
继续深入学习,建议结合实际项目练习,探索 Express 丰富的生态系统,逐步提升你的后端开发能力。
技术栈推荐 :Node.js + Express + MongoDB/MySQL + Redis
学习资源:Express 官方文档、Node.js 最佳实践、相关开源项目
祝你学习顺利,成为一名优秀的后端工程师!