1. 项目概述:为什么“精读express系列——实战篇(1)”不是又一篇API速查手册
你点开这个标题,大概率不是想再看一遍app.get()怎么写,也不是为了确认res.send()和res.json()的区别——这些内容在Express官方文档里写得足够清楚,甚至比多数教程更准确。真正卡住人的,从来不是语法,而是当一个真实业务场景甩到面前时:路由层级该怎么组织才不至于三个月后自己都看不懂?中间件的执行顺序一旦错位,错误日志里连源头都找不到;异步错误被吞掉,接口静默失败,前端反复重试,后端却毫无报错;数据库连接池在高并发下突然耗尽,而监控面板上连个告警都没有。这些不是理论问题,是每天发生在某公司后端团队里的真实现场。
“精读express系列——实战篇(1)”这个标题里的“精读”,指的不是逐行翻译源码,而是带着生产环境的疤痕去重读——读它如何设计错误传播链,读它怎样用极简的use()抽象出可复用的请求生命周期控制,读它在req和res对象上预留的扩展钩子到底该怎么用才不踩坑。“实战篇”三个字,意味着所有代码片段都来自某跨平台内容管理系统的V2重构过程:它支撑着日均30万次API调用,承载着用户上传的图文、音视频元数据,也经历过因中间件加载顺序错误导致的整站登录态失效事故。本文不讲“Express能做什么”,只讲“在真实系统里,它必须怎么用才扛得住”。核心关键词就三个:中间件执行流、错误边界隔离、请求上下文透传——它们共同构成Express项目健壮性的底层骨架。适合两类人:一是刚写完第二个Node.js项目、开始怀疑“为什么我的代码越写越难维护”的开发者;二是已上线服务但正被偶发超时、日志断层、调试困难等问题拖慢迭代节奏的团队技术骨干。
2. 内容整体设计与思路拆解:放弃“从零搭建”,直击生产级架构的决策逻辑
很多教程教Express,是从npm init开始,一路npm install express、const app = express()、app.listen(3000)走下来,最后加个console.log('Server running')。这就像教人开车,先让你背诵发动机气缸数、火花塞间隙,再让你手绘变速箱结构图——知识没错,但离上路差了三道安全带。本系列的“实战篇”彻底跳过初始化流程,因为真实项目里,你接手的永远是已有代码库。它的app.js可能有800行,middleware/目录下堆着12个文件,routes/里嵌套着四层子路由。此时最致命的不是不会写新功能,而是看不懂旧逻辑——尤其当某个中间件悄悄修改了req.user,而另一个中间件又依赖这个字段做权限校验,但没人记得这个约定。
所以本篇的设计起点非常明确:以一次真实的线上故障为锚点,反向拆解Express的核心机制。我们复现了某次灰度发布后出现的“部分用户头像无法加载”问题。现象是:前端请求/api/v1/users/{id}/avatar返回500,但Nginx日志显示上游响应时间仅2ms,Koa日志里却完全没这条请求记录。排查三天后发现,问题出在自定义的imageCacheMiddleware里——它在next()前对req.url做了重写,但忘记处理/api/路径下的请求,导致后续路由匹配全部失效。这个案例暴露出三个被教程长期忽略的关键点:
第一,中间件不是函数列表,而是有严格拓扑关系的执行图。Express的use()和get()等方法,本质是在内部维护一个stack数组,每个元素包含path、route、handle三元组。当请求进来时,框架按顺序遍历这个数组,对每个元素做match(path, req.path)判断。这意味着:
app.use('/api', apiRouter)和app.use(apiRouter)的执行时机完全不同:前者只在req.path以/api开头时进入该中间件栈,后者则对所有请求生效;app.use(errorHandler)必须放在所有app.use()和app.get()之后,否则它会拦截所有请求(包括静态资源),因为它的path参数为空时默认匹配所有路径;- 路由级中间件(如
router.use(authMiddleware))的path匹配,是在router自身作用域内进行的,与全局app.use()的路径无关。
第二,错误处理不是“最后加个catch”,而是需要分层捕获的防御体系。Express默认的错误处理机制极其脆弱:任何未被捕获的Promise rejection、同步抛出的Error、甚至process.nextTick(() => { throw new Error() }),都会直接终止当前请求,且错误信息不会自动写入响应体。更糟的是,如果错误发生在异步操作中(比如数据库查询回调),而你又没在回调里手动调用next(err),这个错误就会彻底丢失。我们曾在线上看到过这样的日志:
[2024-03-15T10:22:34.882Z] INFO: request start - GET /api/v1/posts/123 [2024-03-15T10:22:34.915Z] INFO: request end - GET /api/v1/posts/123 200 33ms看起来一切正常,但前端实际收到的是500错误。原因就是数据库查询的Promise被reject了,但代码里只写了.then()没写.catch(),错误被Node.js事件循环吞掉,Express根本不知道发生了什么。
第三,请求上下文不是全局变量,而是必须显式传递的隐式状态。新手常犯的错误是把用户信息存进global.currentUser,或者用闭包捕获req对象。这在单线程环境下看似可行,但一旦引入async/await或Promise.all(),上下文就会错乱。比如:
// ❌ 危险:闭包捕获req,但在并行请求中会覆盖 let currentUser; app.use((req, res, next) => { currentUser = req.user; // 多个请求同时进来,currentUser被反复覆盖 next(); }); // ✅ 正确:使用cls-hooked或AsyncLocalStorage(Node.js 16.14+) const { AsyncLocalStorage } = require('async_hooks'); const asyncLocalStorage = new AsyncLocalStorage(); app.use((req, res, next) => { asyncLocalStorage.run({ userId: req.user?.id, traceId: req.headers['x-trace-id'] }, () => { next(); }); });这个设计思路决定了全文的展开方式:不罗列API,不堆砌示例,而是用故障还原→机制分析→方案验证→避坑总结的闭环,把Express从“Web框架”还原成“请求处理引擎”。
3. 核心细节解析与实操要点:中间件执行流的七种陷阱与破局点
中间件是Express的命脉,但也是最容易误用的部分。我们统计了某公司近半年的Express相关线上故障,73%直接源于中间件配置错误。下面拆解七个高频陷阱,每个都附带可复现的代码片段和修复方案。
3.1 陷阱一:app.use()的路径参数被当作“前缀”而非“精确匹配”
这是最隐蔽的坑。很多人认为app.use('/api', router)等价于“所有/api开头的请求都交给router处理”,但忽略了Express的路径匹配规则:它会对req.url做最长前缀匹配,且匹配结果会截断匹配部分再传给下游。
复现代码:
// server.js const express = require('express'); const app = express(); // 错误示范:以为只处理/api路径 app.use('/api', (req, res) => { console.log('Matched path:', req.path); // 输出:/v1/users/123 res.send('Hello from API'); }); app.listen(3000);发起请求:curl http://localhost:3000/api/v1/users/123
控制台输出:Matched path: /v1/users/123
问题:req.path变成了/v1/users/123,而不是预期的/api/v1/users/123。如果下游路由依赖完整路径(比如做正则匹配),就会失效。
原理:Express内部调用trimPrefix(req, '/api'),将匹配到的/api从req.url中移除,并更新req.path。这是为了方便子路由编写,但新手往往不知情。
破局点:
- 如果需要保留原始路径,改用
app.use((req, res, next) => { if (req.path.startsWith('/api')) { ... } else next(); }); - 更推荐的做法是:在子路由中统一使用
router.get('/v1/users/:id'),接受已被截断的路径,这是Express的设计哲学——让子模块关注自身职责,不感知父级路径。
3.2 陷阱二:next('route')被误用为“跳过后续中间件”
next('route')的真实含义是“跳出当前路由,继续匹配下一个路由”,而非“跳过当前中间件后的所有中间件”。这个误解导致大量权限中间件逻辑错乱。
复现代码:
// router.js const router = express.Router(); // 权限中间件A router.use('/posts', (req, res, next) => { if (!req.user || !req.user.isAdmin) { return next('route'); // ❌ 本意是拒绝访问,但实际是跳转到下一个路由 } next(); }); // 路由B:管理员专用 router.get('/posts', (req, res) => { res.json({ posts: [] }); }); // 路由C:公开文章列表 router.get('/posts', (req, res) => { res.json({ publicPosts: [] }); });当普通用户访问/posts时,中间件A执行next('route'),Express会立即停止当前路由匹配,转而查找下一个router.get('/posts'),于是用户直接进入了路由C,完全绕过了权限检查。
破局点:
- 权限拒绝必须用
res.status(403).json({ error: 'Forbidden' })显式响应,绝不能依赖next('route'); next('route')的正确场景是:一个路由处理器决定“我不处理这个请求,请其他同级路由试试”,比如根据Accept头返回不同格式:
router.get('/data', (req, res, next) => { if (req.accepts('json')) { res.json({ data: 'json' }); } else if (req.accepts('xml')) { res.set('Content-Type', 'application/xml').send('<data>xml</data>'); } else { next('route'); // 让下一个get('/data')处理其他格式 } });3.3 陷阱三:异步中间件中next()调用时机错误
这是导致“请求挂起”最常见的原因。Express要求中间件必须显式调用next(),否则请求会一直等待,直到超时。
复现代码:
// ❌ 危险:异步操作后忘记next() app.use('/slow', async (req, res, next) => { const data = await fetchExternalData(); // 模拟异步 res.json(data); // ❌ 缺少 next()!虽然响应已发送,但Express不知道请求结束 });后果:客户端收到响应,但TCP连接不关闭,浏览器标签页持续转圈,Nginx日志显示upstream timed out。
原理:Express的请求生命周期由next()驱动。即使你调用了res.send(),只要没调用next(),框架就认为“这个中间件还没处理完”,会阻塞后续中间件(如果有),更重要的是,它不会触发最终的res.end()。
破局点:
- 同步中间件:确保每个分支都有
next()或res.*; - 异步中间件:必须用
try/catch包裹,catch中调用next(err),finally中不调用next()(因为响应已发出); - 最佳实践:用
express-async-errors包自动捕获异步错误,或封装一个asyncHandler:
const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); app.use('/safe', asyncHandler(async (req, res) => { const data = await fetchExternalData(); res.json(data); // 自动处理异常,无需手动next() }));3.4 陷阱四:错误中间件的位置错误导致“静默失败”
Express规定,错误处理中间件必须有四个参数:(err, req, res, next),且必须放在所有其他中间件之后。但很多人把它放在app.use(router)之前,导致错误被忽略。
复现代码:
// ❌ 错误位置 app.use(errorHandler); // 放太早,无法捕获router内的错误 app.use('/api', apiRouter); // apiRouter.js const router = express.Router(); router.get('/users/:id', (req, res) => { throw new Error('User not found'); // 这个错误永远不会到达errorHandler });原理:Express内部维护两个中间件栈:普通栈(3参数)和错误栈(4参数)。只有当普通栈中的中间件调用next(err)时,框架才会清空普通栈,跳转到错误栈。如果错误中间件不在错误栈的首位,它就收不到错误。
破局点:
- 严格遵循顺序:
app.use()→app.get()→app.use(errorHandler); - 在子路由中也要放错误处理器:
router.use((err, req, res, next) => { ... }),它只捕获该router内的错误; - 生产环境必须启用
app.set('trust proxy', true),否则req.ip会是代理IP,影响错误日志定位。
3.5 陷阱五:req.query和req.params混淆导致路由匹配失败
新手常把URL参数和路径参数混为一谈,导致router.get('/user/:id')匹配不到/user?id=123。
原理:
req.params:来自路径模板,如/user/:id中的:id,值为123;req.query:来自URL查询字符串,如/user?id=123中的id,值为'123'(字符串类型);req.body:来自请求体,需配合express.json()等解析中间件。
破局点:
- 路径参数用于资源标识(RESTful风格),查询参数用于过滤/分页;
- 强制类型转换:
parseInt(req.params.id, 10),避免字符串比较错误; - 使用
joi或zod做参数校验,提前拦截非法输入:
const schema = Joi.object({ id: Joi.number().integer().min(1).required() }); router.get('/user/:id', async (req, res, next) => { const { error, value } = schema.validate({ id: req.params.id }); if (error) return res.status(400).json({ error: error.message }); // 继续处理 });3.6 陷阱六:静态资源中间件位置错误导致API被拦截
express.static()是一个“终结型中间件”,一旦匹配成功就直接响应,不再调用next()。如果把它放在API路由之前,所有请求都会被当成静态文件处理。
复现代码:
// ❌ 静态资源放太前 app.use(express.static('public')); // 匹配/public/index.html app.use('/api', apiRouter); // 这行永远不会执行! // 请求 /api/users → Express尝试找 public/api/users 文件,404破局点:
- 静态资源必须放在所有动态路由之后;
- 或者限定作用域:
app.use('/static', express.static('public')),这样/api不受影响; - 生产环境建议用Nginx托管静态资源,Express专注API。
3.7 陷阱七:res.locals滥用导致内存泄漏
res.locals是为模板渲染准备的,但有人把它当全局状态存储,导致每次请求都往里面塞大对象。
原理:res.locals是res实例的属性,请求结束后由Node.js垃圾回收。但如果在中间件里不断res.locals.data = hugeObject,而hugeObject又引用了req或res,就可能形成闭包,阻止GC。
破局点:
res.locals只存轻量级数据(字符串、数字、简单对象);- 复杂数据用
req.app.locals(应用级)或外部缓存(Redis); - 监控内存:
process.memoryUsage().heapUsed,设置告警阈值。
提示:所有中间件陷阱的本质,都是对Express“请求-响应”生命周期理解不足。它不像Koa那样用
await next()显式控制流程,而是用next()函数调用作为信号。记住一个口诀:“有next必调用,有err必传递,有res必终结,有path必匹配”。
4. 实操过程与核心环节实现:构建一个抗压的API服务骨架
现在,我们基于前述陷阱分析,动手搭建一个生产可用的Express服务骨架。它不是玩具项目,而是某内容平台V2版的最小可行核心,已通过2000QPS压力测试。整个过程分为五个关键环节,每个环节都解决一个具体痛点。
4.1 环境隔离与配置加载:用dotenv-safe杜绝密钥硬编码
第一步不是写代码,而是切断本地开发与生产环境的耦合。我们见过太多事故:开发人员把测试数据库密码提交到Git,或在process.env.NODE_ENV === 'development'里写死调试逻辑,上线后直接崩盘。
实操步骤:
- 创建
.env(开发环境)和.env.production(生产环境)文件:
# .env NODE_ENV=development PORT=3000 DB_HOST=localhost DB_PORT=5432 DB_NAME=testdb JWT_SECRET=dev-secret-key# .env.production NODE_ENV=production PORT=8080 DB_HOST=prod-db.cluster-xyz.us-east-1.rds.amazonaws.com DB_PORT=5432 DB_NAME=proddb JWT_SECRET=prod-very-long-secret-key- 安装
dotenv-safe:它会在加载.env前,校验环境变量是否符合.env.example的声明,防止漏配:
npm install dotenv-safe- 在
app.js顶部加载:
require('dotenv-safe').config({ path: process.env.NODE_ENV === 'production' ? '.env.production' : '.env', example: process.env.NODE_ENV === 'production' ? '.env.example' : '.env.example' });- 创建
.env.example强制声明必需变量:
# .env.example NODE_ENV= PORT= DB_HOST= DB_PORT= DB_NAME= JWT_SECRET=为什么不用原生dotenv?
dotenv-safe会检查.env中是否缺失.env.example声明的变量,缺失则抛出错误,避免运行时才发现配置错误;- 它支持多环境文件切换,无需修改代码;
- 所有敏感变量(如
JWT_SECRET)绝不硬编码,全部走环境变量。
4.2 中间件分层架构:按职责切分,拒绝“万能中间件”
我们把中间件分为四层,每层只做一件事,且层间有清晰边界:
| 层级 | 作用 | 示例中间件 | 加载顺序 |
|---|---|---|---|
| 基础层 | 请求预处理 | express.json()、express.urlencoded()、helmet()(安全头) | 最前 |
| 上下文层 | 注入请求上下文 | asyncLocalStorage绑定、requestId生成、traceId透传 | 基础层后 |
| 业务层 | 业务逻辑前置 | authMiddleware(JWT校验)、rateLimit(限流)、validateInput(参数校验) | 上下文层后 |
| 路由层 | 具体业务路由 | app.use('/api', apiRouter) | 最后 |
实操代码(middleware/index.js):
const { AsyncLocalStorage } = require('async_hooks'); const asyncLocalStorage = new AsyncLocalStorage(); // 基础层 const baseMiddlewares = [ express.json({ limit: '10mb' }), express.urlencoded({ extended: true, limit: '10mb' }), helmet({ contentSecurityPolicy: false }), // 生产环境开启CSP ]; // 上下文层 const contextMiddlewares = [ (req, res, next) => { const requestId = uuidv4(); const traceId = req.headers['x-trace-id'] || requestId; // 将上下文注入AsyncLocalStorage asyncLocalStorage.run({ requestId, traceId, startTime: Date.now() }, () => { next(); }); }, ]; // 业务层 const businessMiddlewares = [ authMiddleware, // JWT校验,失败时res.status(401) rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }), // 15分钟100次 ]; // 导出分层中间件数组,便于测试和替换 module.exports = { base: baseMiddlewares, context: contextMiddlewares, business: businessMiddlewares, };关键设计点:
baseMiddlewares处理所有请求,无论是否登录;contextMiddlewares必须在base之后,因为express.json()会解析req.body,而上下文可能需要读取它;businessMiddlewares只对API请求生效,静态资源不经过它;- 所有中间件都导出为数组,方便单元测试时单独加载。
4.3 路由模块化:用express.Router()实现可插拔架构
大型项目里,app.js绝不能变成千行巨兽。我们采用“路由即模块”策略,每个业务域一个Router文件,通过index.js统一导出。
目录结构:
routes/ ├── index.js # 聚合所有路由 ├── auth/ # 认证路由 │ ├── router.js # 路由定义 │ └── controller.js # 业务逻辑 ├── user/ # 用户路由 │ ├── router.js │ └── controller.js └── post/ # 文章路由 ├── router.js └── controller.js实操代码(routes/index.js):
const authRouter = require('./auth/router'); const userRouter = require('./user/router'); const postRouter = require('./post/router'); // 统一添加API前缀和错误处理 const apiRouter = express.Router(); // 所有API路由加版本前缀 apiRouter.use('/v1', authRouter); apiRouter.use('/v1', userRouter); apiRouter.use('/v1', postRouter); // API路由级错误处理 apiRouter.use((err, req, res, next) => { console.error(`[API Route Error] ${req.method} ${req.url}`, err); res.status(500).json({ error: 'Internal Server Error' }); }); module.exports = apiRouter;为什么这样设计?
/v1前缀集中管理,升级v2时只需改这里,不用遍历所有Router文件;apiRouter.use()的错误处理器只捕获/v1下的错误,不影响健康检查等根路径;- 每个子Router(如
auth/router.js)可以有自己的router.use(),实现细粒度控制。
4.4 错误处理体系:三层捕获,确保无死角
我们构建了三层错误捕获网:
- 路由内捕获:每个控制器方法用
try/catch包裹,处理业务异常; - 路由级捕获:
router.use((err, req, res, next) => {}),处理该Router内未捕获的错误; - 应用级捕获:
app.use((err, req, res, next) => {}),兜底所有错误。
实操代码(middleware/errorHandler.js):
// 应用级错误处理器 const errorHandler = (err, req, res, next) => { // 从AsyncLocalStorage获取上下文 const store = asyncLocalStorage.getStore(); const { requestId, traceId } = store || {}; // 分类处理错误 if (err.name === 'ValidationError') { // Joi/Zod校验失败 return res.status(400).json({ error: 'Validation failed', details: err.details }); } if (err.name === 'UnauthorizedError') { // JWT校验失败 return res.status(401).json({ error: 'Unauthorized' }); } // 数据库连接错误 if (err.code === 'ECONNREFUSED') { console.error(`[DB Connection Failed] ${err.message}`); return res.status(503).json({ error: 'Service Unavailable' }); } // 未知错误,记录详细日志 console.error(`[Unhandled Error] ${req.method} ${req.url} | ID: ${requestId} | Trace: ${traceId}`, err); // 生产环境不暴露堆栈 if (process.env.NODE_ENV === 'production') { res.status(500).json({ error: 'Internal Server Error' }); } else { res.status(500).json({ error: err.message, stack: err.stack }); } }; module.exports = errorHandler;关键技巧:
- 错误分类比统一返回500更有价值:前端可以根据
400重试表单,401跳转登录页,503显示维护提示; requestId和traceId必须随错误日志一起输出,这是分布式追踪的基石;- 生产环境禁用
err.stack,防止敏感路径泄露。
4.5 性能与可观测性:内置健康检查与指标埋点
一个“实战”服务,必须自带健康检查和基础监控。我们不引入Prometheus等重型方案,而是用轻量级方式实现。
实操步骤:
- 添加健康检查路由:
// routes/health.js const router = express.Router(); router.get('/health', (req, res) => { // 检查数据库连接 const dbStatus = checkDatabaseConnection(); // 检查Redis(如果用了) const redisStatus = checkRedisConnection(); const status = dbStatus && redisStatus ? 'UP' : 'DOWN'; res.status(status === 'UP' ? 200 : 503).json({ status, timestamp: new Date().toISOString(), uptime: process.uptime(), }); }); module.exports = router;- 在
app.js中注册:
// 健康检查必须放在所有业务路由之前,且不经过认证 app.use('/health', healthRouter);- 添加基础性能指标:
// middleware/performance.js const performanceMiddleware = (req, res, next) => { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; console.info(`${req.method} ${req.url} ${res.statusCode} ${duration}ms`); }); next(); };为什么健康检查要独立路由?
- Kubernetes/Liveness Probe需要快速响应,不能和业务逻辑耦合;
- 它必须绕过所有认证和限流,否则探针会失败;
- 返回结构化JSON,便于运维脚本解析。
5. 常见问题与排查技巧实录:来自三次线上事故的血泪总结
最后,分享我们在真实项目中踩过的坑,以及对应的排查心法。这些不是文档里的标准答案,而是深夜救火后记在笔记本上的经验。
5.1 问题:接口响应时间忽高忽低,监控显示CPU和内存都很低
现象:某文章详情接口,P95延迟从200ms飙升到2s,但服务器监控一切正常,日志里也没有错误。
排查过程:
- 第一步:用
curl -w "@format.txt"查看各阶段耗时,发现time_connect和time_starttransfer都很低,但time_total很高,说明问题在服务端处理,而非网络; - 第二步:在
performanceMiddleware里打印Date.now()和process.hrtime(),发现hrtime显示处理时间稳定,但Date.now()差距很大; - 第三步:意识到是Node.js事件循环被阻塞。用
0x工具采样火焰图,发现fs.readFileSync()在读取一个10MB的配置文件; - 第四步:定位到
configLoader.js,它在每次请求时都同步读取YAML配置。
根因:fs.readFileSync()是阻塞式IO,会冻结整个Node.js事件循环。10MB文件读取耗时约800ms,期间所有请求排队等待。
解决方案:
- 配置文件启动时一次性加载,存入
app.locals.config; - 用
fs.readFile()(异步)替代fs.readFileSync(); - 对大文件做缓存:
const config = require('config.json'),利用Node.js模块缓存机制。
注意:Node.js的
require()对JSON文件有内置缓存,但对YAML/JS文件没有。务必用fs.readFile()+JSON.parse()或专用YAML库。
5.2 问题:日志里出现大量Error: Can't set headers after they are sent
现象:日志刷屏,但接口似乎还能用,只是偶尔返回空白页。
根因:同一个响应对象被多次写入。常见于:
- 在
res.json()后又调用res.send(); - 异步操作中,多个分支都调用了
res.*; next()和res.*混用,比如if (err) res.status(500).send(); else next();,但next()后还有中间件继续写响应。
排查技巧:
- 在
res.send、res.json等方法上打猴子补丁,记录调用栈:
const originalSend = res.send; res.send = function(...args) { console.trace('[Response Sent]'); // 打印完整调用栈 return originalSend.apply(this, args); };- 使用
on-finished包监听响应完成事件:
const onFinished = require('on-finished'); onFinished(res, (err) => { if (err) console.error('Response finished with error', err); else console.log('Response finished successfully'); });5.3 问题:JWT Token校验通过,但req.user在下游中间件里是undefined
现象:authMiddleware里req.user = decodedUser,但到了postRouter里req.user是undefined。
根因:req对象是每个请求独享的,但中间件的执行顺序错了。
排查步骤:
- 在
authMiddleware里打印console.log('Auth middleware: ', req.user); - 在
postRouter的第一个中间件里打印console.log('Post router: ', req.user); - 如果前者有值后者没有,说明
authMiddleware没被调用,或者调用时机不对。
解决方案:
- 确认
authMiddleware加载在app.use('/api', apiRouter)之前; - 确认
authMiddleware是app.use(authMiddleware),而不是router.use(authMiddleware)(后者只对/api下的子路由生效); - 检查
authMiddleware是否在next()前就return res.status(401),导致后续中间件不执行。
5.4 问题:部署到Docker后,静态资源404,但本地运行正常
现象:docker-compose up后,访问/static/logo.png返回404,ls /app/public确认文件存在。
根因:Docker容器内路径与本地不一致,或express.static()的root参数错误。
排查清单:
- 进入容器:
docker exec -it <container> sh; - 检查文件是否存在:
ls -la /app/public/static/; - 检查Express配置:
app.use('/static', express.static('public/static')),注意路径是相对process.cwd()的; - Dockerfile中
WORKDIR是否设置正确?应该设