Day10_Node.js 与 Express 开发实战指南:从零到一构建专业级 Web 服务

一篇面向实战的后端博客:从 原生 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 入门 applistenexpress.static 多页面 + 静态站
路由 GET/POST、参数、模糊匹配、app.route REST 风格 URL
请求/响应 queryparamsbodyres.json 表单、API
中间件 app.usenext、错误处理 日志、鉴权、解析体
实战 新闻列表 + 详情 小型内容站原型

知识脉络(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');
});

【代码注释】

  • 四步流程pathnamepath.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
返回文件内容

真实应用场景
  1. 内容分发网络(CDN):如 Cloudflare、AWS CloudFront
  2. 静态网站托管:如 GitHub Pages、Netlify、Vercel
  3. 企业官网:产品展示、营销页面
  4. 前端应用部署:React/Vue 单页应用的构建产物
静态托管四步归纳
  1. 根据 URL 的 pathname 拼接服务器上的文件路径(常加 public 前缀)。
  2. fs.access / readFile 读取;不存在 → 404 ,读失败 → 500
  3. 中文路径需 decodeURI() 解码浏览器编码。
  4. 响应头设置 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.jsonrequire 后以扩展名为键取值,开闭原则 :新增 .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.createServerapp 内部会创建 HTTP 服务器。
  • app.use(express.static(...)) 注册中间件 :请求若匹配 public 下文件则直接返回,不再走后面路由(除非 static 未找到文件且未 next------static 找不到会 next())。
  • app.get(path, handler) 只匹配 GET + 路径;handlerres.send 自动设置 Content-Type 并发送(字符串/HTML)。
  • app.listen(PORT) 等价于原生 server.listen,默认监听 0.0.0.0process.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 里引用路径要一致。
  • optionsmaxAge 设置 Cache-Control 缓存;etag/lastModified 支持 304 协商缓存,减轻带宽。
  • 多个 app.use(express.static(...)) 按注册顺序查找,先匹配先返回。
静态资源优化策略
  1. 缓存控制:设置合适的 Cache-Control 头
  2. 文件压缩:使用 gzip/brotli 压缩
  3. CDN 加速:分发到全球节点
  4. 图片优化:使用 WebP/AVIF 格式
  5. 资源合并:减少 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;值为字符串,比较数字需 parseIntNumber()
  • 模糊路径 ? + * 是 Express 路径语法,不是正则字面量;复杂规则用 app.get(/^\/users\/\d+$/)
  • :id(\\d+) 限制只匹配数字,避免 /users/admin 被当成 id。
  • 可选参数 :year? 未传时 req.params.yearundefined,需默认值。
  • 新闻详情推荐 /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.bodyundefined
  • 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);

【代码注释】

  • 推荐注册顺序:staticjson / 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 为什么需要模块化路由

随着应用规模增长,将所有路由定义在单个文件中会导致代码难以维护。模块化路由设计可以:

  1. 提高代码可读性:相关路由组织在一起
  2. 便于团队协作:不同开发者负责不同模块
  3. 简化测试:独立模块便于单元测试
  4. 代码复用:通用逻辑可以抽象为中间件

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
  • 全局中间件(jsonstatic)写在 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)与 分页pagelimit);slice 模拟分页,真实项目用 SQL LIMIT/OFFSET
  • 详情 req.params.idfind 匹配;不存在时 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>&copy; 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 性能优化建议

  1. 数据缓存:使用内存缓存或 Redis 缓存新闻数据
  2. 分页优化:对于大数据集,使用数据库分页查询
  3. 静态资源压缩:启用 gzip 压缩
  4. CDN 加速:将静态资源托管到 CDN
  5. 图片优化:使用合适的图片格式和尺寸

八、性能优化与最佳实践

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 进阶学习方向

  1. 数据库集成

    • MongoDB + Mongoose
    • MySQL/PostgreSQL + Sequelize
    • Redis 缓存
  2. 身份认证与授权

    • JWT (JSON Web Token)
    • OAuth 2.0
    • Session 管理
  3. 实时通信

  4. API 设计

    • RESTful API 最佳实践
    • GraphQL
    • API 版本控制
  5. 测试与质量

    • 单元测试 (Jest, Mocha)
    • 集成测试
    • API 测试
  6. 部署与运维

    • 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
路由进阶 模糊匹配、paramsapp.route、404 8080
请求对象 queryparamsbody 8080
响应对象 statussendjsonredirect 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-urlencodedexpress.urlencodedapplication/jsonexpress.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/:idreq.params.id
查询串 ?id=1req.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 中间件三句话

  1. 中间件是 (req, res, next) => {} ,可改 req/res、结束响应或调用 next()
  2. app.use 按注册顺序执行;路由回调本质也是中间件。
  3. 错误处理中间件签名为 (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.jsonid 字段类型需一致(都用字符串或都转数字)。
  • 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.fileSizeexpress.json({ limit: '10mb' })

结语

Node.js 与 Express 为构建高性能的 Web 应用提供了强大的工具和灵活的架构。通过本文的系统学习,你应该掌握了:

  • HTTP 服务的基本原理和实现
  • Express 框架的核心概念和使用
  • 路由系统的设计和应用
  • 中间件机制的理解和运用
  • 模块化项目结构的组织
  • 实际项目开发的最佳实践

继续深入学习,建议结合实际项目练习,探索 Express 丰富的生态系统,逐步提升你的后端开发能力。

技术栈推荐 :Node.js + Express + MongoDB/MySQL + Redis

学习资源:Express 官方文档、Node.js 最佳实践、相关开源项目

祝你学习顺利,成为一名优秀的后端工程师!

相关推荐
问心无愧05131 小时前
ctf show web入门107
android·前端·笔记·android studio
2301_815645381 小时前
react
前端·react.js
FirstFrost --sy1 小时前
基于高并发服务器的web小游戏测试
服务器·前端·javascript·c++·python·集成测试
youyu-youyu1 小时前
oss阿里云图片链接url高清图片设置为缩略图 vue 减少加载体积流量
前端·javascript·vue.js·阿里云·云计算
独隅2 小时前
前端工程化在Chrome插件开发中的具体实践完全指南
前端·chrome
sbjdhjd2 小时前
Tomcat(下) 集群高可用实战:反向代理・负载均衡・分布式 Session
运维·前端·云原生·开源·tomcat·负载均衡·memcached
低保和光头哪个先来2 小时前
聊聊 CSS 编译和 scoped 实现
前端·css·vue.js
object not found2 小时前
Node.js fs 常用 API 整理:node:fs/promises、node:fs、fs 到底怎么用
开发语言·前端·javascript
LiuJun2Son2 小时前
Angular 快速入门:服务和依赖注入
前端·javascript·angular.js