主题
中间件与express.Router(Day10 3/3)
本页对应 Day10_Node,共 7 段视频,约 1 小时 18 分。
学习目标
- 能编写 Express 路由,从
req中获取 URL、客户端 IP 和指定请求头(如user-agent),并通过响应内容验证获取结果。 - 能使用
req.query获取 GET 查询字符串,完成表单提交后的参数读取与响应验证。 - 能定义含路径参数的路由(如
/news/:id.html),使用req.params获取参数,并对不存在或非法参数返回 404。 - 能使用
app.use配置body-parser中间件,解析表单 POST 请求体并通过req.body获取数据,完成响应或写入文件验证。 - 能使用响应对象的
status、set、send、json、download、redirect等方法实现状态码/响应头设置、JSON 响应、文件下载、重定向和 404 页面。 - 能使用
express.Router拆分新闻列表与详情路由,并结合模板引擎或模板字符串根据数据动态生成列表和详情页,验证不同 ID 显示对应内容及异常处理。
知识点
从“应用就是中间件链”理解 Express
Express 自身功能极简,本质上由路由和中间件构成。换句话说,一个 Express 应用并不是一堆孤立的路由函数,而是在请求到来后,按注册顺序调用一串中间件。为什么需要先建立这个认识?因为很多初学者把中间件当成“额外插件”,结果在 app.use、路由顺序、next() 上反复出错。怎么想:把一次请求想成一条流水线,请求对象 req 和响应对象 res 从第一个中间件流到下一个中间件,直到某个环节把响应发出去,流程才结束。怎么做:先记住中间件的形态——它是一个函数,接收请求对象 req、响应对象 res,以及请求-响应循环中的下一个中间件,一般命名为 next。易错点:不要把它理解成普通路由;中间件可以挂在所有请求上,也可以只挂在某段路径上,关键看注册方式与顺序。
中间件到底能做什么
笔记给出的功能很明确:执行任何代码;修改请求和响应对象;终结请求-响应循环;调用堆栈中的下一个中间件。为什么需要这四条?因为它们解释了中间件为何能承担日志、解析请求体、鉴权、设置响应头等工作。怎么想:中间件不是只能“放行”,它可以在 req 上挂数据,也可以改 res,还可以直接 res.send() 结束响应。怎么做:写一个最小中间件:
js
const express = require('express');
const app = express();
app.use((req, res, next) => {
console.log('进入第一个中间件');
req.msg = '来自中间件';
next();
});
app.get('/', (req, res) => {
res.send(req.msg);
});
app.listen(3000, () => {
console.log('server running');
});访问 / 时先打印日志,再进入路由,最后响应“来自中间件”。易错点:如果当前中间件没有终结请求-响应循环,就必须调用 next() 把控制权交给下一个中间件,否则请求会挂起。可以这样验证:临时注释掉 next(),浏览器会一直等待;恢复后请求立即进入路由。这个调试过程能直观看到“中间件链断掉”的后果。
顺序:中间件必须放在需要它的路由之前
转写稿里安装并使用 body-parser 时,有一句很关键的推导:这段代码要“加到前面”,因为请求会按顺序先执行它。为什么需要强调顺序?因为 Express 按注册顺序执行中间件。怎么想:如果解析请求体的中间件放在路由后面,路由先执行,此时 req.body 还没有被解析,自然拿不到表单数据。怎么做:以表单 POST 为例,先安装并引入 body-parser,再在路由前注册:
js
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.post('/submit', (req, res) => {
console.log(req.body);
res.send('提交成功');
});转写稿中反复调试“请求体拿不到”的过程,最后就是靠这个中间件解决的;同时要纠正同音字:是 body-parser,不是“包底 parser”;req.body 也不是“包底”。易错点:body-parser 要放在处理 POST 的路由之前;表单提交通常用 urlencoded,不要误用 JSON 解析去接普通表单;extended: false 表示按较简单的方式解析,初学阶段按文档示例即可。
next() 与“终结响应”的边界
为什么需要区分 next() 和 res.send()?因为前者是“交给下一个”,后者是“我来结束”。怎么想:一个中间件要么继续放行,要么终结循环,通常不要既终结又放行。怎么做:日志、解析、挂载数据这类中间件调用 next();真正返回内容的中间件用 res.send()、res.json()、res.end() 等结束。易错点:调用了 next() 之后又执行 res.send(),容易出现响应重复;已经 res.send() 后还调用 next(),也可能让后续中间件继续处理并报错。更隐蔽的是忘记 next():代码不报错,但请求一直挂起,浏览器转圈。排查时先看中间件是否放行,再看是否已经有响应被发出。
把中间件放回请求处理链中理解
转写稿里还出现过用 app.use 做静态资源托管、用 app.use 注册 body-parser 的场景,这些都在说明:app.use 注册的是应用级中间件,它先于后面的路由参与请求处理。为什么需要这样串起来?因为实际项目里,请求体解析、静态资源、日志等往往都在路由前完成。怎么想:请求进来后,先经过一串 app.use,再匹配具体路由;路由本身也是请求处理链的一环。怎么做:把通用逻辑放前面,把具体业务路由放后面;需要继续时调用 next(),需要返回时直接响应。易错点:不要把所有逻辑都塞进一个中间件;也不要在中间件里忘记区分“所有请求”和“某路径请求”。本节笔记的核心可以压成一句话:Express 应用就是在调用各种中间件,而中间件的正确性,取决于它是否在正确的位置、是否在需要时调用 next()、是否在合适的时候终结请求-响应循环。
为什么要把路由拆成模块
为什么需要:当应用只有一两个接口时,把所有 app.get、app.use 都写在入口文件里还能接受;但路由一多,入口文件会越来越长,新闻列表、用户、后台等路由混在一起,阅读和维护都困难。转写稿里反复出现“写路由、加中间件、再启动服务”的过程,本质上就是路由和中间件不断增多。此时需要一种办法,把一组相关路由封装起来。
怎么想:不要一上来就复制粘贴,而是先判断哪些路由属于同一类。比如鸟类的首页和 about 页,可以看成一个“鸟类路由模块”。模块内部自己定义中间件和路由,外部只负责挂载。这样入口文件只保留启动服务和挂载模块的代码。
怎么做:使用 express.Router 创建路由实例。它本身是一个完整的中间件和路由系统,常被称为“mini-app”。也就是说,router.use、router.get 的写法和 app.use、app.get 很接近,但它可以独立导出。
易错点:不要把 Router 理解成“另一个 Express 应用实例”而重复启动服务;它只是路由和中间件的容器。转写稿里说的“中间键”应纠正为“中间件”,Router 里同样可以挂中间件。
编写并导出 birds 路由模块
为什么需要:把鸟类相关路由放进单独文件,可以让入口文件更干净,也方便以后把整个模块挂到不同前缀下。
怎么想:先创建 birds.js,在文件内引入 express,再创建 router。接着用 router.use 加一个记录时间的中间件,用 router.get 定义首页和 about 页,最后导出 router。
怎么做:笔记中的代码如下,保留原代码:
javascript
var express = require('express');
var router = express.Router();
// 该路由使用的中间件
router.use(function timeLog(req, res, next) {
console.log('Time: ', Date.now());
next();
});
// 定义网站主页的路由
router.get('/', function(req, res) {
res.send('Birds home page');
});
// 定义 about 页面的路由
router.get('/about', function(req, res) {
res.send('About birds');
});
module.exports = router;这里 router.use 中的 next() 很关键。转写稿里讲 app.use 挂载中间件时也强调过顺序和执行流程:中间件执行完要交给下一个处理环节,否则请求会停住。router.get('/') 和 router.get('/about') 定义的是模块内部路径,不是完整 URL。
易错点:第一,module.exports = router 导出的是 Router 实例,不要写成 module.exports = router()。第二,router.use 里忘记 next(),访问页面会卡住。第三,require('./birds') 的路径要相对于入口文件,文件位置不对会报“找不到模块”。
在入口文件挂载路由模块
为什么需要:birds.js 只是导出了一个 Router 实例,还没有接到 Express 应用上。必须通过 app.use 把它挂载,请求才会进入这个路由模块。
怎么想:入口文件先创建 app,再引入路由模块,最后用 app.use(birds) 挂载。挂载可以理解为“把一组路由接到应用上”。转写稿里用 app.use 挂载静态资源托管和 body-parser,说明 app.use 本来就是挂中间件/模块的入口;Router 也是同样的挂载思路。
怎么做:入口文件中写:
javascript
var birds = require('./birds');
...
app.use(birds);挂载后,访问 / 会进入 router.get('/'),访问 /about 会进入 router.get('/about')。如果写成 app.use('/birds', birds),那么模块内 / 就对应 /birds,/about 对应 /birds/about。笔记里写的是 app.use(birds),所以不要误以为一定要加 /birds 前缀。
易错点:第一,模块内路径和挂载前缀会拼接,容易重复写前缀。第二,挂载顺序会影响中间件执行顺序;转写稿中多次提醒“加到前面会按顺序先执行”,所以需要先执行的中间件应放在前面。第三,挂载后要启动服务再测试,不要只改文件不重启。
调试与验证
为什么需要:Router 是“模块化”的,出问题时容易分不清是模块没导出、没挂载,还是路径写错。
怎么想:按请求链路排查:入口是否 require 成功 → 是否 app.use 挂载 → Router 内路径是否匹配 → 中间件是否调用了 next()。
怎么做:启动服务后依次访问 / 和 /about。如果控制台持续输出 Time: ...,说明 router.use 中间件生效;如果页面分别返回 Birds home page 和 About birds,说明路由匹配正确。若访问 404,先检查挂载前缀和模块内路径是否拼接正确。
易错点:不要把 router.get('/about') 写成完整地址;不要漏掉 module.exports;不要忘记 next()。转写稿里“重定向”“响应”“请求头”“JSON”等词被口误成“重庆下”“显应”“请求投”“接证”,阅读时按正确术语理解即可。
在模板引擎之前:先看懂“后端拼页面”
这一节虽然标题是“模板引擎”,但转写稿真正做的是模板引擎的前置铺垫:先用 Express 获取请求报文信息,再在响应对象里手工拼接 HTML,做出一个“新闻列表/新闻详情”的后端渲染小案例。只有先体会“后端把数据替换进 HTML”的过程,后面学模板引擎才知道它到底替我们省了什么。
获取 URL、IP 和请求头
为什么需要:后端处理请求,第一步就是知道“谁来了、访问了什么、带了什么信息”。URL、IP、请求头都是请求报文里的基础信息。
怎么想:Express 的 req 继承了原生请求对象,又增加了自己的便捷属性。URL 用 req.url,IP 用 req.ip,请求头不能一次性全拿时,用 req.get('头名') 取指定项。
怎么做:
js
const express = require('express');
const app = express();
app.get('/index', (req, res) => {
const ip = req.ip;
const url = req.url;
const userAgent = req.get('user-agent');
res.send(`
<h1>Index</h1>
<p>您的 IP:${ip}</p>
<p>您的 URL:${url}</p>
<p>您的浏览器信息:${userAgent}</p>
`);
});
app.listen(3000, () => {
console.log('server running');
});转写稿里特意强调:这个 IP 是客户端 IP。把它显示给用户看,用户看到的是自己的 IP,不是服务器作者的 IP。
易错点:req.ip 不是 req.id;请求头要用 req.get('user-agent'),不要凭空写 req.headers.userAgent 这种大小写不严谨的写法。中文响应能正常显示,是因为 Express 的 res.send 会自动处理 Content-Type。
获取查询字符串:req.query
为什么需要:表单 GET 提交、搜索参数、分页参数常放在 URL 的 ? 后面。以前要手动用 url 模块解析,Express 直接给了 req.query。
怎么想:查询字符串本质是键值对,Express 已经解析成对象。表单里 name="wd"、name="type"、name="origin",提交后就能用 req.query.wd 等读取。
怎么做:
js
app.get('/search', (req, res) => {
res.sendFile(path.resolve(__dirname, 'form.html'));
});
app.get('/submit', (req, res) => {
const { wd, type, origin } = req.query;
res.send(`
<p>关键字:${wd}</p>
<p>类型:${type}</p>
<p>来源:${origin}</p>
`);
});易错点:路由匹配时,查询字符串不参与路径匹配,所以 /submit?wd=xx 仍然进入 /submit。另外表单提交方式若是 GET,参数才在 URL 上;POST 则要另取请求体。
获取路径参数:req.params
为什么需要:有些参数更适合放在路径里,例如 /news/1.html,看起来像静态文件,实际由后端动态生成。
怎么想:路由写成 /news/:id.html,冒号后面的 id 是变量名,可以随便取;请求 /news/845.html 时,req.params.id 就是 845。
怎么做:
js
app.get('/news/:category/:id.html', (req, res) => {
const { category, id } = req.params;
res.send(`分类:${category},ID:${id}`);
});易错点:路径参数用 req.params,查询字符串用 req.query,不要混。路由里写了 :id.html,访问时就必须带上 .html,否则匹配不到。
获取请求体:body-parser
为什么需要:POST 表单数据不在 URL 上,而在请求体里。直接读 req.body 拿不到,需要解析。
怎么想:Express 请求对象继承了原生对象,可以用 data、end 事件自己拼,但很麻烦。第三方模块 body-parser 可以做成中间件,把表单请求体解析到 req.body。
怎么做:
js
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: false }));
app.post('/submit', (req, res) => {
console.log(req.body);
res.send('提交成功');
});易错点:模块名是 body-parser,不是少个 r 的山寨包;表单 POST 一般用 urlencoded,不要错用 JSON 解析。中间件要放在路由前面,否则 req.body 可能还是空。
响应对象:send、status、set、json、download、redirect
为什么需要:后端最终要把状态码、响应头、响应体交给浏览器。
怎么想:Express 的 res 支持链式调用。status 设状态码,set 设响应头,send 发响应体,json 自动把对象转 JSON,download 触发下载,redirect 做跳转。
怎么做:
js
app.get('/index', (req, res) => {
res.status(201)
.set('server', 'my-server')
.set('token', 'abc')
.send('<h1>index</h1>');
});
app.get('/data', (req, res) => {
res.json({ name: 'news', id: 1 });
});易错点:链式调用中 set 后不要误加分号,否则链就断了;res.json 会自动设置 JSON 相关 Content-Type;redirect 是让浏览器重新请求目标地址。
新闻列表案例:用 map + join 拼 HTML
为什么需要:新闻有 3 条就写 3 个详情页,3 万条就不能手写。后端要根据数据动态生成页面。
怎么想:准备一个数组 newsData,列表页把每条新闻的标题拼成 <li><a>...</a></li>,详情页根据 ID 用 find 找对应新闻。
怎么做:
js
const newsData = require('./data.json');
app.get('/news', (req, res) => {
const list = newsData.map(item => {
return `<li><a href="/news/details?id=${item.id}">${item.news_title}</a></li>`;
}).join('');
res.send(`
<h1>新闻列表</h1>
<ul>${list}</ul>
`);
});
app.get('/news/details', (req, res) => {
const id = req.query.id;
const newsItem = newsData.find(item => item.id === id);
if (!newsItem) {
return res.status(404).send('您的新闻走丢了');
}
res.send(`
<h1>${newsItem.news_title}</h1>
<p>${newsItem.news_content}</p>
`);
});易错点:map 返回新数组,必须 join(''),否则数组转字符串会带逗号;find 返回第一个满足条件的元素,找不到返回 undefined,要判断并给 404。转写稿中 JSON 最外层写成数组导致报错,说明 JSON 文件格式必须合法。
升级为路径参数版
把 /news/details?id=1 改成 /news/1.html,路由写成 /news/:id.html,读取从 req.query.id 改成 req.params.id。用户看起来像访问静态文件,实际仍是后端按 ID 动态生成。这个“障眼法”正是理解后端渲染的关键:浏览器拿到的网页源代码里已经是生成好的 HTML,看不到后端变量。
易错点:改路由后,列表页里的 href 也要同步改成 /news/${item.id}.html;否则请求仍走旧地址。模板引擎后面要做的,就是把这里繁琐的字符串拼接替换成更清晰的模板渲染。
课堂画面






代码示例
示例1:Express 获取 URL、IP、请求头、查询字符串、路径参数
js
const express = require('express');
const app = express();
const port = 3000;
// 获取 URL、IP、请求头
app.get('/index', (req, res) => {
res.send(`
<h1>请求信息</h1>
<p>您的IP:${req.ip}</p>
<p>您的URL:${req.url}</p>
<p>您的浏览器信息:${req.get('user-agent')}</p>
`);
});
// 获取查询字符串:/search?wd=如何一夜暴富&type=态度&origin=ORGN
app.get('/search', (req, res) => {
res.send(`
<h1>查询字符串信息</h1>
<p>关键字:${req.query.wd}</p>
<p>搜索类型:${req.query.type}</p>
<p>来源:${req.query.origin}</p>
`);
});
// 获取路径参数:/news/娱乐/123.html
app.get('/news/:cate/:id.html', (req, res) => {
res.send(`
<h1>路径参数信息</h1>
<p>分类:${req.params.cate}</p>
<p>ID:${req.params.id}</p>
`);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});要点解释:
req.url获取请求 URL,req.ip获取客户端 IP,req.get('user-agent')获取指定请求头。req.query一步获取查询字符串对象,req.query.wd对应 URL 中的?wd=...。req.params获取路由中:cate、:id这类路径参数,路由里的.html是固定后缀。- 易错点:
req.query和req.params不要混用;req.get()只能获取指定请求头,不能一次获取全部请求头。 - 运行前先安装依赖:
npm install express。
示例2:body-parser 获取 POST 请求体
html
<!-- form.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>提交信息</title>
</head>
<body>
<form action="/submit" method="post">
<input type="text" name="username" placeholder="用户名">
<input type="text" name="phone" placeholder="电话">
<input type="text" name="message" placeholder="留言">
<button type="submit">提交</button>
</form>
</body>
</html>js
// index.js
const express = require('express');
const path = require('path');
const bodyParser = require('body-parser');
const app = express();
const port = 3000;
// 解析表单 POST 请求体
app.use(bodyParser.urlencoded({ extended: false }));
// 打开表单页面
app.get('/search', (req, res) => {
res.sendFile(path.resolve(__dirname, 'form.html'));
});
// 接收表单提交的数据
app.post('/submit', (req, res) => {
console.log(req.body);
res.send(`
<h1>提交成功</h1>
<p>用户名:${req.body.username}</p>
<p>电话:${req.body.phone}</p>
<p>留言:${req.body.message}</p>
`);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});要点解释:
bodyParser.urlencoded({ extended: false })必须写在 POST 路由之前,否则req.body拿不到表单数据。- 表单的
method="post"和action="/submit"要与app.post('/submit', ...)对应。 - 表单控件的
name要与req.body.username、req.body.phone、req.body.message对应。 - 易错点:
body-parser包名不要拼错;表单提交默认是application/x-www-form-urlencoded格式,所以用urlencoded解析。 - 运行前安装依赖:
npm install express body-parser,并保证form.html与index.js同目录。
示例3:express.Router 模块化路由
js
// birds.js
const express = require('express');
const router = express.Router();
// 该路由使用的中间件
router.use(function timeLog(req, res, next) {
console.log('Time: ', Date.now());
next();
});
// 定义网站主页的路由
router.get('/', function(req, res) {
res.send('Birds home page');
});
// 定义 about 页面的路由
router.get('/about', function(req, res) {
res.send('About birds');
});
module.exports = router;js
// app.js
const express = require('express');
const app = express();
const port = 3000;
const birds = require('./birds');
// 加载路由模块
app.use(birds);
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});要点解释:
express.Router()创建的是一个“mini-app”,可以定义中间件和路由。router.use(...)中必须调用next(),否则请求会挂起,无法继续执行后面的路由。module.exports = router导出路由模块,入口文件通过require('./birds')加载。app.use(birds)会把该 Router 挂载到应用上,因此可访问/和/about。- 易错点:中间件忘记写
next();require路径写错;Router 挂载顺序和中间件顺序要注意。
常见错误
- 现象:请求一直转圈、接口无响应。原因:中间件既没有
res.send()等终结响应,也没有调用next()。修法:检查每个中间件分支,未终结请求时必须return next()或调用下一个中间件。 - 现象:
req.body为undefined或为空对象。原因:未挂载请求体解析中间件,或解析器类型与提交格式不匹配,或中间件挂在路由之后。修法:表单POST使用express.urlencoded({ extended: false }),并确保解析中间件放在业务路由之前。 - 现象:详情页拿不到 ID,或拿到的是
undefined。原因:查询字符串应用req.query.id,路径参数应用req.params.id,二者混用;路由声明也未按/news/:id.html形式匹配。修法:先确认 URL 形式,再选择query或params,并保证路由占位符名称与读取属性一致。 - 现象:用
map()拼接多个<li>后页面多出逗号。原因:map()返回数组,直接插入模板字符串会触发数组默认逗号拼接。修法:在map()后补.join(''),明确用空字符串连接。 - 现象:设置响应头或状态码后报错,或后续响应设置不生效。原因:链式调用中途误加分号,提前结束了表达式;或响应已经发送后再次
send。修法:保持res.status(...).set(...).send(...)链式结构完整;每个请求只终结一次响应。 - 现象:详情页 ID 不存在时直接抛错或页面不友好。原因:
find()未命中时返回undefined,后续继续读取newsItem.title。修法:查到数据后先判断,未命中时return res.status(404).send(...),避免继续执行后续逻辑。
练习
- 编写一个全局中间件,打印请求方法、URL 和时间,然后调用
next();再验证不写next()时请求会怎样。 - 创建
/info路由,把客户端 IP、请求 URL、User-Agent请求头输出到页面;再用另一个路由演示res.json()返回对象。 - 做一个 GET 搜索表单,提交到
/search,使用req.query读取关键字、类型、来源,并把三项内容渲染到响应页面。 - 基于新闻数据实现详情页两种版本:一版用查询字符串
?id=...,一版用路径参数/news/:id.html;两版都要处理 ID 不存在时的 404。 - 用
express.Router()拆分新闻列表与详情路由,在主入口中挂载;要求 Router 内部至少有一个记录时间的中间件。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | express-获取url、ip和请求头 | 07:38 |
| 2 | express-获取查询字符串 | 06:07 |
| 3 | express-获取URL中的路径参数 | 04:52 |
| 4 | express-获取请求体 | 11:38 |
| 5 | express-响应对象 | 13:22 |
| 6 | express新闻列表小案例01 | 29:20 |
| 7 | express新闻列表小案例02-通过URL参数获取新闻ID | 05:42 |
小结
- 中间件首先是流程控制工具,写完后要检查“终结响应”还是“交给下一个”,不能两头都不做。
- Express 中顺序就是行为:解析器、日志、Router、404 兜底的位置放错,表现会完全不同。
- 读请求参数前先分清来源:URL 路径、查询字符串、请求体不是一回事,混用是高频错误。
- 响应对象适合链式组织,但要保证一次请求只终结一次,重定向和 404 分支尤其要记得
return。 - 动态页面不是“凭空动态”,本质仍是后端生成完整 HTML 后返回,浏览器源码中看不到后端变量。
- 第三方模块不要背 API,优先看官方文档的最小示例,并核对包名、安装来源和依赖是否写进
package.json。 - 调试时先确认请求是否进入目标路由,再确认中间件是否继续,最后确认响应是否已发送。