【NodeJs-5天学习】第二天篇③ ——Express Web框架 和 中间件
面向读者群体
- ❤️ 电子物联网专业同学,想针对硬件功能构造简单的服务器,不需要学习专业的服务器开发知识 ❤️
- ❤️ 业余爱好物联网开发者,有简单技术基础,想针对硬件功能构造简单的服务器❤️
- ❤️ 本篇创建记录 2023-03-12 ❤️
- ❤️ 本篇更新记录 2023-03-12 ❤️
技术要求
- 有HTML、CSS、JavaScript基础更好,当然也没事,就直接运行实例代码学习
专栏介绍
- 通过简短5天时间的渐进式学习NodeJs,可以了解到基本的服务开发概念,同时可以学习到npm、内置核心API(FS文件系统操作、HTTP服务器、Express框架等等),最终能够完成基本的物联网web开发,而且能够部署到公网访问。
🙏 此博客均由博主单独编写,不存在任何商业团队运营,如发现错误,请留言轰炸哦!及时修正!感谢支持!🎉 欢迎关注 🔎点赞 👍收藏 ⭐️留言📝
1. 前言
在前面一篇
我们讲解了HTTP服务器相关内容,但是你会发现我们需要关注非常多的细节(比如需要人工编码干预body的解析,需要分发请求方法等等),用起来有点复杂,开发效率低。那么有没有一些更加简单快捷的方式来创建web服务器?
当然有,这就是本篇要重点讲解的
Express
框架。那我们先看看它和HTTP模块的关系。
- 问题1:
不使用Express 能否创建 Web 服务器
?
能,使用 Node.js提供的原生http 模块即可。- 问题2:
有了http 内置模块,为什么还有用 Express
?
http内置模块用起来很复杂,开发效率低;Express是基于内置的http模块进一步封装出来的,能够极大的提高开发效率。- 问题3:
http 内置模块与Express是什么关系
?
后者是基于前者进一步封装出来的。
1.1 Express简介
官方给出的概念:Express
是基于 Node.js
平台,快速、开放、极简的Web 开发框架
。
通俗的理解:Express
的作用和 Node.js 内置的http
模块类似,是专门用来创建Web 服务器的。
Express 是npm上的一个第三方包,http模块是Node内置的模块,只不过Express基于http模块之上提供了更加简单快速创建web服务器的方法。
Express 中文官方网站:
https://www.expressjs.com.cn/
习惯性,我们都要点开一下官方说明看看:
- 官方定义
- 学习内容1:
快速入门
,如何快速搭建运行服务器,并且搭载静态文件资源(html、css等等)
- 学习内容2:
指南
,主要是学习中间件(MiddleWare
),包括了路由、错误、全局、局部等等
- 学习内容3:
API手册
,对各个api进行详细介绍,目前主要是4.x版本
- 学习内容4:
最佳实践
1.2 Express能做什么
对于大前端程序员来说,最常见的两种服务器,分别是:
Web 网站服务器
:专门对外提供Web 网页资源的服务器。
典型代表:我们经常使用浏览器看到的页面信息,基本上都是Web网页资源。
API 接口服务器
:专门对外提供API 接口的服务器。
典型代表:我们平常使用App看到的信息,基本上都是通过API接口返回给到app,app拿到数据之后进行渲染显示。
2. Express 快速入门
-
① 安装
express
、body-parser
、moment
模块 -
② 导入
express
、body-parser
模块 -
③ 创建 web 服务器
-
④ 注册
中间件
,处理业务逻辑 -
⑤ 调用
app.listen
(端口号, 启动成功后的回调函数) ,启动服务器
2.1 ① 安装 express
、body-parser
、moment
模块
分别执行:
npm install express --save
express主要是构建web服务器。
npm install body-parser --save
body-parser主要是用来解析post请求体,包括json
、urlencoded
表单
npm install moment --save
moment主要是用来处理时间
。
2.2 ② 导入 express
、body-parser
模块
创建一个 web 服务器,对外提供 web 服务,需要导入 express
模块:
// 1. 导入 express
const express = require('express')
const {getIPAdress} = require('./utils/utils.js')
const time = require('./utils/time.js')
const bodyParser = require('body-parser')
- express 用来构建web服务器
- bodyParser 用来解析post请求体,包括
json
、urlencoded
表单 - time 主要是使用特定格式显示时间
const moment = require('moment');
// 获取当前时间 2022-07-31
function getCurrentDate() {
return moment().format("YYYY-MM-DD");
}
// 获取当前时间 2022-07-31 11:30:30
function getCurrentDateTime() {
return moment().format("YYYY-MM-DD HH:MM:SS");
}
// 获取当前时间
function getCurrentDateFormat(format) {
return moment().format(format);
}
// 向外导出方法
module.exports = {
getCurrentDate,
getCurrentDateFormat,
getCurrentDateTime
}
- getIPAdress用来获取本机IP地址,后面浏览器用来访问服务
const os = require('os');
// 获取本机ip
function getIPAdress() {
var interfaces = os.networkInterfaces();
for (var devName in interfaces) {
var iface = interfaces[devName];
for (var i = 0; i < iface.length; i++) {
var alias = iface[i];
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
return alias.address;
}
}
}
}
// 向外导出方法
module.exports = {
getIPAdress
}
2.3 ③ 创建 web 服务器
// 2. 创建 web 服务器
const app = express()
const port = 8266 // 端口号
const myHost = getIPAdress(); // 获取本机IP地址
这里定义了服务器的IP和端口号,后面监听客户端请求会用到。
2.4 ④ 注册中间件
,处理业务逻辑
// 3.注册中间件,处理业务逻辑
// 注意:中间件注入顺序,必须严格区分
// - 1、预处理中间件(排在最前面)
// - 2、路由中间件(中间位置,路由分为API路由和静态文件路由)
// - 3、错误处理中间件(兜底,专门用于捕获整个项目发生的异常错误,防止项目奔溃,必须注册在所有路由之后)
/*********************** 预处理中间件 *************************/
// 注入一些自定义中间件
// 定义一个最简单的中间件函数
// 常量 mw1 所指向的,就是一个中间件,这里打印请求进来的时间
const mw1 = function(req , res , next) {
console.log('这是第一个中间件函数')
var date = time.getCurrentDateTime()
console.log('请求时间:%s', date)
// 注意:在当前中间件的业务处理完毕后, 必须调用next()函数
// 表示把流转关系转交给下一个中间件或路由
next()
}
app.use(mw1)
// 解析JSON格式的请求体数据 (post请求:application/json)
app.use(bodyParser.json());
// 解析 URL-encoded 格式的请求体数据(表单 application/x-www-form-urlencoded)
app.use(bodyParser.urlencoded({ extended: true }));
/*********************** 预处理中间件 *************************/
/*********************** 路由中间件 *************************/
// 创建路由对象
const router = express.Router();
router.get('/api/test1', (req, res) => {
console.log("请求:GET /api/test1")
// 获取 URL 中携带的查询参数
console.log(req.query)
res.send("/api/test1 get OK")
})
router.post('/api/test1', (req, res) => {
console.log("请求:POST /api/test1")
// 获取 请求体 中携带的内容
console.log(req.body)
res.send("/api/test1 Post OK")
})
router.get('/api/test2', (req, res) => {
console.log("请求:GET /api/test2")
// 获取 URL 中携带的查询参数
console.log(req.query)
res.send("/api/test2 get OK")
})
router.post('/api/test2', (req, res) => {
console.log("请求:POST /api/test2")
// 获取 请求体 中携带的内容
console.log(req.body)
res.send("/api/test2 Post OK")
})
// 注入API路由中间件
app.use(router);
// app.use('/api', router) // 添加/api 访问前缀
// 注入静态路由中间件,快速托管静态资源的中间件,比如 HTML文件、图片、CSS等
app.use(express.static('web'))
// all可以匹配任何提交方式 兜底方案
app.all('*',(req,res)=>{
// 做一个其它比较友好界面 响应给浏览器
console.log('页面还没完成,请等待...')
res.send('页面还没完成,请等待...')
})
/*********************** 路由中间件 *************************/
/*********************** 错误处理中间件 *************************/
app.use((err, req, res, next) => {
console.error('出现异常:' + err.message)
res.send('Error: 服务器异常,请耐心等待!')
})
/*********************** 错误处理中间件 *************************/
这里的中间件分了几类:
- 预处理中间件(排在最前面)
一般这种中间件主要是在处理业务逻辑之前对
req
或者res
做处理,比如打印请求到来的时间、解析post请求带过来的数据。
- 路由中间件(中间位置,路由分为API路由和静态文件路由)
一般我们会在这里
处理对应请求URL
,比如路由映射以及html链接访问等等
- 错误处理中间件(兜底,专门用于捕获整个项目发生的异常错误,防止项目奔溃,必须注册在所有路由之后)
一般这里就是错误兜底,当其他中间件出现错误问题时,会在这里捕获到。
注意:
中间件一定是从上到下依序执行
,它们之间通过next
方法进行流转,部分3会详细讲解中间件。
接下来介绍一下用到的中间件。
2.4.1 预处理中间件
第一个
执行的中间件是自定义的预处理中间件,主要是打印请求进来的时间
,然后调用next方法将操作权流转给下一个中间件
// 注入一些自定义中间件
// 定义一个最简单的中间件函数
// 常量 mw1 所指向的,就是一个中间件,这里打印请求进来的时间
const mw1 = function(req , res , next) {
console.log('这是第一个中间件函数')
var date = time.getCurrentDateTime()
console.log('请求时间:%s', date)
// 注意:在当前中间件的业务处理完毕后, 必须调用next()函数
// 表示把流转关系转交给下一个中间件或路由
next()
}
app.use(mw1)
第二个
执行的中间件是第三方编写的预处理中间件,主要是解析JSON格式的请求体数据 (post请求:application/json
),内部会调用next方法将操作权流转给下一个中间件
// 解析JSON格式的请求体数据 (post请求:application/json)
app.use(bodyParser.json());
第三个
执行的中间件是第三方编写的预处理中间件,主要是解析 URL-encoded 格式的请求体数据(表单 application/x-www-form-urlencoded),内部会调用next方法将操作权流转给下一个中间件
// 解析 URL-encoded 格式的请求体数据(表单 application/x-www-form-urlencoded)
app.use(bodyParser.urlencoded({ extended: true }));
2.4.2 路由中间件
在Express中,路由
指的是客户端的请求与服务器处理函数之间的映射关系
。
Express中的路由分3 部分组成,分别是请求的类型
、请求的URL
地址、处理函数
,格式如下:
app.METHOD(PATH , HANDLER)
// METHOD 请求的类型 可以是get / post
// PATH 请求的URL地址
// HANDOD 处理函数
https://www.expressjs.com.cn/guide/routing.html
第四个
执行的中间件是API路由中间件
,主要是解析各个请求方法以及对应URL,然后响应具体对应的操作处理,内部会调用next方法将操作权流转给下一个中间件
// 创建路由对象
const router = express.Router();
router.get('/api/test1', (req, res) => {
console.log("请求:GET /api/test1")
// 获取 URL 中携带的查询参数
console.log(req.query)
res.send("/api/test1 get OK")
})
router.post('/api/test1', (req, res) => {
console.log("请求:POST /api/test1")
// 获取 请求体 中携带的内容
console.log(req.body)
res.send("/api/test1 Post OK")
})
router.get('/api/test2', (req, res) => {
console.log("请求:GET /api/test2")
// 获取 URL 中携带的查询参数
console.log(req.query)
res.send("/api/test2 get OK")
})
router.post('/api/test2', (req, res) => {
console.log("请求:POST /api/test2")
// 获取 请求体 中携带的内容
console.log(req.body)
res.send("/api/test2 Post OK")
})
// 注入API路由中间件
app.use(router);
// app.use('/api', router) // 添加/api 访问前缀
第五个
执行的中间件是静态文件路由中间件
,主要是提供外界可以访问本地文件服务器(需要指定一个文件夹目录,可以用于存放web项目),一般都是用于html、css、js等等文件组成的web页面。内部会调用next方法将操作权流转给下一个中间件
// 注入静态路由中间件,快速托管静态资源的中间件,比如 HTML文件、图片、CSS等
app.use(express.static('web'))
- 假设上面两个路由中间件都没有命中,会继续执行第六个
API路由中间件
。这里我们作为兜底处理,所以无法匹配的请求方法和请求URL都执行这个,一般多是用于提供一个友好页面给到用户。
// all可以匹配任何提交方式 兜底方案
app.all('*',(req,res)=>{
// 做一个其它比较友好界面 响应给浏览器
console.log('页面还没完成,请等待...')
res.send('页面还没完成,请等待...')
})
注意:
- 路由中间件只会命中其中一个,不会依序执行。
在路由中间件中,我们需要注意一些知识点。
2.4.2.1 监听GET 请求 app.get()
通过 app.get()
方法,可以监听客户端的 GET
请求,具体的语法格式如下:
// 参数1: 客户端请求的 URL 地址
// 参数2: 请求对应的处理函数
// req:请求对象(包含了与请求相关的属性与方法)
// res:响应对象(包含了与响应相关的属性与方法)
app.get('请求路径URL' , function(req , res) {/*处理函数*/})
app.get('请求路径URL' , (req , res) => {/*处理函数*/}) // 利用箭头函数
2.4.2.2 监听 POST 请求 app.post()
通过 app.post()
方法,可以监听客户端的POST
请求,具体的语法格式如下:
// 参数1: 客户端请求的 URL 地址
// 参数2: 请求对应的处理函数
// req:请求对象(包含了与请求相关的属性与方法)
// res:响应对象(包含了与响应相关的属性与方法)
app.post('请求路径URL' , function(req , res) {/*处理函数*/})
app.post('请求路径URL' , (req , res) => {/*处理函数*/}) // 利用箭头函数
2.4.2.3 监听 所有 请求 app.all()
通过 app.all() 方法,可以监听客户端的任意请求,具体的语法格式如下:
// 参数1: 客户端请求的 URL 地址
// 参数2: 请求对应的处理函数
// req:请求对象(包含了与请求相关的属性与方法)
// res:响应对象(包含了与响应相关的属性与方法)
app.post('*' , function(req , res) {/*处理函数*/})
app.post('*' , (req , res) => {/*处理函数*/}) // 利用箭头函数
这里使用到了正则匹配表达式。
注意:
不管是GET、POST、PUT、DELETE还是all,它们的路径均可以使用正则匹配表达式。比如:
'/ab?cd'
会匹配到acd
和abcd
'/ab+cd'
会匹配到abcd
,abbcd
,abbbcd
'/ab*cd'
会匹配到abcd
,abxcd
,abRANDOMcd
,ab123cd
'/ab(cd)?e'
会匹配到/abe
and/abcde
.
2.4.2.4 把内容响应给客户端 res.send()
通过 res.send()
方法,可以把处理好的内容,发送给客户端:
app.get('/user' , (req , res) => {
// 调用express 提供的 res.send() 方法 , 向客户端响应(发送)一个 JSON 对象
res.send({name: 'zs' , age : 20 , gender: '男'})
})
app.post('/user' , (req , res) => {
// 调用express 提供的 res.send() 方法 , 向客户端响应(发送)一个 文本字符串
res.send('请求成功')
})
2.4.2.5 获取 URL 中携带的查询参数 req.query
通过 req.query
对象,可以访问到客户端通过查询字符串
(queryString HTTP)的形式,发送到服务器的参数:
app.get('/' , (req, res) => {
// req.query 默认是一个空对象
// 客户端使用 ?name=zs&age=20 这种查询字符串形式 , 发送到服务器的参数,
// 可以通过req.query 对象访问到,例如:
// req.query.name req.query.age
console.log(req.query)
})
2.4.2.6 获取 URL 中的动态参数 req.params
通过 req.params
对象,可以访问到 URL 中,通过 : 匹配到的动态参数:
// URL 地址中,可以通过 :参数名 的形式 , 匹配动态参数值
// 注意 : 这里的 :id 是一个动态的参数
app.get('/user/:id' , (req , res) => {
// req.params 默认是一个空对象
// 里面存放着通过 : 动态匹配到的参数值
console.log(req.params)
})
具体案例:
// 动态参数可以是多个 例如:/user/:id/:name
//: 后面的值可以随便写(只要合理) 例如: /user/:ids
// 动态参数的个数要保持一致 ,不然会报错
http://127.0.0.1/user/1/zs/20 // 这时会报错
app.get('/user/:id/name' , (req , res) => {
console.log(req.params)
})
// 动态参数的顺序可以调换,对应的参数也会改变
// 第一次 http://127.0.0.1/user/1/zs
app.get('/user/:id/name' , (req , res) => {
console.log(req.params) // id:1 , name:zs
})
// 第二次 http://127.0.0.1/user/zs/1
app.get('/user/:id/name' , (req , res) => {
console.log(req.params) // id:zs , name:1
})
2.4.2.7 req.body、req.query、req.params区别
- req.query:
// 注册中间件
app.use(express.urlencoede({extended:false}))
// 以?传递的参数都是get形式 接收的时候使用req.query
app.post('/login' ,(req , res) => {
console.log(req.query)
})
- req.body
app.use(expres.json())
// req.body 取到的是post请求
app.post('/login' ,(req , res) => {
console.log(req.body)
})
- req.params
// URL 地址中,可以通过 :参数名 的形式 , 匹配动态参数值
// 注意 : 这里的 :id 是一个动态的参数
app.get('/user/:id' , (req , res) => {
// req.params 默认是一个空对象
// 里面存放着通过 : 动态匹配到的参数值
console.log(req.params)
})
2.4.2.8 express.static()静态路由
express 提供了一个非常好用的函数,叫做 express.static()。通过它,我们可以非常方便地创建一个静态资源服务器, 例如,通过如下代码就可以将public目录下的图片、CSS文件、JavaScript文件对外开放访问了:
app.use(express.static('静态资源目录名'))
// 例如:
app.use(express.static('public'))
现在,你就可以访问public目录中的所有文件了:
- http://localhost:8266/images/bg.jpg
- http://localhost:8266/css/style.css
- http://localhost:8266/js/login.js
注意:
- Express在指定的静态目录中查找文件,并对外提供资源的访问路径。 因此,存放静态文件的目录名不会出现在URL 中。托管多个静态资源目录,请多次调用express.static() 函数:
app.use(express.static('public'))
app.use(express.static('files'))
访问静态资源文件时,express.static()函数会根据目录的添加顺序查找
所需的文件。
如果希望在托管的静态资源访问路径之前,挂载路径前缀,则可以使用如下的方式:
app.use("路径前缀",express.static('目录名'))
// 前缀可以随便写
// 例如
app.use("/abc",express.static('public'))
// 建议前缀 和 目录名一致
// 例如:
app.use("/public",express.static('public'))
现在,你就可以通过带有/public
前缀地址访问public目录中的所有文件了:
- http://localhost:8266/public/images/bg.jpg
- http://localhost:8266/public/css/style.css
- http://localhost:8266/public/js/login.js
2.4.2.9 为API路由模块添加前缀
类似于托管静态资源时,为静态资源统一挂载访问前缀一样,路由模块添加前缀的方式也非常简单:
//服务器 文件名: 02.模块化路由.js
const express = require('express')
// 创建 Web 服务器 , 命名为 app
const app = express()
// 1. 导入路由模块
const router = require('./03.router')
// 2. 使用 app.use() 注册路由模块 , 并添加统一的访问前缀 /api
//参考 app.use('/files', express.static('./files'))
app.use('/api', router)
// 注意: app.use() 函数的作用,就是来注册全局中间件
app.listen(80 , (req , res) => {
console.log('http:127.0.0.1')
})
2.4.2.10 路由的匹配过程
每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数。
在匹配时,会按照路由的顺序进行匹配,如果请求类型和请求的URL 同时匹配成功,则Express 会将这次请求,转 交给对应的function函数进行处理。
路由匹配的注意点:
- ① 按照定义的先后顺序进行匹配
- ② 请求类型和请求的URL同时匹配成功, 才会调用对应的处理函数
2.4.3 错误处理中间件
- 当前面的中间件出现异常问题时,为了让系统还能继续正常运行,一般需要对错误进行处理,这也是为什么错误处理中间件放在最后的原因。
第七个
执行的中间件就是我们的错误处理中间件。
/*********************** 错误处理中间件 *************************/
app.use((err, req, res, next) => {
console.error('出现异常:' + err.message)
res.send('Error: 服务器异常,请耐心等待!')
})
/*********************** 错误处理中间件 *************************/
2.4.4 中间件的使用注意事项
- ① 一定要在
路由中间件
之前注册预处理中间件
- ② 客户端发送过来的请求,可以连续调用
多个中间件
进行处理 - ③ 执行完中间件的业务代码之后,不要忘记调用
next()
函数进行转移 - ④ 为了防止代码逻辑混乱,调用next()函数后不要再写额外的代码
- ⑤ 连续调用多个中间件时,多个中间件之间,
共享 req 和 res对象
2.5 ⑤ 调用 app.listen
(端口号, 启动成功后的回调函数) ,启动服务器
// 4.调用 app.listen(端口号, 启动成功后的回调函数) ,启动服务器
app.listen(port, () => {
console.log("express 服务器启动成功 http://"+ myHost +":" + port);
})
这里就会开始启动服务器,并且使用上面定义的端口号进行监听(至于端口号有啥用请看上一篇)。
2.6 测试效果
2.6.1 完整代码
// 1. 导入 express
const express = require('express')
const {getIPAdress} = require('./utils/utils.js')
const time = require('./utils/time.js')
const bodyParser = require('body-parser')
// 2. 创建 web 服务器
const app = express()
const port = 8266 // 端口号
const myHost = getIPAdress(); // 获取本机IP地址
// 3.注册中间件,处理业务逻辑
// 注意:中间件注入顺序,必须严格区分
// - 1、预处理中间件(排在最前面)
// - 2、路由中间件(中间位置,路由分为API路由和静态文件路由)
// - 3、错误处理中间件(兜底,专门用于捕获整个项目发生的异常错误,防止项目奔溃,必须注册在所有路由之后)
/*********************** 预处理中间件 *************************/
// 注入一些自定义中间件
// 定义一个最简单的中间件函数
// 常量 mw1 所指向的,就是一个中间件,这里打印请求进来的时间
const mw1 = function(req , res , next) {
console.log('这是第一个中间件函数')
var date = time.getCurrentDateTime()
console.log('请求时间:%s', date)
// 注意:在当前中间件的业务处理完毕后, 必须调用next()函数
// 表示把流转关系转交给下一个中间件或路由
next()
}
app.use(mw1)
// 解析JSON格式的请求体数据 (post请求:application/json)
app.use(bodyParser.json());
// 解析 URL-encoded 格式的请求体数据(表单 application/x-www-form-urlencoded)
app.use(bodyParser.urlencoded({ extended: true }));
/*********************** 预处理中间件 *************************/
/*********************** 路由中间件 *************************/
// 创建路由对象
const router = express.Router();
router.get('/api/test1', (req, res) => {
console.log("请求:GET /api/test1")
// 获取 URL 中携带的查询参数
console.log(req.query)
res.send("/api/test1 get OK")
})
router.post('/api/test1', (req, res) => {
console.log("请求:POST /api/test1")
// 获取 请求体 中携带的内容
console.log(req.body)
res.send("/api/test1 Post OK")
})
router.get('/api/test2', (req, res) => {
console.log("请求:GET /api/test2")
// 获取 URL 中携带的查询参数
console.log(req.query)
res.send("/api/test2 get OK")
})
router.post('/api/test2', (req, res) => {
console.log("请求:POST /api/test2")
// 获取 请求体 中携带的内容
console.log(req.body)
res.send("/api/test2 Post OK")
})
// 注入API路由中间件
app.use(router);
// app.use('/api', router) // 添加/api 访问前缀
// 注入静态路由中间件,快速托管静态资源的中间件,比如 HTML文件、图片、CSS等
app.use(express.static('web'))
// all可以匹配任何提交方式 兜底方案
app.all('*',(req,res)=>{
// 做一个其它比较友好界面 响应给浏览器
console.log('页面还没完成,请等待...')
res.send('页面还没完成,请等待...')
})
/*********************** 路由中间件 *************************/
/*********************** 错误处理中间件 *************************/
app.use((err, req, res, next) => {
console.error('出现异常:' + err.message)
res.send('Error: 服务器异常,请耐心等待!')
})
/*********************** 错误处理中间件 *************************/
// 4.调用 app.listen(端口号, 启动成功后的回调函数) ,启动服务器
app.listen(port, () => {
console.log("express 服务器启动成功 http://"+ myHost +":" + port);
})
2.6.2 测试效果
先把服务器启动起来。
接下来我们使用Apifox进行模拟测试。
2.6.2.1 测试 POST http://ip地址:8266/api/test2,请求体是JSON
Apifox执行:
vscode打印结果:
可以看到打印了请求时间以及请求体JSON
内容。
2.6.2.2 测试 POST http://ip地址:8266/api/test1,请求体是urlencoded
Apifox执行:
vscode打印结果:
可以看到打印了请求时间以及请求体urlencoded
内容。
2.6.2.3 测试 GET http://ip地址:8266/api/test1
浏览器输入:
http://ip地址:8266/api/test1
vscode打印结果:
2.6.2.4 测试 GET http://ip地址:8266/api/test3
这里测试一个不存在的URL,浏览器输入:
http://ip地址:8266/api/test3
vscode打印结果:
2.6.2.5 测试web页面,GET http://ip地址:8266/love.html
浏览器输入:
http://ip地址:8266/love.html
这个web项目是博主从网上找到的一个表白效果。读者也可以放入其他的
vscode打印结果:
2.6.2.6 测试 中间件异常
在预处理中间件上加入一个主动抛出异常(除以0)的中间件。
const mw2 = function(req , res , next) {
console.log('这是第二个中间件函数,主动抛出异常')
throw new Error('主动抛出异常!!!!!!!!!!!!!!!!!!!!')
// 注意:在当前中间件的业务处理完毕后, 必须调用next()函数
// 表示把流转关系转交给下一个中间件或路由
next()
}
app.use(mw2)
同样执行 2.6.2.4的操作看看。
3. Express常见中间件
Express官方把常见的中间件,根据功能分成了5 大类,分别是:
- ①
应用
级别的中间件 - ②
路由
级别的中间件 - ③
错误
级别的中间件 - ④
Express
内置的中间件 - ⑤
第三方
的中间件
根据作用范围又区分为两类:
- ① 全局中间件
- ② 局部中间件
3.1 功能分类
3.1.1 应用
级别的中间件
通过 app.use
() 或 app.METHOD
() (METHOD包括 get
、post
、put
、delete
、all
),绑定到 app 实例上的中间件,叫做应用级别的中间件。
示例代码:
const express = require('express')
const app = express()
// 应用级别的中间件(全局中间件)
app.use((req , res , next) => {
next()
})
// 应用级别的中间件(局部中间件)
app.get('/' , mw1 , (req , res) => {
res.send('Home page.')
})
3.1.2 路由
级别的中间件
绑定到 express.Router
() 实例上的中间件,叫做路由级别的中间件。它的用法和应用级别中间件没有任何区别。只不过,应用级别中间件是绑定到 app 实例上,路由级别中间件绑定到router实例上。
示例代码:
const express = require('express')
const app = express()
const router = express.Router()
// 路由级别的中间件
router.use(function(req , res, next) => {
console.log('Time:' , Date.now())
next()
})
app.use('/' , router)
3.1.3 错误
级别的中间件
错误级别中间件的作用:专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题。
格式
:错误级别中间件的function处理函数中,必须有 4 个形参,形参顺序从前到后,分别是 (err, req, res, next
)。
// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()
// 1.定义路由
app.get('/' ,function(req , res) {
// 1.1 抛出一个自定义的错误(人为的制造错误)
throw new Error('服务器内部发生了错误!')
res.send('Home Page.')
} )
// 2.定义错误级别的中间件,捕获整个项目的异常错误,从而防止程序的崩溃
app.use(function(err, req , res , next) {
// 2.1 在服务器打印错误消息
console.log('发生了错误:' + err.message)
// 2.2 向客户端响应错误相关的内容
res.send('Error!' + err.message)
})
// 调用 app.listen 方法 , 指定端口号并启动web服务器
app.listen(80, (req, res) => {
console.log('Express server running at http://127.0.0.1');
})
注意:错误级别的中间件,必须注册在所有路由之后
!
3.1.4 Express内置
的中间件
自 Express 4.16.0 版本开始,Express内置了 3 个常用的中间件,极大的提高了Express 项目的开发效率和体验:
- ①
express.static
快速托管静态资源的内置中间件,例如:HTML 文件、图片、CSS 样式等(无兼容性) - ②
express.json
解析JSON格式的请求体数据(有兼容性,仅在4.16.0+ 版本中可用) - ③
express.urlencoded
解析URL-encoded格式的请求体数据(有兼容性,仅在4.16.0+ 版本中可用)
后面两个跟body-parser是一个作用。
3.1.5 第三方
级别的中间件
非 Express 官方内置的中间件,而是由第三方开发出来的Express 中间件,叫做第三方中间件。在项目中,大家可以 按需下载并配置第三方中间件,从而提高项目的开发效率。
例如:除了使用express.urlencoded这个内置中间件来解析请求体数据,还可以使用body-parser 这个第三方中间 件,来解析请求体数据。使用步骤如下:
- ① 运行npm install body-parser安装中间件
- ② 使用require导入中间件
- ③ 调用 app.use()注册并使用中间件
// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()
// 1.导入解析表单数据的中间件 body-parser
const parser = require('body-parser')
// 2.使用 app.use() 注册中间件
app.use(parser.urlencoded({ extended: false }))
// app.use(express.urlencoded({ extended : false }))
app.post('/' , (req , res) => {
// 如果没有配置任何解析表单数据的中间件,则 req.body 默认等于 undefined
console.log(req.body);
res.send('ok')
})
// 调用 app.listen 方法 , 指定端口号并启动web服务器
app.listen(80, (req, res) => {
console.log('Express server running at http://127.0.0.1');
})
注意:Express
内置的express.urlencoded
中间件,就是基于`body-parser 这个第三方中间件进一步封装出来。
3.2 作用范围分类
3.2.1 `全局中间件
客户端发起的任何请求
,到达服务器之后,都会触发
的中间件,叫做全局生效的中间件。
通过调用 app.use(中间件函数),即可注册一个全局生效的中间件,示例代码如下:
// 定义一个最简单的中间件函数
const mw = function (req, res, next) {
console.log('这是最简单的全局中间件函数')
// 把流转关系,转交给下一个中间件或路由
next()
}
// 将 mw 注册为全局生效的中间件
app.use(mw)
可以使用 app.use()
连续定义多个全局中间件
。客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行 调用,示例代码如下::
const express = require('express')
const app = express()
// 这是定义全局中间件的简化形式
app.use((req, res, next) => {
console.log('这是最简单的中间件函数')
next()
})
app.get('/', (req, res) => {
console.log('调用了 / 这个路由')
res.send('Home page.')
})
app.get('/user', (req, res) => {
console.log('调用了 /user 这个路由')
res.send('User page.')
})
app.listen(80, () => {
console.log('http://127.0.0.1')
})
3.2.2 `局部中间件
不使用 app.use()定义的中间件,叫做局部生效的中间件,示例代码如下:
// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()
// 1. 定义中间件函数 mw1
const mw1 = (req, res, next) => {
console.log('调用了局部生效的中间件')
next()
}
// 2. 创建路由
// mw1 这个中间件只在"当前路由中生效" ,这种用法属于"局部生效的中间件"
app.get('/', mw1, (req, res) => {
res.send('Home page.')
})
// mw1 这个中间件不会影响下面的这个路由
app.get('/user', (req, res) => {
res.send('User page.')
})
// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(80, function () {
console.log('Express server running at http://127.0.0.1')
})
可以在路由中,通过如下两种等价的方式,使用多个局部中间件:
// 以下两种写法是'完全等价'的 , 可根据自己的喜好 , 选择任意一种方式进行使用
app.get('/' , mw1 , mw2 , (req , res) => { res.send('Home page')})
app.get('/' ,[ mw1 , mw2 ], (req , res) => { res.send('Home page')})
同时使用多个局部中间件:
// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()
// 1. 定义中间件函数
const mw1 = (req, res, next) => {
console.log('调用了第一个局部生效的中间件')
next()
}
const mw2 = (req, res, next) => {
console.log('调用了第二个局部生效的中间件')
next()
}
// 2. 创建路由
app.get('/', [mw1, mw2], (req, res) => {
res.send('Home page.')
})
app.get('/user', (req, res) => {
res.send('User page.')
})
// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(80, function () {
console.log('Express server running at http://127.0.0.1')
})
注意:
- 局部或者全局主要是区分
app.use
方法。
4. 路由中间件实现思路(选读)
假设我们也来实现一个简单的Express 路由中间件
我们会怎么实现?
路由中间件最核心的思路:
- method 请求方法(all、get、post、put、delete)
- URL 请求地址
- handle 处理函数
那么按照思路:
定义一个app对象,并且它有一个叫做use的方法,use方法里面肯定需要存在一个路由映射关系
4.1 请求方法
需要区分请求方法设计
var routes = {'all':[]}
var methods = ['get', 'post', 'put', 'delete']
var app = {}
app.use = function(path, action){
routes.all.push([path,action])
}
// 每个method方法都对应自己的一个路由映射,这样我们就可以使用类似于express的
// app.get/app.post/app.put等等方法
for (let index = 0; index < methods.length; index++) {
const method = methods[index];
routes[method] = []
app[method] = function(path, action){
routes[method].push([path],action)
}
}
这里就提供了一个类似于express的路由方法:
app.use
app.get
app.post
app.put
app.delete
每个方法都最终共享一个路由对象 routes ,对象分别有 all、get、post、put、delete五个属性,每个属性对应一个数组A,数组A元素又是一个数组B,而数组B的元素1就是URL,元素2就是处理函数。(往下看能看到效果)
4.2 路由匹配
针对我们注册进来的请求方法、URL来做匹配
var match = function(pathname, routes, req, res) {
for (let index = 0; index < routes.length; index++) {
const route = routes[index];
var key = route[0]
var action = route[1]
if (pathname === key) {
action(req, res)
}
}
return false
}
4.3 路由分发
针对请求过来的方法和URL进行内容分发。
function route(req, res){
var pathname = url.parse(req.url).pathname
// 将请求方法变成小写
var method = req.method.toLowerCase()
// 根据请求方法分发
if (routes.hasOwnProperty(method)){
// 匹配请求URL
if (match(pathname, routes[method], req, res)) {
return
}
}
// 上面没有匹配,尝试让all()来处理
if (match(pathname, routes.all, req, res)) {
return;
}
// 处理 404 请求
handle404(req, res)
}
// 处理 404 请求
function handle404(req, res) {
res.writeHead(404)
res.end()
}
4.4 注册路由
app.get('/get', (req, res)=>{
console.log('匹配/get')
res.writeHead(200,{
"content-type":"text/html;charset=UTF-8"
});
res.end('匹配/get')
})
app.put('/put', (req, res)=>{
console.log('匹配/put')
res.writeHead(200,{
"content-type":"text/html;charset=UTF-8"
});
res.end('匹配/put')
})
app.delete('/delete', (req, res)=>{
console.log('匹配/delete')
res.writeHead(200,{
"content-type":"text/html;charset=UTF-8"
});
res.end('匹配/delete')
})
app.post('/post', (req, res)=>{
console.log('匹配/post')
res.writeHead(200,{
"content-type":"text/html;charset=UTF-8"
});
res.end('匹配/post')
})
app.use('/all', (req, res)=>{
console.log('匹配/all')
res.writeHead(200,{
"content-type":"text/html;charset=UTF-8"
});
res.end('匹配/all')
})
4.5 完整业务代码使用
我们在原来的HTTP Server试试效果:
// 1.导入 http 模块
const http = require ('http')
const url = require('url')
const querystring = require('querystring')
// 2. 创建 web 服务器实例
const sever = http.createServer()
/****************** 自定义路由处理 **************************/
var routes = {'all':[]}
var methods = ['get', 'post', 'put', 'delete']
var app = {}
app.use = function(path, action){
routes.all.push([path,action])
}
// 每个method方法都对应自己的一个路由映射,这样我们就可以使用类似于express的
// app.get/app.post/app.put等等方法
for (let index = 0; index < methods.length; index++) {
const method = methods[index];
routes[method] = []
app[method] = function(path, action){
routes[method].push([path,action])
}
}
var match = function(pathname, routes, req, res) {
for (let index = 0; index < routes.length; index++) {
const route = routes[index];
var key = route[0]
var action = route[1]
if (pathname === key) {
action(req, res)
}
}
return false
}
function route(req, res){
var pathname = url.parse(req.url).pathname
// 将请求方法变成小写
var method = req.method.toLowerCase()
// 根据请求方法分发
if (routes.hasOwnProperty(method)){
// 匹配请求URL
if (match(pathname, routes[method], req, res)) {
return
}
}
// 上面没有匹配,尝试让all()来处理
if (match(pathname, routes.all, req, res)) {
return;
}
// 处理 404 请求
handle404(req, res)
}
// 处理 404 请求
function handle404(req, res) {
res.writeHead(404)
res.end()
}
app.get('/get', (req, res)=>{
console.log('匹配/get')
res.writeHead(200,{
"content-type":"text/html;charset=UTF-8"
});
res.end('匹配/get')
})
app.put('/put', (req, res)=>{
console.log('匹配/put')
res.writeHead(200,{
"content-type":"text/html;charset=UTF-8"
});
res.end('匹配/put')
})
app.delete('/delete', (req, res)=>{
console.log('匹配/delete')
res.writeHead(200,{
"content-type":"text/html;charset=UTF-8"
});
res.end('匹配/delete')
})
app.post('/post', (req, res)=>{
console.log('匹配/post')
res.writeHead(200,{
"content-type":"text/html;charset=UTF-8"
});
res.end('匹配/post')
})
app.use('/all', (req, res)=>{
console.log('匹配/all')
res.writeHead(200,{
"content-type":"text/html;charset=UTF-8"
});
res.end('匹配/all')
})
// 打印 routes 路由对象
console.log(routes)
/******************* 自定义路由处理 *************************/
// 3.为服务器实例绑定request 事件, 监听客户端的请求
// request req
// response res
sever.on('request' , function(req , res){
console.log('服务端:我收到了客户端请求');
// 3.1 打印请求状态行
console.log(`请求方式: ${req.method}`);
console.log(`HTTP协议版本: ${req.httpVersion}`)
console.log(`请求地址: ${req.url}`)
console.log(`请求头: `)
console.log(req.headers)
// 路由解析
route(req, res)
})
sever.on('connection', function(){
console.log('服务端:我和客户端建立了底层TCP连接')
})
sever.on('close', function(){
console.log("服务端:我已经关闭服务")
})
sever.on('checkContinue', function(){
console.log("服务端:客户端数据较大")
})
sever.on('connect', function(){
console.log("服务端:客户端发起了连接请求")
})
sever.on('upgrade', function(){
console.log("服务端:客户端协议升级")
})
// 4. 启动服务器
sever.listen(80 , function(){
console.log('server running at http://127.0.0.1:80');
})
执行nodejs文件:
在浏览器上输入 http://127.0.0.1:80/all 看看效果:
说明我们整个流程是可以跑通的。
思考:
如何匹配正则表达式?
5. 中间件剖析(选读)
在学习 【NodeJs-5天学习】第二天篇② —— 网络编程(TCP、HTTP、Web应用服务) 过程中,我们片段式地接触完Web应用的基础功能和路由功能后,我们发现实际项目中有太多琐碎的细节工作需要处理。而对于web应用来说,我们作为开发者来说当然不希望接触到这么多细节性的处理,为此我们引入中间件(middleware
)来简化和隔离这些基础设施与业务逻辑之间的细节,让开发者能够关注在业务开发上,以达到提升开发效率的目的。
在最早的中间件定义中,它是一种在操作系统上为应用软件提供服务的计算机软件。它处于操作系统与应用软件之间,让应用软件更好、更方便地使用底层服务。借助于这种思想,对于HTTP请求的很多细节,我们也可以封装为中间件,开发者可以脱离这部分细节,专注于业务上。
从HTTP请求到具体业务逻辑,经历了一系列中间件处理(访问日志中间件、查询字符串中间件、cookie中间件、其他中间件),每个中间件处理掉相对简单的逻辑,然后给到应用层业务处理。 而中间件的上下文也就是请求对象和响应对象:req
和res
,而要完成它们之间的顺序调用就需要依赖于next
传递(next 函数是实现多个中间件连续调用的关键)。我们用一个链式图来看看:
红色箭头
表示我们一个请求到来之后会经历的处理顺序,最后到达具体业务处理,业务处理完之后把结果响应给到客户端,它们主要依靠next
来流转。虚线箭头
表示每一个请求在任意一个中间件上都共享同一份req & res对象
,这样意味着我们对req & res对象进行的数据操作会在后续的中间件能够引用到。
比如我在中间件1加入了req.a = 1, 在中间件2加入了 req.b = 2。那么我们就可以在 中间件3或者4访问到 req.a以及req.b。
所以一个中间件的基本格式如下:
// 基本中间件定义
var middleware = function(req, res, next) {
// 逻辑处理
handle(req,res)
// 传递给下一个中间件
next()
}
思考:
- 如何在路由中间件基础上实现链式?
6.总结
篇③主要是讲解了Express这个强大的web框架用来构造api请求或者web页面,同时剖析了一下中间件的概念以及实现了一下简单的路由中间件,更多是抛砖引玉,需要物联网初学者慢慢消化。