主题
中间件(Day11 1/3)
本页对应 Day11_Node,共 7 段视频,约 1 小时 40 分。
学习目标
- 能说明 Express 中间件的本质、
req/res/next三个参数的作用及中间件执行顺序,并区分应用级、路由级和错误处理中间件。 - 能正确使用
app.use()挂载全局或指定路径的中间件,并说明调用next()、结束响应与请求挂起之间的关系。 - 能识别并使用 Express 自带中间件(如
express.static)和第三方中间件(如请求体解析中间件),说明其挂载方式与作用。 - 能独立编写并挂载访问日志中间件,将请求方式、请求路径、IP、访问时间等信息追加写入日志文件,并在写入成功后调用
next()。 - 能独立编写并挂载四参数错误处理中间件,在路由出错时返回 500 响应并记录错误日志,且将其放在所有路由和中间件之后。
- 能使用
express.Router()将路由拆分为独立模块,通过module.exports暴露路由对象,并在入口文件中用app.use()完成挂载与路径拼接。
知识点
注:给定笔记未包含图片,以下不虚构图片;代码按转写稿口述还原,并纠正同音字。
从 Node 回顾到 Express 的定位
为什么需要:先明确 Node 不是浏览器,而是让 JS 在服务端运行;内置模块里 path、url、querystring、fs 中 fs 方法最多。模块规范有 CommonJS 和 ESM,Node 默认主流仍是 CommonJS,ESM 需改 package.json 或把扩展名改成 .mjs。怎么想:后续用第三方包,很多新版本可能转向 ESM,若 require 不动,可降级到支持 CommonJS 的版本;json 文件被 require 会自动解析成对象。怎么做:用 npm 管理第三方包,用内置 http 或 Express 实现 HTTP 服务。易错点:别把 json 叫内置模块,它是内置对象;Express 不是内置模块,是第三方包;学习 Express 是为熟悉后端流程、会写简单服务,不是深究所有框架,Koa 等知道即可。转写稿还强调新闻列表案例:后端把数据填进 HTML 再响应,浏览器拿到的是处理好的内容;/news?id=1 和 /news/1.html 中后者是伪静态,并非真有该文件。
创建服务:先跑通再谈中间件
为什么需要:先有一个能启动的 Express 服务,才能挂中间件和路由。怎么想:npm init 初始化项目,包名必须 URL friendly,不能中文和特殊符号,所以 01-中间件 不合法,可改 express-demo;后端依赖一般是产品依赖。怎么做:
js
const express = require('express');
const app = express();
app.listen(8080, () => {
console.log('HTTP Server is running at :8080');
});易错点:express 拼写错会报意外;改代码后 Node 不会自动重启,调试可用 nodemon;没写路由时访问也会得到默认 404 响应。
中间件本质:收费站模型
为什么需要:Express 的核心是路由和中间件,应用就是在调用各种中间件。怎么想:中间件是函数,接收 req、res、next;像高速收费站,请求按顺序一个个过。路由回调也是中间件。怎么做:
js
app.use((req, res, next) => {
console.log('hello,我是中间件tom');
next();
});
app.use('/login', (req, res, next) => {
console.log('刷我的卡,我是中间件泰酷辣');
next();
});易错点:中间件要么结束响应,要么调用 next(),否则浏览器一直转圈;next() 是放行,不是结束,next() 后面的语句仍会执行,除非 return;app.use 不写路径则所有请求都经过,写路径只匹配该路径,且不挑请求方式;先匹配的中间件不放行,后面的不会执行。
自带与第三方中间件
为什么需要:静态资源、请求体解析不必手写。怎么想:express.static 是 Express 自带中间件,app.use(express.static(...)) 是挂载;body-parser 是第三方中间件,用来解析请求体,让 req.body 出现。怎么做:先安装并 require,再 app.use 挂载;静态资源托管内部会匹配 URL、读文件、设 MIME 类型。易错点:不挂 body-parser 时 req.body 可能没有;中间件顺序影响结果,谁先结束响应,后面就不执行。
自定义访问日志中间件
为什么需要:记录谁、何时、用什么方式访问了什么路径。怎么想:把独立功能拆成模块,放 middleware/access.log.js,入口只负责挂载。怎么做:
js
const moment = require('moment');
const fs = require('fs');
const path = require('path');
module.exports = (req, res, next) => {
const ip = req.ip.slice(7);
const dt = moment().format('YYYY-MM-DD HH:mm:ss');
const logMsg = `${ip} ${dt} ${req.method} ${req.url}\n`;
fs.appendFile(path.resolve(__dirname, '../logs/access.log'), logMsg, (err) => {
if (err) throw err;
console.log(logMsg);
next();
});
};入口 app.use(accessLog)。易错点:req.ip 常见 ::ffff: 前缀,转写稿用 slice(7) 截掉;本地访问可能截后为空;路径要用 path.resolve(__dirname, ...);日志要换行;写日志成功后再 next()。
错误处理中间件:四参数且放最后
为什么需要:Express 默认把错误信息响应给浏览器,会暴露服务器路径,历史上很多攻击与此有关。怎么想:开发时给自己看,上线时只给客户端 500,详细错误写日志。怎么做:
js
module.exports = (err, req, res, next) => {
const errMsg = `${req.ip} ${req.method} ${req.url}\n${err.stack}\n\n\n`;
fs.appendFile(path.resolve(__dirname, '../logs/error.log'), errMsg, () => {});
res.status(500).send('服务器出错');
};入口最后 app.use(catchError)。易错点:必须四个参数,少一个就不是错误处理中间件;必须挂在所有路由和中间件后面;这里不再 next();err.stack 只写日志,不直接响应给用户。
路由模块化:express.Router
为什么需要:入口文件不能越写越长,路由要按模块拆。怎么想:express.Router() 创建可挂载的路由系统,模块暴露的必须是 Router 返回值。怎么做:
js
// routes/index.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => res.send('首页'));
router.get('/index', (req, res) => res.send('首页'));
module.exports = router;
// routes/login.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => res.send('登录页'));
router.post('/', (req, res) => res.send('提交成功'));
module.exports = router;入口:
js
const indexRouter = require('./routes/index');
const loginRouter = require('./routes/login');
app.use(indexRouter);
app.use('/login', loginRouter);易错点:每个路由模块要自己 require('express');module.exports = router;挂载 /login 后,模块内写 / 就是 /login,若模块内再写 /login 会拼接成 /login/login;挂载顺序应在访问日志之后、错误处理之前;转写稿调试时先没响应,是因为入口还没挂载路由模块。
回顾总览:先建立一条主线
这一节是回顾课,不是重新背 API,而是把 Node 学习的链路串起来:内置模块 → 自定义模块 → npm 包管理 → HTTP 协议与内置 http 模块 → express。先看本节笔记保留的清单:
1. 内置模块
path url querystring fs
2. 自定义模块
模块内部暴露数据
导入模块
3. npm包管理工具
4. http协议
5. 内置 http 模块
6. express
6.1 创建服务 启动服务
6.2 路由
6.3 请求对象和响应对象为什么需要:零散知识容易忘,回顾能看清每块解决什么问题。怎么想:把“Node 能直接运行 JS”作为起点,再逐步加模块化、包、HTTP 服务。怎么做:按清单逐项过,重点抓 CommonJS 的 require/module.exports、express 的 app.listen、路由和 req/res。易错点:回顾不是背名词,像 querystring 不要写成“queing stream”,CommonJS、package.json、.mjs 都要写准。
内置模块:path、url、querystring、fs
为什么需要:Node 要处理路径、URL、查询字符串和文件,这些能力不能都自己写。怎么想:path 管路径,url 管 URL,querystring 管查询串,fs 管文件;其中 fs 方法最多,文件读写、追加等都要用。怎么做:用 require('path') 等直接导入,内置模块不需要写路径。易错点:querystring 在后续案例会用到;JSON 文件也能被 require,会自动解析成对象,但别把它和内置模块混为一谈,JSON 更像可直接使用的数据。
自定义模块:CommonJS 与 ESM
为什么需要:代码要拆分,模块内部数据不能全暴露。怎么想:Node 默认支持 CommonJS,用 module.exports 暴露、require 导入;ESM 也支持,但默认不开启,要改 package.json 或把扩展名改成 .mjs。怎么做:模块里写 module.exports = ...,入口里 const x = require('./x')。易错点:module.exports 默认是空对象,不写暴露就拿到空对象;后续用第三方包时,新版本可能改成 ESM,require 不动就不要硬顶,转写稿明确说可找支持 CommonJS 的历史版本。当前主流仍是 CommonJS,先把这套用熟。
npm:包管理工具
为什么需要:第三方包能极大方便开发,express 就是第三方包。怎么想:项目先初始化,再安装依赖。怎么做:npm init 生成 package.json,npm install express 安装;package-lock.json 锁版本。转写稿里演示 npm init 时包名写成“01-中间件”被拒,因为包名只能包含 URL 友好字符,不能中文和特殊符号,于是改成 express-demo。易错点:后端开发一般只有产品依赖;安装慢可换源;包名别乱起。
HTTP协议与内置http模块
为什么需要:前端要和后端通信,必须理解 HTTP。怎么想:内置 http 模块能实现 HTTP 协议,但写法繁琐;express 是第三方框架,把常用流程简化了。怎么做:知道请求方式、URL、请求体、响应这些基本概念;内置 http 能建服务,但实际搭建更常用 express。易错点:express 不是内置模块,要安装;Koa 等类似框架很多,但本阶段用 express 足够,重点熟悉后端开发流程。
express:创建服务和启动服务
为什么需要:手写内置 http 服务太繁琐。怎么想:require('express') 得到的是一个函数,调用后得到应用对象,再用 listen 启动。怎么做:
js
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('首页'));
app.listen(8080, () => console.log('http server is running:8080'));易错点:转写稿演示时 express 拼写错导致“有意外”;Node 改完代码要重启,可用 nodemon;listen 的端口和回调要写对。
express路由:从URL判断到路由方法
为什么需要:传统方式在回调里判断 URL 很累。怎么想:用 app.get、app.post 等方法,把请求方式和路径直接匹配。怎么做:路由方法第一个参数是路径,第二个是回调;路径可字符串精确匹配、模糊匹配,也可用正则;同一路径不同请求方式可用 app.route().get().post()。易错点:路由回调也是中间件式函数,多个回调要按顺序执行,前一个不放行也不结束响应会挂起;路径和请求方式要同时匹配。
请求对象和响应对象:继承原生并扩展
为什么需要:路由里要拿请求数据、给响应。怎么想:express 的 req/res 继承了原生请求、响应对象,又有自己独有方法,如 res.send。怎么做:回调参数写 (req, res),从 req 取数据,用 res 结束响应。易错点:不要既结束响应又继续放行;响应结束方式要统一。转写稿的新闻列表案例说明:后端把数据填进 HTML 再返回,浏览器拿到的是已处理好的 HTML,这就是后端眼里的动态资源;传新闻 ID 可用 query,也可用路径参数,路径后加 .html 是伪静态,并不是真有这个文件。
先理解:Express 应用为什么离不开中间件
Express 是一个自身功能极简、完全由路由和中间件构成的 Web 开发框架。转写稿里反复强调:一个 Express 应用本质上就是在调用各种中间件。为什么需要这个认识?因为后面写 app.use()、路由回调、错误处理、静态资源托管,全都落在“中间件”这个模型上。怎么想?可以把一次请求想成车辆上高速:中间件就是一个个收费站,请求按顺序经过,谁能结束响应、谁放行,决定了后面的代码是否执行。怎么做?先记住中间件是一个函数,接收请求对象、响应对象和调用下一个中间件的函数。易错点:不要把它当成普通“配置项”,它是按挂载顺序执行的函数。
中间件到底是什么:函数、参数与 next()
为什么需要:如果不理解中间件的函数本质,就会把路由回调和 app.use() 割裂开。转写稿明确说:路由方法的回调函数也是中间件。怎么想:中间件能执行任意代码、修改请求和响应对象、结束响应、调用堆栈中的下一个中间件。怎么做:基本形态如下:
js
app.use((req, res, next) => {
console.log('hello, 我是中间件 tom');
next();
});next() 的作用是放行,不是结束。易错点:如果当前中间件既不结束响应,也不调用 next(),请求就会挂起,浏览器一直转圈;如果既 res.send() 又 next(),后面的中间件可能再次响应,产生冲突。转写稿调试时还发现:next() 后面的语句仍会执行,除非在 next() 前加 return。
应用级中间件:app.use 的挂载与顺序
为什么需要:应用级中间件可以统一处理所有请求,比如日志、鉴权、静态资源。怎么想:app.use() 是“挂载”中间件;不写路径时,所有请求都会经过;写路径时,只匹配该路径。怎么做:
js
app.use((req, res, next) => {
console.log('我是中间件 tom');
next();
});
app.use('/login', (req, res, next) => {
console.log('我是中间件 泰酷辣');
next();
});易错点:中间件按顺序执行。转写稿用“收费站”比喻:先经过 A 收费站,A 不放行,B 就等不到这辆车;A 若结束响应,后面的路由也不会执行。所以一般把“结束响应”留给最后一个处理者。
自带与第三方中间件:静态托管和请求体解析
为什么需要:很多通用能力不必自己写。转写稿提到两个已用过的中间件:express.static 负责静态资源托管;body-parser 负责解析请求体。怎么想:它们本质仍是“调用后得到一个函数”,再交给 app.use() 挂载。怎么做:
js
app.use(express.static('public'));
// body-parser 用法在转写稿中用于让 req.body 出现易错点:请求对象原本不一定有 req.body,经过请求体解析中间件处理后才有;这正是中间件“修改请求对象”的体现。第三方中间件要先安装、再 require,不能直接使用。
自定义中间件:以访问日志为例
为什么需要:把访问路径、请求方式、IP、时间记录下来,便于排查问题。怎么想:把日志功能单独写成模块,在入口文件挂载,体现模块化。怎么做:转写稿中的核心代码如下:
js
const moment = require('moment');
const fs = require('fs');
const path = require('path');
module.exports = (req, res, next) => {
const ip = req.ip.slice(7);
const dt = moment().format('YYYY-MM-DD HH:mm:ss');
const logMsg = `${ip} ${dt} ${req.method} ${req.url}\n`;
fs.appendFile(path.resolve(__dirname, '../logs/access.log'), logMsg, err => {
if (err) throw err;
console.log(logMsg);
next();
});
};入口挂载:
js
const accessLog = require('./middleware/accessLog');
app.use(accessLog);易错点:本地访问时 req.ip 可能是 ::ffff: 后为空;写入成功后要 next() 放行;路径拼接要用 path.resolve,不能凭感觉写相对路径。
错误处理中间件:四个参数与最后挂载
为什么需要:Express 默认可能把错误信息直接响应给浏览器,暴露服务器路径,转写稿明确说这很危险。怎么想:错误处理中间件要接收四个参数,并且放在所有路由和中间件之后,被动等待前面的错误。怎么做:
js
module.exports = (err, req, res, next) => {
console.error(err.stack);
res.status(500).send('服务器错误');
};挂载:app.use(errorHandler);。易错点:四个参数必须写全,少一个就不是错误处理中间件;对外只返回 500 和简单提示,详细错误写日志或控制台;它一般不再调用 next()。
路由级中间件:用 express.Router 做路由模块化
为什么需要:入口文件不能越写越长。怎么想:express.Router() 返回一个路由对象,把同一类路由拆到单独文件,最后在入口 app.use() 挂载。怎么做:
js
// routes/index.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => res.send('首页'));
router.get('/index', (req, res) => res.send('首页'));
module.exports = router;js
// routes/login.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => res.send('登录页'));
router.post('/', (req, res) => res.send('提交成功'));
module.exports = router;入口:
js
const indexRouter = require('./routes/index');
const loginRouter = require('./routes/login');
app.use(indexRouter);
app.use('/login', loginRouter);
app.use(errorHandler);易错点:路由模块暴露的必须是 express.Router() 的返回值;挂载路径会与模块内路径拼接,例如 app.use('/login', loginRouter) 后,模块内写 router.get('/') 就对应 /login。转写稿还提醒:如果中间件不放行,后面的路由不会执行;路由回调通常负责结束响应。
课堂画面






代码示例
示例一:应用级中间件 + next() 放行(理解“收费站”模型)
js
// index.js
const express = require('express')
const app = express()
// 中间件1:不指定路径,任何请求都会先经过它
app.use((req, res, next) => {
console.log('hello,我是中间件 tom,how are you')
next() // 放行,交给下一个中间件/路由
})
// 中间件2:挂载到指定路径 /login,只有该路径的请求才会经过
app.use('/login', (req, res, next) => {
console.log('刷瓦迪卡,我是中间件泰酷辣')
next()
})
// 路由的回调函数本身也是中间件
app.get('/', (req, res) => {
res.send(`<h1>首页</h1><hr><a href="/login">登录</a>`)
})
app.get('/login', (req, res) => {
res.send(`
<h1>登录</h1>
<form action="/login" method="post">
<input type="text" name="username" placeholder="请输入用户名">
<input type="password" name="pwd" placeholder="请输入密码">
<button>提交</button>
</form>
`)
})
app.post('/login', (req, res) => {
res.send('<h2>提交成功</h2>')
})
app.listen(8080, () => {
console.log('http server is running: http://localhost:8080')
})要点解释 / 易错点:
app.use(中间件函数)就是“挂载”——新建一个收费站;中间件本质是一个函数,参数是req、res、next。- 不指定路径的中间件对所有请求生效;
app.use('/login', fn)只匹配以/login开头的请求(如/login、/login/abc都会经过)。 - 易错点:既不
res.send()结束响应、又不调用next(),请求会被“挂起”,浏览器一直转圈。 - 易错点:既结束了响应又调用
next()会冲突,后面中间件再res.send()会报错。要么结束响应,要么放行,二选一。 next()只是“放行”,next()后面的代码仍会继续执行(除非写return next());且next是函数,不能写成next不带括号。- 多个中间件按注册顺序依次匹配,前一个不放行,后面的中间件根本轮不到。
示例二:自定义访问日志中间件(模块化 + fs 追加写 + moment 格式化)
js
// middleware/accessLog.js
const fs = require('fs')
const path = require('path')
const moment = require('moment')
// 中间件就是一个函数,直接暴露出去
module.exports = (req, res, next) => {
let ip = req.ip // ::ffff:192.168.23.62
const method = req.method
const url = req.url
// 截掉 ::ffff: 前缀(前 7 个字符)
if (ip && ip.startsWith('::ffff:')) {
ip = ip.slice(7)
}
// 格式化当前时间:YYYY-MM-DD HH:mm:ss
const dt = moment().format('YYYY-MM-DD HH:mm:ss')
// 拼接日志内容,末尾一定要换行
const logMsg = `${ip} ${dt} ${method} ${url}\n`
// 追加写,注意路径要相对“当前文件”计算
fs.appendFile(path.resolve(__dirname, '../logs/access.log'), logMsg, (err) => {
if (err) throw err // 抛给错误处理中间件
console.log(logMsg)
next() // 日志写完再放行
})
}js
// index.js
const express = require('express')
const accessLog = require('./middleware/accessLog') // .js 可省略
const app = express()
// 挂载访问日志中间件:放在所有路由前面,任何请求都会先被记录
app.use(accessLog)
app.get('/', (req, res) => {
res.send('<h1>首页</h1><hr><a href="/login">登录</a>')
})
app.get('/login', (req, res) => {
res.send('<h1>登录</h1>')
})
app.listen(8080, () => {
console.log('http server is running: http://localhost:8080')
})要点解释 / 易错点:
module.exports = 函数就是自定义模块暴露中间件;入口文件用require('./middleware/accessLog')导入后app.use()挂载,两步缺一不可。req.ip本机访问会得到::ffff:开头(甚至后面为空),用ip.slice(7)截掉前缀;req.method、req.url来自继承的原生请求对象。- 易错点:
path.resolve(__dirname, '../logs/access.log')—— 路径必须基于当前 js 文件所在目录,直接写'logs/access.log'会相对启动目录,容易找不到文件。目录logs需提前建好。 - 用
fs.appendFile(追加写)而不是writeFile(覆盖),否则每次只留最后一条。 - 易错点:
next()必须写在fs.appendFile的异步回调里,写在外面会先放行再写日志;日志内容末尾要加\n,否则所有记录挤成一行。 - 中间件挂载顺序:访问日志要在路由之前,错误处理要在最后。
示例三:错误处理中间件 + Express.Router 路由模块化(完整项目结构)
js
// middleware/catchError.js —— 错误处理中间件,4 个参数,必须挂载在最后
const fs = require('fs')
const path = require('path')
const moment = require('moment')
module.exports = (err, req, res, next) => {
const ip = req.ip
const method = req.method
const url = req.url
const dt = moment().format('YYYY-MM-DD HH:mm:ss')
// err.stack 记录详细错误(哪一行出错)
const logMsg = `${ip} ${dt} ${method} ${url}\n${err.stack}\n\n\n`
fs.appendFile(path.resolve(__dirname, '../logs/error.log'), logMsg, () => {
// 这里不需要再 next(),错误中间件是最后一站
})
// 对外只给 500,不暴露服务器路径等细节
res.status(500).send('<h2>500 服务器出错</h2>')
}js
// routes/index.js —— 首页路由模块
const express = require('express')
const router = express.Router() // 创建路由对象
router.get('/', (req, res) => {
res.send(`<h1>首页</h1><hr><a href="/login">登录</a>`)
})
router.get('/index', (req, res) => {
res.send('<h1>首页 index</h1>')
})
module.exports = router // 暴露的必须是 express.Router() 的返回值js
// routes/login.js —— 登录路由模块
const express = require('express')
const router = express.Router()
router.get('/', (req, res) => {
res.send(`
<h1>登录</h1>
<form action="/login" method="post">
<input type="text" name="username" placeholder="请输入用户名">
<input type="password" name="pwd" placeholder="请输入密码">
<button>提交</button>
</form>
`)
})
router.post('/', (req, res) => {
// 故意调用一个不存在的函数,触发错误处理中间件
// notExistFn()
res.send('<h2>提交成功</h2>')
})
module.exports = routerjs
// index.js —— 入口文件只负责导入、挂载
const express = require('express')
const accessLog = require('./middleware/accessLog')
const catchError = require('./middleware/catchError')
const indexRouter = require('./routes/index')
const loginRouter = require('./routes/login')
const app = express()
app.use(accessLog) // 1. 访问日志(最前)
app.use(indexRouter) // 2. 首页路由
app.use('/login', loginRouter) // 3. 登录路由,挂载时指定前缀路径
app.use(catchError) // 4. 错误处理(最后)
app.listen(8080, () => {
console.log('http server is running: http://localhost:8080')
})要点解释 / 易错点:
- 错误处理中间件必须是 4 个参数
(err, req, res, next),少一个 Express 就当成普通中间件,不会捕获错误;且必须app.use()挂载在所有路由和中间件之后。 - 错误中间件内部不要再调用
next(),它是最后一站;对外只res.status(500).send('服务器出错'),真正的err.stack只写进日志,绝不能把错误信息直接响应给浏览器(会暴露服务器目录结构)。 - 路由模块里必须自己
require('express')——每个模块有自己的作用域,不能借用入口文件的变量。 - 易错点:
module.exports暴露的必须是express.Router()的返回值,把原来的app.get全改成router.get。 - 挂载
app.use('/login', loginRouter)时是路径拼接:模块里写router.get('/')实际就是/login,写router.get('/abc')就是/login/abc。所以登录模块内部两条路由都写成/即可。 - 入口文件顺序不能乱:访问日志 → 路由 → 错误处理;中间件一旦结束响应,后面的就不再执行。
常见错误
1. 中间件既不结束响应,也不调用 next()
- 现象:浏览器一直转圈,控制台能看到中间件里的
console.log输出,但页面没有响应,也没有报错。 - 原因:中间件是"收费站",要么放行(
next()),要么结束响应(res.send()等)。两者都不做,请求就一直挂起。 - 修法:检查每个中间件末尾,明确写
next()或res.send(...),没有第三种选择。
2. 同一个中间件里既 res.send() 又 next()
- 现象:控制台报
Cannot set headers after they are sent,或者响应内容是上一条路由的内容。 - 原因:
send已经把响应结束,next()又把控制权交给下一个中间件,下一个中间件再次响应就会冲突。 - 修法:二选一。约定俗成:中间件只负责加工和放行,结束响应交给最后一个路由处理函数。
3. 以为 next() 后面的代码不会执行
- 现象:
next()之后写的console.log依然输出,甚至造成重复写日志。 - 原因:
next()只是"放行"动作,不是return,当前函数体仍会继续往下执行。 - 修法:写成
return next();,把后续代码放到下一个中间件里。
4. 错误处理中间件参数写成 3 个
- 现象:故意制造错误后,页面直接显示报错堆栈(含服务器绝对路径),自己写的 500 页面没生效。
- 原因:Express 靠函数形参个数(4 个)识别错误处理中间件,写成
(req, res, next)就被当成普通中间件。 - 修法:必须写成
(err, req, res, next)四个参数且一个都不能省,并挂载在所有路由之后。
5. 中间件挂载顺序颠倒
- 现象:日志中间件只记录了部分请求;或者错误处理中间件"从来不执行";或者静态资源 404。
- 原因:Express 严格按挂载顺序自上而下匹配,写在前面的先执行,写在路由后面的日志中间件只能记录"漏网"的请求。
- 修法:固定顺序 —— 日志/静态资源/
body-parser→ 业务路由 → 404 处理 → 错误处理中间件(永远最后一个)。
6. 路由模块里直接用 app.get,报 app is not defined
- 现象:拆出
routes/index.js后启动报错,或模块能加载但请求 404。 - 原因:每个模块有独立作用域,入口文件里的
app在路由模块里不可见;且路由模块必须导出的是express.Router()的返回值。 - 修法:路由文件内单独
const express = require('express'),const router = express.Router(),把app.get改成router.get,最后module.exports = router,入口用app.use(router)挂载。
7. 挂载前缀与路由路径重复拼接
- 现象:
app.use('/login', loginRouter)后访问/login变成 404,访问/login/login反而正常。 - 原因:
app.use的第一个参数是前缀,会与 router 内部的路径拼接。 - 修法:挂了
/login前缀,router 内部就写router.get('/', ...)、router.post('/', ...),不要重复写'/login'。
8. 请求体拿不到,req.body 是 undefined
- 现象:POST 表单提交后,
req.body输出undefined,取字段报错。 - 原因:Express 默认不解析请求体,必须挂载解析中间件,且要挂在路由之前。
- 修法:
app.use(express.json())/app.use(express.urlencoded({ extended: true }))(或 body-parser),位置放在所有路由上方。
9. 改完代码不生效,还以为是逻辑写错了
- 现象:改了路由返回值,刷新页面还是旧内容;删掉的中间件日志还在输出。
- 原因:Node 不会热更新,进程里跑的还是旧代码。
- 修法:用
nodemon index.js启动;若已用 nodemon 仍不生效,检查是不是改错了文件(多份同名路由文件互相覆盖)。
练习
练习 1(中间件基础) 写一个中间件,为每个请求打印 方法 路径 耗时ms,要求请求结束后才打印耗时(提示:监听 res 的 finish 事件),并且不影响任何路由正常响应。
练习 2(顺序验证) 在入口文件中故意按"错误"顺序挂载:错误处理中间件写在路由之前。制造一个路由内异常,观察现象;再把顺序调整正确,对比两次的页面输出,用一句话写下结论。
练习 3(路由模块化) 把下面这组路由从入口文件中拆出: GET /users、GET /users/:id、POST /users、DELETE /users/:id。 要求:新建 routes/users.js,用 express.Router() 组织,在入口用前缀 /users 挂载,且 router 内部路径不重复写 /users。
练习 4(错误日志) 在练习 3 的基础上,让错误处理中间件除了返回 500 页面,还要把「时间 / IP / 请求方法 / 路径 / err.stack」追加写入 logs/error.log,每条之间空一行。故意在 /users/:id 中调用一个不存在的函数,验证日志文件内容。
练习 5(鉴权中间件) 写一个中间件挂在 /admin 前缀上:请求头没有 token 时返回 401 和一句提示;有 token 时放行。要求同时验证:访问 /users 不会被拦截,访问 /admin 会被拦截。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 总结回顾 | 12:48 |
| 2 | expess中间件介绍 | 08:19 |
| 3 | express自带中间件和第三方中间件 | 07:29 |
| 4 | express自定义中间件 | 20:55 |
| 5 | 定义一个访问日志中间件 | 22:39 |
| 6 | 定义一个错误处理中间件 | 14:26 |
| 7 | 实现路由模块化 | 14:11 |
小结
- 中间件的唯一铁律:要么
next()放行,要么结束响应,两者都不做请求就会挂起,两者都做就会响应冲突。 - 中间件就是普通函数,
app.use只是"挂载"动作;路由处理函数本质上也是中间件,区别只在是否绑定了路径和请求方法。 - 执行顺序完全由代码书写顺序决定,不是由路径长短或匹配精度决定;先匹配到又不放行,后面的永远轮不到。
- 中间件的核心价值是"横切":日志、静态资源、请求体解析、鉴权、错误捕获,都是同一段代码服务所有路由。
- 错误处理中间件靠 4 个形参被识别,必须挂在所有路由之后;它的存在不仅是兜底响应,更是避免把服务器路径等内部信息暴露给客户端。
express.Router()是路由模块化的标准做法:模块内自建 router、自导所需依赖、导出 router 实例,入口只负责导入 + 挂载。- 模块化的判断标准很简单:入口文件只做装配(导中间件、导路由、挂载、监听端口),业务细节一律下沉到独立文件。
- 调试期养成两个习惯:用 nodemon 启动避免"改了没生效"的误判;每次改完中间件顺序,都用"正常路径 + 异常路径 + 不存在路径"三条请求各验一遍。