【NodeJs-5天学习】第二天篇③ ——Express Web框架 和 中间件

【NodeJs-5天学习】第二天篇③ ——Express Web框架 和 中间件

面向读者群体

  • ❤️ 电子物联网专业同学,想针对硬件功能构造简单的服务器,不需要学习专业的服务器开发知识 ❤️
  • ❤️ 业余爱好物联网开发者,有简单技术基础,想针对硬件功能构造简单的服务器❤️

技术要求

  • HTMLCSSJavaScript基础更好,当然也没事,就直接运行实例代码学习

专栏介绍

  • 通过简短5天时间的渐进式学习NodeJs,可以了解到基本的服务开发概念,同时可以学习到npm、内置核心API(FS文件系统操作、HTTP服务器、Express框架等等),最终能够完成基本的web开发,而且能够部署到公网访问。

学习交流群

  • NodeJs物联网五天入门学习之旅(搜索:729040020

🙏 此博客均由博主单独编写,不存在任何商业团队运营,如发现错误,请留言轰炸哦!及时修正!感谢支持!🎉 欢迎关注 🔎点赞 👍收藏 ⭐️留言📝

1. 前言

在前面一篇

【NodeJs-5天学习】第二天篇② —— 网络编程(TCP、HTTP、Web应用服务)

我们讲解了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 快速入门

  • ① 安装 expressbody-parsermoment 模块

  • ② 导入 expressbody-parser 模块

  • ③ 创建 web 服务器

  • ④ 注册中间件,处理业务逻辑

  • ⑤ 调用 app.listen(端口号, 启动成功后的回调函数) ,启动服务器

2.1 ① 安装 expressbody-parsermoment 模块

分别执行:

  • npm install express --save
    express主要是构建web服务器。
    在这里插入图片描述
  • npm install body-parser --save
    body-parser主要是用来解析post请求体,包括jsonurlencoded表单
    在这里插入图片描述
  • npm install moment --save
    在这里插入图片描述
    moment主要是用来处理时间

2.2 ② 导入 expressbody-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请求体,包括jsonurlencoded表单
  • 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'会匹配到 acdabcd
  • '/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包括 getpostputdeleteall),绑定到 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中间件、其他中间件),每个中间件处理掉相对简单的逻辑,然后给到应用层业务处理。 而中间件的上下文也就是请求对象和响应对象:reqres,而要完成它们之间的顺序调用就需要依赖于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页面,同时剖析了一下中间件的概念以及实现了一下简单的路由中间件,更多是抛砖引玉,需要物联网初学者慢慢消化。

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

单片机菜鸟爱学习

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值