Skip to content

中间件(Day11 1/3) ​

本页对应 Day11_Node,共 7 段视频,约 1 小时 40 分。

学习目标 ​

  1. 能说明 Express 中间件的本质、req/res/next 三个参数的作用及中间件执行顺序,并区分应用级、路由级和错误处理中间件。
  2. 能正确使用 app.use() 挂载全局或指定路径的中间件,并说明调用 next()、结束响应与请求挂起之间的关系。
  3. 能识别并使用 Express 自带中间件(如 express.static)和第三方中间件(如请求体解析中间件),说明其挂载方式与作用。
  4. 能独立编写并挂载访问日志中间件,将请求方式、请求路径、IP、访问时间等信息追加写入日志文件,并在写入成功后调用 next()。
  5. 能独立编写并挂载四参数错误处理中间件,在路由出错时返回 500 响应并记录错误日志,且将其放在所有路由和中间件之后。
  6. 能使用 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。转写稿还提醒:如果中间件不放行,后面的路由不会执行;路由回调通常负责结束响应。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:应用级中间件 + 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 = router
js
// 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
2expess中间件介绍08:19
3express自带中间件和第三方中间件07:29
4express自定义中间件20:55
5定义一个访问日志中间件22:39
6定义一个错误处理中间件14:26
7实现路由模块化14:11

小结 ​

  1. 中间件的唯一铁律:要么 next() 放行,要么结束响应,两者都不做请求就会挂起,两者都做就会响应冲突。
  2. 中间件就是普通函数,app.use 只是"挂载"动作;路由处理函数本质上也是中间件,区别只在是否绑定了路径和请求方法。
  3. 执行顺序完全由代码书写顺序决定,不是由路径长短或匹配精度决定;先匹配到又不放行,后面的永远轮不到。
  4. 中间件的核心价值是"横切":日志、静态资源、请求体解析、鉴权、错误捕获,都是同一段代码服务所有路由。
  5. 错误处理中间件靠 4 个形参被识别,必须挂在所有路由之后;它的存在不仅是兜底响应,更是避免把服务器路径等内部信息暴露给客户端。
  6. express.Router() 是路由模块化的标准做法:模块内自建 router、自导所需依赖、导出 router 实例,入口只负责导入 + 挂载。
  7. 模块化的判断标准很简单:入口文件只做装配(导中间件、导路由、挂载、监听端口),业务细节一律下沉到独立文件。
  8. 调试期养成两个习惯:用 nodemon 启动避免"改了没生效"的误判;每次改完中间件顺序,都用"正常路径 + 异常路径 + 不存在路径"三条请求各验一遍。