Skip to content

Express 概述与路由(Day10 2/3) ​

本页对应 Day10_Node,共 7 段视频,约 43 分钟。

学习目标 ​

  1. 能够使用 express.static 托管 public 目录,并通过浏览器验证静态页面、图片、视频及中文路径资源可正常访问。
  2. 能够将静态资源托管与路由注册在同一 Express 服务中,并依据注册顺序验证请求匹配优先级。
  3. 能够分别编写字符串精确匹配、字符串模糊匹配(?、+、*、{})和正则匹配路由,并验证不同 URL 的匹配结果。
  4. 能够编写带参数的 URL 路由,并通过 req.params 获取路径参数并返回对应响应。
  5. 能够使用 app.route() 组合同一路径的 GET 与 POST 请求,并分别执行对应回调返回结果。
  6. 能够为路由设置多个回调函数并使用 next 控制执行顺序,能够使用 app.all() 定制 404 页面并设置响应状态码。

知识点 ​

Express 概述与静态资源托管 ​

Express 是 Node.js 中常用的 Web 框架。转写稿里反复强调:它把 HTTP 服务、路由、静态资源托管等操作封装好了,底层思路和手写 HTTP 模块“可谓是一模一样”,只是用起来更省事。下面按“为什么需要 → 怎么想 → 怎么做 → 易错点”梳理本节内容。

为什么需要:如果每个静态文件都自己写判断和读取逻辑,代码会又长又容易漏。怎么想:把 public 目录整体交给 Express 托管,让框架按请求路径自动找文件。怎么做:用 express.static 作为中间件,并建议用 path.join(__dirname, 'public') 拼绝对路径。代码可整理为:

js
const express = require('express');
const path = require('path');
const app = express();

app.use(express.static(path.join(__dirname, 'public')));

app.listen(8080, () => {
  console.log('HTTP server is running on 8080');
});

启动后访问根路径,Express 会自动找到 public/index.html;转写稿中还观察到中文路径、编码和响应头都由它处理了。易错点:不要把相对路径写得太随意;express.static 是中间件,要放在合适顺序上。

路由与静态托管结合 ​

为什么需要:只有静态托管时,访问 /login 这类没有对应静态文件的路径会得到 404。怎么想:静态托管负责文件,路由负责动态响应,两者可以按顺序共存。怎么做:先写静态托管,再写路由;转写稿中把路由代码复制到静态托管之后,访问 /login 能正常响应,表单 POST 也能提交。易错点:路由有顺序,“谁先匹配到就按谁的”;根路径 / 可能被静态目录的 index.html 先接住。调试时如果代码放错文件,会出现“怎么没生效”的情况,转写稿里就出现过把代码复制到 03 文件后又换位置的过程。

字符串精确匹配与模糊匹配 ​

为什么需要:不同 URL 要返回不同内容,不能只靠一个根路径。怎么想:先掌握最常用的精确匹配,再了解类似正则子集的模糊匹配。怎么做:

js
app.get('/home', (req, res) => {
  res.send('<h1>Home</h1>');
});

app.get('/admin/*', (req, res) => {
  res.send('admin 页面');
});

转写稿说明:? 表示前面字符一次或零次,+ 表示一次或多次但不能零次,* 表示任意字符任意次;{} 可把多个字符作为整体。易错点:模糊匹配不是正则;/admin/* 能匹配 /admin/ 后任意内容,但精确路径不能随便加多余字符。转写稿还演示了 /index.html 这类路径看起来像静态文件,实际也可以由路由程序决定响应。

正则匹配与带参数 URL ​

为什么需要:有时要按后缀或复杂规则匹配,例如所有 .css 请求。怎么想:Express 路由可直接写 JS 正则;带参数的 URL 则用 :参数名 提取。怎么做:

js
app.get(/.*\.css$/, (req, res) => {
  res.send('这是一个快乐的页面');
});

app.get('/article/:id', (req, res) => {
  console.log(req.params.id);
  res.send('<h1>文章</h1>');
});

访问 /article/123 时,req.params.id 得到 123。易错点:正则不要加引号;/article/:id 不是让你访问字面量“:id”,而是把该段作为参数。转写稿中 /article 不行,/article/abc 可以,再加一段就不行。

路由组合与多回调 ​

为什么需要:同一路径可能有 GET、POST 等不同请求方式;同一路由也可能需要前置处理。怎么想:用 app.route() 把同一路径的多种方式组合起来;用多个回调做前置操作。怎么做:

js
app.route('/login')
  .get((req, res) => {
    res.sendFile(path.join(__dirname, 'public/login.html'));
  })
  .post((req, res) => {
    res.send('表单提交成功');
  });

app.get('/product/index',
  (req, res, next) => {
    console.log('第一个函数');
    next();
  },
  (req, res) => {
    res.send('product 页面');
  }
);

易错点:多回调中第一个回调不能提前结束响应;要执行下一个必须调用 next()。转写稿中第一个回调既 send 又 next 会报错,正确做法是把结束响应放到最后一个回调。

用 all 方法定制 404 ​

为什么需要:前面所有路由都没匹配到时,应给用户一个统一提示。怎么想:app.all('*') 能匹配所有请求方式,放在最后作为兜底。怎么做:

js
app.all('*', (req, res) => {
  res.status(404).send('<h1>404 页面不存在</h1>');
});

易错点:app.all('*') 必须放在最后;如果放前面,会把正常路由也拦截掉。转写稿中通过访问不存在的路径验证了 404 页面,说明兜底路由生效。

先理解:路由到底解决什么问题 ​

为什么需要:只用 http 模块时,我们要自己判断请求方式、请求路径,再写一堆 if/switch。路径一多,代码很快失控。
怎么想:Express 把“请求方式 + 路径 + 回调函数”封装成路由方法,匹配成功就执行回调。
怎么做:app.get()、app.post() 等方法名对应请求方式,参数是“路径字符串 + 回调函数”。
易错点:请求方式和路径必须同时匹配;不是写了路由就一定能响应,还要看注册顺序。

js
const express = require('express');
const app = express();

app.get('/home', (req, res) => {
  res.send('<h1>首页</h1>');
});

app.listen(8080, () => {
  console.log('HTTP server is running on 8080');
});

静态资源托管:一行中间件 ​

为什么需要:public 目录下有 HTML、CSS、JS、图片、视频等静态文件,如果每个都手写路由,不现实。
怎么想:Express 提供 express.static(),把一个目录作为静态资源根目录,请求进来后自动去目录里找文件。
怎么做:用 app.use() 挂载,路径用 path.join(__dirname, 'public') 拼绝对路径更稳妥。
易错点:__dirname 是当前文件所在目录;静态托管本质是中间件,会先处理请求。转写稿里访问根路径时自动返回了 index.html,中文路径也能正常处理,说明编码等工作 Express 已经封装好了。

js
const express = require('express');
const path = require('path');
const app = express();

app.use(express.static(path.join(__dirname, 'public')));

app.listen(8080, () => {
  console.log('HTTP server is running on 8080');
});

路由与静态托管结合:顺序决定谁先响应 ​

为什么需要:一个服务既要有接口路由,又要能访问静态页面。
怎么想:把静态托管和路由都注册到同一个 app 上,Express 按注册顺序依次匹配。
怎么做:先写路由,再写静态托管,或者反过来,但要知道“谁先匹配到就按谁的”。
易错点:转写稿里调试时发现,如果前面写了 app.get('/') 并直接响应,后面的静态托管就不会再处理根路径;访问不存在的路径时,Express 会给出 404。不要误以为静态托管“永远自动加载 index”,它也要受顺序影响。

js
app.get('/login', (req, res) => {
  res.send('登录页');
});

app.use(express.static(path.join(__dirname, 'public')));

字符串精准匹配:路径必须一模一样 ​

为什么需要:大多数接口路径是固定的,比如 /home、/login。
怎么想:字符串精准匹配最普遍、最简单,写什么路径就匹配什么路径。
怎么做:app.get('/home', callback),访问 /home 可以,访问 /home/ 通常也可以;但 /home/index 不行。
易错点:不要以为写了 /home 就能匹配 /home/abc;精准匹配只认那一个路径。

js
app.get('/home', (req, res) => {
  res.send('<h1>首页</h1>');
});

字符串模糊匹配:?、+、*、{} ​

为什么需要:有些路径有规律,比如 /admin 下面任意页面,没必要一个个写。
怎么想:Express 的字符串模糊匹配有点像正则的子集:? 表示前一个字符 0 次或 1 次,+ 表示前一个字符 1 次或多次,* 表示任意字符任意次,{} 把多个字符当成整体。
怎么做:例如 /admin/* 可以匹配 /admin/ 后面的任意内容。
易错点:/admin 和 /admin/ 不一样;* 很实用,但不要把模糊匹配当成万能正则。转写稿里也强调:它“类似于正则,但又不是正则”。

js
app.get('/admin/*', (req, res) => {
  res.send('admin 下的任意路径');
});

正则匹配:路径也可以写正则 ​

为什么需要:当路径规则更复杂时,字符串模糊匹配不够用。
怎么想:Express 允许直接把正则作为路径,匹配能力更强。
怎么做:正则不要加引号,例如匹配所有以 .css 结尾的路径。
易错点:正则写错会导致匹配不到;初学阶段不必深究复杂正则,知道“路径可以传正则”即可。

js
app.get(/\.css$/, (req, res) => {
  res.send('这是一个 CSS 页面');
});

带参数的 URL:/article/:id ​

为什么需要:文章详情页的 id 是变化的,不可能为每个 id 写一条路由。
怎么想:用冒号 :id 占位,Express 会把这一段作为参数取出来。
怎么做:通过 req.params.id 获取。访问 /article/123 得到 123,访问 /article/121 得到 121。
易错点:/article 不行,/article/123/456 也不行;它只匹配一段参数。

js
app.get('/article/:id', (req, res) => {
  console.log(req.params.id);
  res.send('<h1>文章</h1>');
});

路由组合:同一路径不同请求方式 ​

为什么需要:/login 的 GET 要返回登录页,POST 要处理表单提交,路径相同、请求方式不同。
怎么想:用 app.route('/login') 把同一路径的路由组合起来,再链式调用 .get()、.post()。
怎么做:GET 用 res.sendFile() 返回页面,POST 返回提交成功。
易错点:sendFile 里不要用相对路径,转写稿里强调要用绝对路径拼接;链式调用时注意每个回调都要有响应或交给下一个处理。

js
app.route('/login')
  .get((req, res) => {
    res.sendFile(path.join(__dirname, 'pages', 'login.html'));
  })
  .post((req, res) => {
    res.send('表单提交成功');
  });

多个回调函数与 next ​

为什么需要:有时一个路由要先做前置处理,比如打印日志、校验数据,再执行真正响应。
怎么想:一个路由可以设置多个回调函数,匹配成功后按顺序执行;调用 next() 才会进入下一个。
怎么做:第一个回调只做前置处理,最后再 res.send()。
易错点:转写稿调试时先在第一个回调里 res.send(),再 next(),结果报错——响应已经结束,不能再设置响应。第一个回调不要提前结束响应。

js
app.get('/product/index', (req, res, next) => {
  console.log('第一个函数:前置处理');
  next();
}, (req, res) => {
  res.send('product 首页');
});

app.all 定制 404 页面 ​

为什么需要:用户访问不存在的路径时,应该给一个统一的 404 页面。
怎么想:app.all 可以匹配所有请求方式,把它放在所有路由最后,前面都没匹配到才会到这里。
怎么做:用 res.status(404).send() 设置状态码并返回内容。
易错点:app.all('*') 一定要放在最后;如果放在前面,会把正常路由也拦截掉。

js
app.all('*', (req, res) => {
  res.status(404).send('<h1>404 页面不存在</h1>');
});

总结:Express 路由比手写 switch 方便得多,既能精准匹配,也能模糊匹配、正则匹配,还能取 URL 参数、组合同一路径的不同请求方式、设置多个回调,最后用 app.all 兜底 404。关键要记住:路由按注册顺序匹配,谁先匹配到就执行谁。

先分清请求对象和响应对象 ​

Express 里每个路由回调都能拿到 req 和 res:req 是请求对象,负责“读”客户端传来的信息;res 是响应对象,负责“写”回给浏览器的内容。为什么需要先分清?因为静态托管、路由匹配、参数读取、404 处理,最后都落在“怎么从 req 取数据、怎么用 res 返回结果”上。怎么想:req 只读,res 只写;一个请求进入后,先由路由或中间件判断,再由 res.send、res.sendFile、res.status 结束响应。怎么做:app.get('/home', (req, res) => { res.send('<h1>首页</h1>') })。易错点:res.send 一旦发出并结束响应,后面不能再写响应;需要继续交给下一个回调时,要用 next(),而不是提前 send。

静态资源托管:让 express.static 接管请求 ​

为什么需要?public 下的 HTML、CSS、JS、图片等静态文件很多,不可能每个文件都写一条路由。怎么想?以目录为单位托管,express.static 本质是中间件,请求先经过它,能匹配到文件就直接响应。怎么做:

js
const express = require('express');
const path = require('path');
const app = express();

app.use(express.static(path.resolve(__dirname, 'public')));

app.listen(8080, () => {
  console.log('HTTP server is running on 8080');
});

运行 node 03.js 后访问根路径,Express 会自动找到 public/index.html;中文路径也能自动处理。易错点:静态目录建议用 path.resolve(__dirname, 'public') 拼绝对路径;静态托管和后面的路由有顺序,谁先匹配到就谁处理。

路由匹配:请求方式 + 路径都要对 ​

为什么需要?静态托管只能处理文件,业务路径如 /login 需要自己写路由。怎么想?app.get、app.post 的方法名就是请求方式,第一个参数是路径,第二个是回调。怎么做:

js
app.get('/home', (req, res) => {
  res.send('<h1>首页</h1>');
});

/home 能匹配,/home/ 也可以;但 /home/index 不行。易错点:精准匹配不是前缀匹配;请求方式也必须一致,GET 路由不会响应 POST。

字符串模糊匹配与正则匹配 ​

为什么需要?有些路径有规律,比如 /admin/任意内容、以 .css 结尾的资源。怎么想?字符串模糊匹配里,? 表示前一个字符 0 或 1 次,+ 表示 1 次以上,* 表示任意字符任意次,{} 可把多个字符当整体;正则则直接写 /.../。怎么做:

js
app.get('/admin/*', (req, res) => {
  res.send('admin 页面');
});

app.get(/\.css$/, (req, res) => {
  res.send('这是一个快乐的页面');
});

易错点:* 和正则里的 * 不是一回事;正则不要加引号;/admin 与 /admin/ 的匹配要实际测试,不要凭感觉。

带参数的 URL:用 req.params 取动态值 ​

为什么需要?文章详情这类路径 /article/123 的 ID 是变化的。怎么想?用 :id 占位,匹配后通过 req.params.id 读取。怎么做:

js
app.get('/article/:id', (req, res) => {
  console.log(req.params.id);
  res.send('文章');
});

访问 /article/123 得到 123,访问 /article 不匹配,/article/123/456 也不匹配。易错点:参数值是字符串;路径层级要一致;它和查询字符串不是一回事。

路由组合与多回调:app.route、next ​

为什么需要?同一路径可能既有 GET 又有 POST;多个回调可把前置处理拆出来复用。怎么想?app.route('/login').get(...).post(...) 把同路径不同方法组合;多回调按顺序执行,必须调用 next() 才继续。怎么做:

js
app.route('/login')
  .get((req, res) => {
    res.sendFile(path.resolve(__dirname, 'login.html'));
  })
  .post((req, res) => {
    res.send('表单提交成功');
  });

app.get('/product',
  (req, res, next) => {
    console.log('第一个回调');
    next();
  },
  (req, res) => {
    res.send('第二个回调');
  }
);

调试时若第一个回调先 res.send 再 next(),会报错,因为响应已结束;应把结束响应放到最后一个回调。易错点:sendFile 要用绝对路径;不调用 next(),下一个回调不会执行。

app.all 定制 404 与 res.status ​

为什么需要?前面路由都没匹配时,需要统一兜底。怎么想?app.all 匹配所有请求方式,放在最后;用 res.status(404) 设置状态码,再链式 send。怎么做:

js
app.all('*', (req, res) => {
  res.status(404).send('<h1>404 页面不存在</h1>');
});

易错点:app.all('*') 必须放在所有路由后面,否则会抢先匹配;前面能匹配的路由仍按顺序先执行。整体调试流程就是:先跑静态托管,访问根路径自动出 index.html;再访问 /login 出现 404,补上路由后 GET 返回页面、POST 提交成功;最后用 app.all 统一处理找不到的路径。

中间件是什么,为什么需要它 ​

Express 自身功能很精简,本质上是由路由和中间件构成的 Web 开发框架。可以这样理解:一个 Express 应用,就是在按顺序调用各种中间件。中间件(Middleware)是一个函数,接收请求对象 req、响应对象 res,以及请求-响应循环中的下一个中间件,通常命名为 next。

为什么需要中间件?因为很多处理并不是“到路由后一次性完成”的,比如静态资源托管、日志、前置校验、统一 404 等,都适合插在请求进入最终响应之前执行。怎么想?把请求-响应循环看成一串处理函数:当前函数可以改请求、改响应、结束响应,或调用 next() 把控制权交给下一个。怎么做?中间件的功能包括:

  • 执行任何代码。
  • 修改请求和响应对象。
  • 终结请求-响应循环。
  • 调用堆栈中的下一个中间件。

易错点:如果当前中间件没有终结请求-响应循环,就必须调用 next(),否则请求会挂起。也就是说,不 send/end 响应,又不 next(),浏览器就一直等不到结果。

从静态资源托管看中间件 ​

转写稿里用 express.static 托管 public 目录,一行代码就实现了静态资源服务:

js
const express = require('express');
const path = require('path');
const app = express();

app.use(express.static(path.join(__dirname, 'public')));

为什么需要?手写静态资源服务要判断路径、读文件、设置 Content-Type、处理 404,很繁琐。怎么想?express.static(...) 返回的其实就是一个中间件,用 app.use() 注册后,请求会先经过它:能匹配到 public 下的文件就响应,匹配不到再交给后面的路由。怎么做?把静态目录用 path.join(__dirname, 'public') 拼成绝对路径,交给 express.static。转写稿演示时直接访问根路径,Express 自动找到了 index.html;中文路径也能被自动编码处理。

易错点:app.use(express.static(...)) 要放在合适位置;如果前面已有路由先匹配并响应,后面的静态托管就不一定执行。转写稿中把路由和静态托管结合时强调“按顺序,谁先匹配到就按谁的”,这就是中间件顺序。

next() 与多个回调 ​

转写稿用“一个路由设置多个回调”来体会 next:

js
function pre(req, res, next) {
  console.log('第一个函数');
  next();
}

app.get('/product', pre, (req, res) => {
  res.send('product');
});

为什么需要?可以把前置处理抽出来复用,比如日志、权限判断、数据预处理。怎么想?一个路由匹配成功后,可以依次执行多个回调;第一个回调不结束响应,而是调用 next(),才会进入下一个。怎么做?前置函数接收 (req, res, next),在合适位置调用 next();最后一个回调负责 res.send() 结束响应。

易错点:转写稿调试时,第一个回调里已经 res.send() 又调用 next(),结果报错——响应已经结束,不能再继续交给下一个中间件。正确做法是:前置中间件只做处理并 next(),结束响应放到最后一个回调。另一个易错点是忘记 next(),后面回调不会执行。

顺序与 404:把兜底放在最后 ​

app.all 可以匹配所有请求方式。转写稿用它定制 404:

js
app.all('*', (req, res) => {
  res.status(404).send('<h1>404 页面不存在</h1>');
});

为什么需要?前面所有路由都没匹配到时,需要一个统一兜底响应,而不是让 Express 返回默认 404。怎么想?中间件和路由按注册顺序执行,兜底必须放在最后;前面的路由都没命中,才轮到它。怎么做?用 app.all('*', ...) 或合适的兜底中间件,设置状态码 404 并发送页面。

易错点:404 兜底不要写在前面,否则会抢先匹配;res.status(404) 后仍要发送响应内容;如果前面已经结束响应,再进入兜底会出错。整体记住:Express 应用由中间件和路由组成,顺序决定谁先处理,next() 决定是否继续,响应结束就不要再次响应。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:静态资源托管 + 路由结合 ​

js
// 03-静态资源托管服务.js
const express = require('express');
const path = require('path');

const app = express();

// 一行代码实现静态资源文件托管:托管 public 目录下的所有 HTML/CSS/JS 等文件
app.use(express.static(path.resolve(__dirname, 'public')));

// 路由和静态资源托管结合
app.get('/login', (req, res) => {
  res.send('<h1>登录页面</h1>');
});

app.post('/login', (req, res) => {
  res.send('表单提交成功');
});

app.listen(8080, () => {
  console.log('HTTP server is running on 8080');
});

关键行与易错点

  • app.use(express.static(...)) 就是中间件:请求在到达后面路由之前,先在这里过一遍,具备静态文件托管能力;原理和手写判断 URL 一样,只是 Express 帮你封装了。
  • 目录一定要用 path.resolve(__dirname, 'public') 拼成绝对路径;Express 的响应方法不允许使用相对路径。
  • 访问根路径 / 时它会自动找 index.html,中文路径也自动做了编码,都不用自己操心。
  • 顺序很关键:路由和静态托管谁先匹配到就按谁处理。/ 与 index.html 会互相“抢”,写 app.get('/') 时会先命中它。
  • 中间件如果没有终结请求-响应循环,就必须调用 next(),否则请求会挂起。

示例二:路由的三种匹配方式 + 带参数的 URL ​

js
// index.js 路由匹配方式
const express = require('express');

const app = express();

// 1 字符串精准匹配
app.get('/home', (req, res) => {
  res.send('<h1>home 页面</h1>');
});

// 2 字符串模糊匹配:* 表示任意字符任意次
app.get('/admin/*', (req, res) => {
  console.log('模糊匹配到 admin 下的路径');
  res.send('<h1>admin 页面</h1>');
});

// 2 字符串模糊匹配:? 表示前面字符 0 次或 1 次
app.get('/index.htm?', (req, res) => {
  res.send('<h1>首页</h1>');
});

// 3 正则匹配:只要以 .css 结尾即可
app.get(/\.css$/, (req, res) => {
  res.send('<h1>这是一个快乐的页面</h1>');
});

// 匹配带有参数的 URL
app.get('/article/:id', (req, res) => {
  console.log(req.params.id); // /article/123 → 输出 123
  res.send('<h1>文章</h1>');
});

app.listen(8080, () => {
  console.log('HTTP server is running on 8080');
});

关键行与易错点

  • 精准匹配:/home 只能匹配 /home(结尾多一个斜杠没事),/home/index 匹配不到。
  • 模糊匹配三个符号:? 前面字符 0 或 1 次;+ 前面字符 1 次或多次(不能 0 次);* 任意字符任意次(和正则里的 * 不一样);() 把多个字符作为一个整体,如 (\.html)?。
  • /admin/* 中的斜杠必须实实在在写上,Express 会自动去掉路径结尾的斜杠,所以 /admin 匹配不到,/admin/ 才行。
  • 正则匹配直接写 /\.css$/,不加引号,$ 表示以 .css 结尾,前面写什么都可以。
  • /article/:id 里冒号表示参数,用 req.params.id 取;/article 和 /article/123/456 都不匹配。

示例三:路由组合 + 多个回调 + all 定制 404 ​

js
// index.js 路由组合、多回调、404
const express = require('express');
const path = require('path');

const app = express();

// 路由组合:同一路径的不同请求方式写在一起
app.route('/login')
  .get((req, res) => {
    res.sendFile(path.resolve(__dirname, 'pages/login.html'));
  })
  .post((req, res) => {
    res.send('表单提交成功');
  });

// 多个回调函数:第一个作为前置处理,必须调用 next()
app.get('/product/index', (req, res, next) => {
  console.log('第一个回调函数');
  next(); // 不调用 next(),后面的回调不会执行
}, (req, res) => {
  res.send('<h1>product 首页</h1>');
});

// all 匹配所有请求方式,用来定制 404,必须写在最后
app.all('*', (req, res) => {
  res.status(404).send('<h1>404 页面不存在</h1>');
});

app.listen(8080, () => {
  console.log('HTTP server is running on 8080');
});

关键行与易错点

  • app.route('/login') 把同一路径的 get、post 组合在一起链式调用,少写路径、可读性高,“打眼一看就知道你俩是一对儿”。
  • 一个路由可以设多个回调,匹配成功后按顺序都会执行,但前一个必须调 next()。
  • 最常见的报错:第一个回调里已经 res.send() 结束了响应,再调 next() 会报错——第一个回调只做前置处理(如打印、改数据),结束响应的事放到最后一个回调。
  • 第一个回调可以写成具名函数,作为可复用的“前置操作”给多个路由用。
  • app.all('*') 匹配所有请求方式,用于兜底 404,一定写在所有路由最后;res.status(404) 可以链式接 .send()。
  • 再次提醒顺序问题:/admin/* 若写在 /admin/index 之前,会先把 /admin/index 的请求抢走。

常见错误 ​

  1. 换个目录启动后静态资源全 404 现象:本地 node app.js 正常,用 pm2 或在别的目录启动后 public 里的 css/js/图片全部 404。 原因:express.static('public') 的相对路径按进程工作目录解析,不是按 app.js 所在目录解析。 修法:一律写绝对路径 app.use(express.static(path.join(__dirname, 'public')))。

  2. 多个回调里第一个就 res.send() 再 next() 现象:控制台抛 ERR_HTTP_HEADERS_SENT: Cannot set headers after they are sent。 原因:响应已经终结,第二个回调还在往响应里写东西。 修法:前置回调只做数据加工/日志/校验,末尾 next();响应只放在最后一个回调里发。

  3. 请求一直挂起,浏览器转圈没有任何返回 现象:Network 面板该请求一直 pending,服务端不报错。 原因:中间件或回调既没终结响应、也没调 next()(多出现在 if 分支里漏写)。 修法:逐条分支检查,每条出口要么 res.xxx(),要么 next()。

  4. 404 兜底把正常请求也拦截了 现象:/login、静态资源全都返回“404 页面不存在”。 原因:app.all('*') 写在了路由和静态托管前面,Express 先命中先执行。 修法:兜底路由永远放最后一行。

  5. 带参数的 URL 取不到值 现象:/article/123 能进回调,但打印出来是 undefined。 原因:用成了 req.query.id,或路由路径里漏写 :id。 修法:app.get('/article/:id') 配 req.params.id;只有 ?id=123 这种查询串才用 req.query.id。

  6. 正则路由写成了字符串 现象:app.get('/.*\.css$/') 匹配不到任何 css 请求。 原因:加了引号就退化成字符串匹配,不再是正则。 修法:直接写正则字面量 app.get(/.*\.css$/, ...),不加引号。

  7. res.sendFile 报路径错误 现象:TypeError: path must be absolute。 原因:sendFile 不接受相对路径。 修法:res.sendFile(path.join(__dirname, 'views/login.html'))。

练习 ​

  1. 顺序实验:写一个同时包含「静态托管 + /login 路由 + app.all('*') 404」的应用。先把 404 那行移到最前面,访问 /login 和 index.html,记录现象;再移回最后,对比结果,用一句话写出你的结论。

  2. 三种匹配对照表:只用一个 index.js,分别用精确 /admin、模糊 /admin/*、参数 /admin/:id 写三条路由(各自返回不同文字)。然后手工访问至少 6 个不同 URL(含 /admin、/admin/、/admin/1、/admin/a/b、/admin/index.html),填一张“URL → 命中哪条路由”的表,标出和你预判不一致的项。

  3. 路由组合改造:用 app.route('/login') 把原来的 GET(返回登录页 HTML,用 sendFile + 绝对路径)和 POST(返回“表单提交成功”)合并成链式写法,跑通表单提交。

  4. 多回调与 next:给 /product/index 配两个回调,第一个打印时间戳并 next(),第二个返回响应。然后再故意在第一个回调里加 res.send('x') 并保留 next(),把报错信息完整抄下来,说明为什么不允许。

  5. 自定义 404:实现兜底 404,要求返回状态码 404 + 一段 HTML。用 curl -i http://localhost:8080/不存在的路径 验证响应头里的状态行,并对比“只写 res.send 不设状态码”时的差别。

原视频对照 ​

#视频时长
1express演示-实现静态文件资源托管05:24
2express演示-路由和静态文件资源托管结合03:07
3express路由-路由的匹配方式16:42
4express路由-匹配带有参数的URL03:23
5express路由-路由组合04:25
6express路由-设置多个回调函数07:00
7express路由-使用all方法定制404页面03:08

小结 ​

  1. Express 的路由匹配是自上而下、先命中先执行,代码顺序就是优先级,兜底永远放最后。
  2. express.static 是中间件不是路由,一行代码替代了手写 MIME 判断、编码处理、目录遍历的全部工作量。
  3. req.params 和 req.query 是两条独立的取数通道,路径占位符和查询串别混用。
  4. 多回调的设计意图是把前置逻辑抽成可复用函数,不是把响应拆成两半——响应只能有一次。
  5. 中间件和路由回调的契约只有两条:要么终结响应,要么 next(),没有第三条路。
  6. 匹配方式的选择顺序应是:精确 > 模糊 > 正则,能用简单的就不用复杂的,可读性优先于炫技。
  7. 这一阶段的目标是把「请求进来 → 匹配 → 响应出去」的链路走通,不是造生产级后端框架,够用即可。