> 文档中心 > 【Nodejs】深入理解Express框架之如何使用各类中间件_04

【Nodejs】深入理解Express框架之如何使用各类中间件_04

目录

一. 中间件

❣️ 中间件概念

❣️ 中间件结构

❣️ 中间件分类

1. 应用级中间件

2. 路由级中间

3. 内置中间件

4. 第三方中间件

5. 错误处理中间件

二. mysql模块

1. 创建普通连接

2. 创建连接池

🚀 写在最后 🚀


【前文回顾】👉 初识Node.js Web应用开发框:Express_03


 

一. 中间件

中间件为路由服务的,用于拦截对路由的请求,也可以作出响应。

说到中间件,官网对它的阐述是这样的:

“Express是一个自身功能极简,完全是路由和中间件构成一个web开发框架:从本质上来说,一个Express应用就是在调用各种中间件。”

由此可见,中间件在Express web应用开发中的重要性。

❣️ 中间件概念

在nodejs中,中间件主要是指封装所有Http请求细节处理的方法,是从Http请求发起到响应结束过程中的处理方法。一次Http请求通常包含很多工作,如记录日志、ip过滤、查询字符串、请求体解析、Cookie处理、权限验证、参数验证、异常处理等,但对于Web应用而言,并不希望接触到这么多细节性的处理,因此引入中间件来简化和隔离这些基础设施与业务逻辑之间的细节,让开发者能够关注在业务的开发上,以达到提升开发效率的目的。

中间件的行为比较类似Java中过滤器的工作原理,就是在进入具体的业务处理之前,先让过滤器处理。它的工作模型下图所示。

❣️ 中间件结构

app.use([path],function)

path:是路由的url,默认参数‘/',意义是路由到这个路径时使用这个中间件

function:中间件函数

这个中间件函数可以理解为就是function(request,response,next)

❣️ 中间件分类

中间件分为5大类,应用级中间件、路由级中间件、内置中间件、第三方中间件、错误处理中间件

1. 应用级中间件

也称为自定义中间件   ------本质就是个函数

app.use(url,(req,res ,next)=>{});

url 表示要拦截的URl,对应路由中的URl,一旦拦截会自动执行回调函数

next  是一个函数,表示往后执行下一个中间件或者路由

🌴  注:自定义中间件的参数说明
 

这个function总共有三个参数(req,res,next);
 

当每个请求到达服务器时,nodejs会为请求创建一个请求对象(request),该请求对象包含客户端提交上来的数据。同时也会创建一个响应对象(response),响应对象主要负责将服务器的数据响应到客户端。而最后一个参数next是一个方法,因为一个应用中可以使用多个中间件,而要想运行下一个中间件,那么上一个中间件必须运行next()。

练习:创建添加到购物车的路由(get  /shopping),传递商品的价格price,最后响应'商品的最终价格为:xxxx'。添加中间件。实现对价格打九折

*02_middleware.js文件

const express = require('express');//创建web服务器const app = express();//设置端口app.listen(8080);//只能按照URL拦截//拦截对 /list的请求//参数1:要拦截的URL//参数2:回调函数,一旦拦截到,自动调用这个函数app.use('/list', (req, res, next) => {  //req res和路由中的是一样的  //next 是一个函数,表示执行下一个中间件或者路由  //获取以查询字符串传递的数据   console.log(req.query);  //判断传递的用户名是否为管理员root  //如果不是  if (req.query.uname !== 'root') {    //响应    res.send('请提供管理员账户');  } else {    //往后执行(下一个中间件或者路由)    next();  }});//用户列表路由//get  /listapp.get('/list', (req, res) => {  res.send('这是所有用户的数据');});// http://127.0.0.1:8080/list?uname=abcd//添加中间件,拦截添加购物车的请求app.use('/shopping', (req, res, next) => {  //在中间件中获取数据,然后打折  console.log(req.query); // {}  //对价格打九折  req.query.price *= 0.9; //req.query.price={}*0.9=NaN  //往后执行  next();});//添加购物车路由  get  /shoppingapp.get('/shopping', (req, res) => {  //获取查询字符串传递的数据  //个人尝试理解:req.query格式化对象,req.query.price=NaN,即给空对象强制添加price属性,属性值为NAN,所以{ price: NaN }   console.log(req.query); // { price: NaN }    res.send('商品最终价格为:' + req.query.price);});

我们简单分析一下:

启动服务:node 02_middleware.js


在8080后面输入/lis请求,被中间件拦截,向页面作出响应res.send('请提供管理员账户');

在/list后面输入?uname=root,即利用查询字符串传参,然后经过中间件验证,通过验证后,往后执行用户列表路由,服务器作出响应res.send('这是所有用户的数据');
 

所以,如同以上,本例中我们来实现购物车路由,利用中间件打折功能


传参前:在8080后面输入/shopping请求,经过中间件打折功能,往后执行购物车路由,服务器作出响应,res.send('商品最终价格为:'+req.query.price);由于还没有把参数price的值传过去,所以最终价格为NaN

传参后:在/shopping后面输入?price=8000,即利用查询字符串传参,然后经过中间件打折功能,往后执行购物车路由,此时已携带price参数值,然后服务器作出响应res.send('商品最终价格为:'+req.query.price);
 

2. 路由级中间

路由器的使用,就是路由级中间件

app.use('/product',productRouter)

3. 内置中间件

托管静态资源(html,css,js,图像...)

当浏览器端请求(静态资源)文件时,不需要通过路由去寻找文件,而是让浏览器自动到指定的目录下去寻找。(没有托管的话,浏览器每请求一个文件,就得写一个请求路由去响应浏览器的请求。使用res.sendFile(__dirname+'/文件名.html'))

app.use(express.static('目录路径'))

🌴 注:我该如何请求托管的静态资源?
 

托管静态资源后,向服务器请求一个文件时,启动服务器后,在浏览器地址栏,端口后输入/带后缀的文件名,即可完成请求

🏝️  扩展:内置中间件

从 4.x 版本开始,, Express 已经不再依赖 Connect 了。除了 express.static, Express 以前内置的中间件现在已经全部单独作为模块安装使用了。请参考中间件列表。 

express.static(root, [options])

express.static是Express中唯一的内建中间件。用来处理静态资源文件。它以server-static模块为基础开发,负责托管 Express 应用内的静态资源。 参数root为静态资源的所在的根目录。 参数options是可选的,支持以下的属性:

属性 描述 类型 默认值
dotfiles 是否响应点文件(以点“.”开头的文件或目录)。供选择的值有"allow","deny"和"ignore"。请参阅下面的点文件。 String "ignore"
etag 是否启用etag生成 Boolean true
extensions 设置文件扩展名回退(设置文件扩展名备份选项):如果未找到文件,请搜索具有指定扩展名的文件并提供找到的第一个扩展名,例如:['html', 'htm']  Array []
index 发送目录索引文件。设置false将不发送。 Mixed "index.html"
lastModified 设置文件在系统中的最后修改时间到Last-Modified头部。可能的取值有falsetrue Boolean true
maxAge 在Cache-Control头部中设置max-age属性,精度为毫秒(ms)或则一段ms format的字符串 Number 0
redirect 当请求的pathname是一个目录的时候,重定向到尾随"/" Boolean true
setHeaders 当响应静态文件请求时设置headers的方法 Funtion

如果你想获得更多关于使用中间件的细节,你可以查阅在 Express 中提供静态文件Serving static files in Express和使用中间件 - 内置中间件

❣️ Express 4.x API 中文文档:Express 4.x API 中文文档 | 菜鸟教程
❣️ Express 4.x API reference:Express 4.x - API Reference

下面的例子使用了 express.static 中间件,其中的 options 对象经过了精心的设计。

var options = {  dotfiles: 'ignore',  etag: false,  extensions: ['htm', 'html'],  index: false,  maxAge: '1d',  redirect: false,  setHeaders: function (res, path, stat) {    res.set('x-timestamp', Date.now())  }}app.use(express.static('public', options))

练习:再次托管静态资源到file目录,然后添加文件测试。加入public和file目录下出现相同的文件名称,查看显示哪一个。

//引入express包const express=require('express');//创建web服务器const app=express();//设置端口app.listen(8080);//托管静态资源到public目录//浏览器端请求文件,自动到public目录寻找app.use( express.static('./public') );app.use( express.static('./files') );

🌴 注:每个应用可有多个静态目录。

那么,同一个文件,多个托管目录,听谁的?

如果在第一个托管的目录中找到文件,就不在寻找;反之,去第一个托管目录寻找,依次直到找到为止

练习:编写文件04_three.js,使用express创建web服务器,托管静态资源到public目录下,包含登录文件login.html,点击提交向服务器发请求(post  /mylogin),创建对应的路由,获取请求的数据

//引入express包const express = require('express');//引入查询字符串模块//const querystring=require('querystring');//1.引入body-parser模块const bodyParser = require('body-parser');const app = express();app.listen(8080);//托管静态资源到publicapp.use(express.static('./public')); //托管静态资源代替了通过路由找寻文件:app.get('/login',(req,res)=>{res.sendFile(__dirname+'/login.html');});详见day04中的03_express.js//2.将post请求的数据解析为对象app.use(bodyParser.urlencoded({  //是否使用扩展的查询字符串模块qs;false不使用,此时会使用官方提供的querystring,true使用  extended: false}));//根据表单的请求创建对应的路由//post  /myloginapp.post('/mylogin', (req, res) => {  //获取post请求的数据  //3.获取数据,前提已经使用了body-parser中间件  console.log(req.body);  //以往我们是通过事件,来获取post请求的数据  /*  req.on('data',(chunk)=>{    //格式为buffer,需要转字符串,转后格式为查询字符串let str=String(chunk);//将查询字符串解析为对象---需要引入querystring模块    let obj=querystring.parse(str);console.log(obj);  })  */  res.send('登录成功');});

4. 第三方中间件

属于第三方模块,需要提前下载安装

使用body-parser将post请求数据解析为对象

//1.引入body-parser模块const bodyParser=require('body-parser');//2.使用body-parser中间件,会将所有post请求数据解析为对象app.use( bodyParser.urlencoded({        //是否使用扩展的模块qs;true表示使用,false表示不使用,此时会自动使用querystring模块        extended:false}) );//3. 在路由中获取对象格式数据req.body

🌴 注:body-parser中间件要写在路由或是路由器之前!

🏝️  扩展扩展:关于body-parser中间件
 

body-parser代替了客户端post请求发送(传递)的http请求体内容,通过事件以流的形式获取buffer数据、转字符串再转为方便的对象格式内容的繁琐过程,从而让后台可以使用req.body直接获取(接收)为对象格式的数据

  //😅通过事件:以往我们是通过事件,来获取post请求的数据  req.on('data',(chunk)=>{  //格式为buffer,需要转字符串,转后格式为查询字符串  let str=String(chunk);  //将查询字符串解析为对象  let obj=querystring.parse(str);  console.log(obj);  })

不用body-parser的话,post请求必须通过req.on('data',(chunk)=>{…}以数据流事件来获取数据(buffer格式),进而利用全局函数String()或是tostring()方法转字符串、引入查询字符串模块解析为对象

😇闲话:express中间件—body-parser获取post、get数据

在express中,已经封装好获取get参数的方法,即req.query,但是post请求的参数却没有被封装,需要我们借助中间件(body-parser)来获取!

也就是说在express中对get请求内置了req.query来获取请求数据,对post请求,需要配合使用body-parser中间件来获取,否则无法使用req.body获取post传递的数据(不引入body-parser,默认获取到的是undefined)。这也就是为什么我们在nodejs里面使用express框架中的req.body 获取post传值为undifined的原因

另外,body-parser是非常常用的一个express中间件,作用是对post请求的请求体进行解析。即bodyparser的作用:它用于解析客户端请求的body中的内容,内部使用JSON编码处理,url编码处理以及对于文件的上传处理。使用非常简单,以下两行代码已经覆盖了大部分的使用场景。
①app.use(bodyParser.json());
②app.use(bodyParser.urlencoded({ extended: false }));

5. 错误处理中间件

错误处理中间件有 4 个参数,定义错误处理中间件时必须使用这 4 个参数。即使不需要 next 对象,也必须在签名中声明它,否则中间件会被识别为一个常规中间件,不能处理错误。

错误处理中间件和其他中间件定义类似,只是要使用 4 个参数,而不是 3 个,其签名如下: (err, req, res, next)。

app.use((err,req,res,next)=>{  console.error(err.stack)  res.status(500).send('Something broke!')})

二. mysql模块

nodejs操作mysql数据库的工具模块

🌴  闲话:mysql模块跟express模块没有任何关系,他们都是第三方模块,需要下载安装,安装后,在node_modules文件夹里查看已下载的包

连接数据库

mysql -urootmysql -h127.0.0.1 -p3306 -uroot -p  #数据库服务器IP  mysql端口  用户名  密码

🌴 闲话:本机既做客户端又做服务器。本机做数据库服务器 ip为127.0.0.1、3306为MySQL的默认端口;本机又是客户端,用户名是root、密码为空,就像ATM机器取钱(要访问银行数据库服务器)一样要输入用户名密码

增删改查

select * from emp where eid=1;insert into emp values(...);update emp set sex=1,salary=8000 where eid=3;delete from emp where eid=2;

下载安装

npm install mysql

 mysql使用

mysql包的使用:https://www.npmjs.com/package/mysql

1. 创建普通连接

 创建普通连接或是创建连接对象:👇

//创建连接对象。mysql模块下的Connection()方法const c=mysql.createConnection({ })  //需要提供mysql连接的相关内容//测试连接。connect()为连接对象下的connect方法c.connect()  //测试连接,可以省略。//执行SQL命令c.query(sql命令,回调函数) // 要执行的SQL命令,通过回调函数获取结果

🌴 注:连接对象下的query方法

连接对象.query(sql命令,回调函数)
 

c.query(sql命令,回调函数)          

                其中回调函数的参数:
                        err         可能产生的错误  ---比如sql命令写错了等等
                        result    具体SQL命令的执行结果

//引入mysql模块const mysql = require('mysql');//创建连接对象const c = mysql.createConnection({  host: '127.0.0.1',  port: '3306',  user: 'root',  password: '',  database: 'tedu' //连接成功后要进入的数据库});//测试连接:没有报错就是连接成功//c.connect();//接下来,我们使用nodejs来操作数据库,即使用mysql模块下的query(sql命令,回调函数)方法操作数据库//执行SQL命令//连接对象下的query方法//参数1:SQL命令//参数2:回调函数,用于获取结果c.query('SELECT * FROM emp', (err, result) => {  //err 可能产生的错误  if (err) throw err;  //result  SQL命令执行的结果  console.log(result); //查看SQL命令执行的结果});

2. 创建连接池

连接池中保存若干个mysql的连接,如果要操作数据库,只需要从中取出一个连接。当使用完会自动归还到连接池

创建连接池对象:👇

//创建连接池对象。mysql模块下的createPool()方法const pool=mysql.createPool({ });//执行SQL命令pool.query(sql命令,回调函数);//要执行的SQL命令,通过回调函数获取结果

🌴 注:连接池对象下的query方法 

连接池对象.query(sql命令,回调函数)

pool.query(sql命令,回调函数)    
                     其中回调函数的参数:
                               err    可能产生的错误
                               result  具体SQL命令的执行结果 //  sql命令执行后返回的对象存放在这里

❣️ 连接池对象没有上面普通连接对象的测试方法connect(),只能通过直接执行SQL命令pool. query(sql命令,回调函数)

SQL注入:在用户提供的条件中,添加了具有攻击性条件。
delete from emp WHERE eid=5 or 1=1;
1=1  所有的数据都满足这个条件

占位符(?):会对用户提供的条件进行过滤,把具有攻击性的条件给删除

pool.query( SQL命令,要过滤的数据,回调函数 )

要过滤的数据格式为数组,过滤完会自动替换到SQL命令中的占位符
pool.query('delete from emp WHERE eid=?',['5 or 1=1'],(err,result)=>{      

           if(err) throw err;
           console.log(result);
});

🌴 注:普通连接与连接池,没有区别,平时我们使用连接池更多

练习:往员工表emp下插入1条数据,对所有的值进行过滤。

//引入mysql模块const mysql = require('mysql');//创建连接池对象const pool = mysql.createPool({  host: '127.0.0.1',  port: '3306',  user: 'root',  password: '',  database: 'tedu',  connectionLimit: '20'});//执行SQL命令/*//在员工表中查询员工编号为1的员工的所有信息pool.query('SELECT * FROM emp WHERE eid=1',(err,result)=>{  if(err) throw err;  console.log(result);})//删除编号为5的员工信息  ------附带过滤数据的功能,防止sql注入pool.query('DELETE FROM emp WHERE eid=?',['5 or 1=1'],(err,result)=>{  if(err) throw err;  console.log(result);});//在员工表中插入一条数据:使用连接池对象query方法,用对象的形式向数据库插入数据pool.query('INSERT INTO emp VALUES(?,?,?,?,?,?)',[null,'然哥',0,'1977-5-29',50000,10],(err,result)=>{  if(err) throw err;  console.log(result);});*/let ran = {  //向emp表插入数据时,由于缺失的列会默认使用默认值  //所以,eid列如果设置为空的话,可以省略,会使用默认值  //eid:null,  ename: '然然',  salary: 40000,  deptId: 20,  sex: 0}pool.query('INSERT INTO emp SET ?', [ran], (err, result) => { // 插入对象的方式只能在mysql模块里使用,在操作mysql数据库时不能这样使用,只能使用values  if (err) throw err;  console.log(result);});

❣️:一般的,服务器获取从客户端请求过来的数据,无论是使用get或post方法请求的,最后服务器接受(获取)的数据(req.query或req.body)都会解析为对象格式,所以,在mysql模块里提供了在要过滤的数组里放置对象数据,这样的便利操作,仅限在mysql模块里使用,在正常操作数据库时

练习:创建web服务器,托管静态资源到public目录,包含部门添加网页add.html,输入部门名称,点击提交,向服务器发请求(get  /add),创建对应的路由,在路由中获取传递的数据,将数据插入到数据库tedu下的dept表中,响应“部门添加成功”

 

*add.html文件

部门添加

部门名称

 *app.js文件

//引入express包const express = require('express');//引入mysql包const mysql = require('mysql');//创建web服务器const app = express();//设置端口app.listen(8080);//托管静态资源到public目录app.use(express.static('./public'));//连接mysql数据库,创建连接池const pool = mysql.createPool({  host: '127.0.0.1',  port: '3306',  user: 'root',  password: '',  database: 'tedu',  connectionLimit: '20'});//根据表单请求创建对应的路由//get  /add// 因为托管了静态资源,所以地址栏直接输入/add.html即可。http://127.0.0.1:8080/add.htmlapp.get('/add', (req, res) => {  //获取查询字符串传递的数据  console.log(req.query);  //将数据插入到数据表dept  pool.query('INSERT INTO dept SET ?', [req.query], (err, result) => {    if (err) throw err;    console.log(result);    //只有数据插入后再响应    res.send('部门添加成功');  });});

🚀 写在最后 🚀

Tips:对中间件的一个小总结👇

1、中间件就是一种功能的封装方式,就是封装在程序中处理http请求的功能,

2、中间件是在管道中执行

3、中间件有一个next()函数,如果不调用next函数,请求就在这个中间件中终止了,

4、中间件和路由处理器的参数中都有回调函数,这个函数有2,3,4个参数  如果有两个参数就是req和res;如果有三个参数就是req,res和next  如果有四个参数就是err,req,res,next

5、如果不调用next ,管道就会终止,不会再有处理器做后续响应,应该向客户端发送一个响应

6、如果调用了next,不应该发送响应到客户端,如果发送了,则后面发送的响应都会被忽略

7、中间件的第一个参数可以是路径,如果忽略则全部都匹配

如果这篇【文章】有帮助到你,希望可以给【青春木鱼】点个👍,创作不易,相比官方的陈述,我更喜欢用【通俗易懂】的文笔去讲解每一个知识点,如果有对【前端技术】感兴趣的小可爱,也欢迎关注❤️❤️❤️青春木鱼❤️❤️❤️,我将会给你带来巨大的【收获与惊喜】💕💕!

NICE