前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

nodejs系列之express中间件机制与路由用法详解

首页2018-12-23 23:02:10Back-End
NodeExpress中间件

接手过一个 Node 服务,路由文件八百多行,全塞在 app.js 里,改一个接口得先滚一遍屏幕找位置。翻代码的时候发现一个更要命的问题,有个鉴权中间件被写在了路由后面,实际上一次都没执行过,接口是裸奔的状态。这类问题的根子不在业务代码写得糙,而在于对 Express 的执行顺序没搞明白。

这篇把 Express 从最底下的 http 模块开始捋一遍,讲清楚中间件到底是什么、use 和 get 这些方法的执行顺序凭什么是那样、路由怎么用 Express.Router 拆出去,最后补一段 2018 年之后 Express 变过的写法。读完你应该能自己判断一个中间件该放在哪一行。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • Express 相对原生 http 模块到底包了一层什么
  • 中间件的定义、next 的两种用法与错误传递机制
  • use 注册的中间件为什么严格按注册顺序执行
  • all 与 HTTP 动词方法的区别,路径的模式匹配怎么写
  • response 与 request 上最常用的那几个属性和方法
  • 用 hbs 模板引擎从静态页面过渡到动态渲染的完整例子
  • Express.Router 拆路由、router.param 处理路径参数、app.route 链式写法
  • multer 处理文件上传的最小可用配置
  • 这篇里哪些写法在 Express 4 / 5 上已经不成立了

# 一、Express 到底包了一层什么

官网在 expressjs.com,中文文档在 expressjs.com.cn。Express 是基于 Node.js 的 Web 开发框架,用它能比较快地搭出一个功能完整的网站。

想要一个现成的骨架,装应用生成器 express-generator 就行。

$ npm install express-generator -g
@前端进阶之旅: 代码已经复制到剪贴板

生成出来的目录结构是 bin/www 加 app.js 加 routes/,这套结构现在看有点老,但它把入口、应用配置、路由三块分开的思路是对的,后面第十节讲 Router 的时候你会看到一样的分法。

先说结论,Express 的核心就是对 Node 内置 http 模块的再包装。不用 Express 直接起一个服务,代码是这样。

var http = require("http");

var app = http.createServer(function(request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.end("Hello world!");
});

app.listen(3000, "localhost");
@前端进阶之旅: 代码已经复制到剪贴板

createServer 的回调只有一个,所有请求都从这一个函数进来。路径判断、参数解析、错误处理,全得你自己在这个函数里写 if。项目一大,这个函数就会变成前面说的那种八百行的怪物。

同样的功能用 Express 写是这样。

var express = require('express');
var app = express();

app.get('/', function (req, res) {
  res.send('Hello world!');
});

app.listen(3000);
@前端进阶之旅: 代码已经复制到剪贴板

差别不在代码短了几行,而在于 Express 在 http 模块之上加了一个中间层。这个中间层干的事,就是把那个巨大的回调函数拆成一串可以排队执行的小函数。

# 二、中间件就是排队处理请求的函数

中间件(middleware)就是处理 HTTP 请求的函数。它最大的特点是,一个中间件处理完,再传递给下一个中间件。App 实例在运行过程中,会调用一系列的中间件。

每个中间件从 App 实例接收三个参数,依次是 request 对象(代表 HTTP 请求)、response 对象(代表 HTTP 回应)、next 回调函数(代表下一个中间件)。每个中间件都可以对 request 对象进行加工,并且决定是否调用 next 方法,把 request 对象再传给下一个中间件。

一个什么都不干、只负责往下传的中间件长这样。

function uselessMiddleware(req, res, next) {
  next();
}
@前端进阶之旅: 代码已经复制到剪贴板

这里有个坑要注意,next 带不带参数是两件完全不同的事。不带参数就是正常往下走;带上参数就代表抛出一个错误,参数是错误文本。

function uselessMiddleware(req, res, next) {
  next('出错了!');
}
@前端进阶之旅: 代码已经复制到剪贴板

抛出错误以后,后面的普通中间件全部跳过,Express 会一路找到第一个错误处理函数为止。错误处理函数的签名是四个参数 (err, req, res, next),Express 靠形参个数来区分它和普通中间件,少写一个参数它就不认了。这个我踩过,错误中间件写成三个参数,结果错误一路穿透到默认处理器,返回一个带堆栈的 HTML 页面直接甩给了前端。

还有一件事不能忘,调用了 next() 之后如果又调用了 res.send(),或者反过来,就会撞上 Cannot set headers after they are sent 这个报错。一个请求的生命周期里,响应只能结束一次。

# 三、use 方法与最原始的路由

use 是 Express 注册中间件的方法,它返回一个函数。下面是连续调用两个中间件的例子。

var express = require("express");
var http = require("http");

var app = express();

app.use(function(request, response, next) {
  console.log("In comes a " + request.method + " to " + request.url);
  next();
});

app.use(function(request, response) {
  response.writeHead(200, { "Content-Type": "text/plain" });
  response.end("Hello world!\n");
});

http.createServer(app).listen(1337);
@前端进阶之旅: 代码已经复制到剪贴板

收到 HTTP 请求后先调第一个中间件,在控制台输出一行信息,然后通过 next 把执行权交给第二个中间件,输出 HTTP 回应。第二个中间件没有调 next,request 对象就不再往后传了。

顺着上面聊,use 内部可以对访问路径做判断,靠这个就能实现最朴素的路由,根据不同的请求网址返回不同的内容。

var express = require("express");
var http = require("http");

var app = express();

app.use(function(request, response, next) {
  if (request.url == "/") {
    response.writeHead(200, { "Content-Type": "text/plain" });
    response.end("Welcome to the homepage!\n");
  } else {
    next();
  }
});

app.use(function(request, response, next) {
  if (request.url == "/about") {
    response.writeHead(200, { "Content-Type": "text/plain" });
    response.end("Welcome to the about page!\n");
  } else {
    next();
  }
});

app.use(function(request, response) {
  response.writeHead(404, { "Content-Type": "text/plain" });
  response.end("404 error!\n");
});

http.createServer(app).listen(1337);
@前端进阶之旅: 代码已经复制到剪贴板

这段通过 request.url 判断请求的网址,从而返回不同的内容。app.use 一共登记了三个中间件,只要请求路径匹配上了,就不会把执行权交给下一个。所以最后一个中间件承担了兜底职责,前面都没匹配上,它就返回 404。

顺便说一句,原文这个例子的第二个中间件里只写了 writeHead 没写 response.end,请求会一直挂着直到超时。上面这版我把 end 补上了。这种漏掉 end 的 bug 在真实项目里很常见,表现是接口一直 pending,前端还以为是网络慢。

除了在回调函数内部判断请求的网址,use 也允许把请求网址写在第一个参数上。只有请求路径匹配这个参数,后面的中间件才会生效。这样写更清晰。

// 只对根目录的请求,调用某个中间件
app.use('/path', someMiddleware);
@前端进阶之旅: 代码已经复制到剪贴板

于是上面那段可以改写成下面的样子。

var express = require("express");
var http = require("http");

var app = express();

app.use("/home", function(request, response, next) {
  response.writeHead(200, { "Content-Type": "text/plain" });
  response.end("Welcome to the homepage!\n");
});

app.use("/about", function(request, response, next) {
  response.writeHead(200, { "Content-Type": "text/plain" });
  response.end("Welcome to the about page!\n");
});

app.use(function(request, response) {
  response.writeHead(404, { "Content-Type": "text/plain" });
  response.end("404 error!\n");
});

http.createServer(app).listen(1337)
@前端进阶之旅: 代码已经复制到剪贴板

原文这段的第一行是 ar express = require("express"),少了个 v,照抄会直接报语法错误,这里改对了。

有一点必须记牢,use 的路径参数是前缀匹配。写 app.use('/home', fn) 的话,/home/detail 也会命中。这和后面 app.get('/home', fn) 的精确匹配是两种行为,混着用很容易出现「为什么这个中间件在子路径上也跑了」的困惑。

# 四、all 方法和 HTTP 动词方法

针对不同的请求,Express 提供了 use 方法的一些别名。上面那段也可以用别名的形式来写。

fe
  • 一、Express 到底包了一层什么
  • 二、中间件就是排队处理请求的函数
  • 三、use 方法与最原始的路由
  • 四、all 方法和 HTTP 动词方法
  • 五、set 方法配置应用级变量
  • 六、response 对象常用的三个方法
  • 七、request 对象上你会用到的东西
  • 八、搭建 HTTPS 服务器
  • 九、从静态页面到模板引擎
    • 9.1 准备数据
    • 9.2 写模板文件
    • 9.3 把数据接到模板上
    • 9.4 指定静态文件目录
  • 十、用 Express.Router 把路由拆出去
    • 10.1 router.route 方法
    • 10.2 router 中间件
    • 10.3 对路径参数的处理
    • 10.4 app.route
  • 十一、文件上传
  • 十二、这篇里过时的写法,现在怎么写
  • 总结
  • 参考

← shell入门,从变量语法到能上生产的脚本nodejs系列之Koa2 洋葱模型与中间件实战 →