express中间件
中间件(Middleware),特指业务流程的
中间处理环节
1.express中间件的调用流程
当一个请求到达express服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理
1.1express中间件的格式
express的中间件,本质上就是一个function处理函数,其格式如下:
next函数的作用:
next函数时实现多个中间件连续调用的关键,他表示把流转关系转交给下一个中间件或路由
1.2定义中间件函数
const express = require("express");
const app = express();
// 定义一个最简单的中间件函数
const mw = function (req, res, next) {
console.log("这是一个最简单的中间件函数");
// 把流转关系,转交给下一个中间件或路由
next();
};
// 将mw注册为全局生效的中间件
app.use(mw)
app.get('/',(req,res)=>{
res.send('Home page')
})
app.get('/user',(req,res)=>{
res.send('User page')
})
app.listen(80, () => {
console.log("express server running http://127.0.0.1");
});
因此,客户端发起的任何请求,到达服务器之后,都会触发的中间件,就叫做全局生效的中间件
通过调用app.use(中间件函数),即可定义一个全局生效的中间件
定义中间件的简化形式
//取消中间变量接收,直接提供一个函数
app.use((req,res,next)=>{
console.log('这是简化版的最简单的中间件函数')
next()
})
1.3中间件的作用
多个中间件之间,共享同一份
req和res.基于这样的特性,我们可以在上游的中间件中,统一为req或res对象添加自定义的属性或者方法,供下游的中间件或者路由访问
app.use((req, res, next) => {
const time = Date.now();
// 为req对象,挂载自定义属性,把时间共享给后面所有的路由
req.startTime = time;
next();
});
//在路由中使用startTime,如下图我们可以访问
app.get("/", (req, res) => {
res.send("Home page" + req.startTime);
});
app.get("/user", (req, res) => {
res.send("User page" + req.startTime);
});
1.4.定义多个全局中间件
可以使用app.use()连续定义多个全局中间件,客户端请求到达服务器之后,会按照中间件定义的先后顺序一次精选调用,示例代码如下:
const express = require("express");
const app = express();
// 定义第一个全局中间件
app.use((req, res, next) => {
console.log("调用了第一个全局中间件");
next();
});
// 定义第二个全局中间件
app.use((req, res, next) => {
console.log("调用了第二个全局中间件");
next();
});
// 路由
app.get("/user", (req, res) => {
res.send("User page");
});
app.listen(80, () => {
console.log("express server running http://127.0.0.1");
});
1.5 局部生效的中间件
不使用app.use()定义的中间件,叫做局部生效的中间件
const express = require("express");
const app = express();
// 定义一个中间件mw
const mw = (req, res, next) => {
console.log("调用了第一个全局中间件");
next();
};
// 路由
// 此时mw为一个局部中间件,只在/这个路由中生效
app.get("/", mw, (req, res) => {
res.send("Home page");
});
// 如果用户请求了/user地址,中间件不会生效
app.get("/user", (req, res) => {
res.send("User page");
});
app.listen(80, () => {
console.log("express server running http://127.0.0.1");
});
2.使用中间件的5个注意事项
- 一定要在
路由之前定义中间件 - 客户端发送过来的请求,
可以连续调用多个中间件进行处理 - 切记定义中间件函数中执行完中间件的业务代码后,要记得
调用next()函数 - 为了
防止代码逻辑混乱,调用next()函数之后不要再写额外的代码 - 连续调用多个中间件时,多个中间件之间,
共享req和res对象
3.中间件的分类
为了方便理解和记忆,express官方直接把常见的中间件用法,分为了5大类
应用级别的中间件路由级别的中间件错误级别的中间件express内置的中间件第三方的中间件
3.1应用级别的中间件
通过app.use或app.get()或app.post(),
绑定到app实例上的中间件,叫做应用级别的中间件
// 应用级别的中间件(局部中间件)
app.get("/", mw1, (req, res) => {
res.send("Home page");
});
// 应用级别的中间件(全局中间件)
app.use((req,res,next) => {
next()
});
3.2 路由级别的中间件
绑定到express.Router()实例上的中间件.叫做路由级别的中间件,它的用法和应用级别中间件没有任何区别,只不过,应用级别中间件是绑定到app实例上,路由级别中间件绑定到router实例上
3.3错误级别的中间件
错误级别中间件的作用: 专门用来捕获整个项目中的发生的异常错误从而防止项目异常崩溃的问题
格式:错误级别的中间件的function处理函数中,必须有4个形参,形参顺序从前到后,分别是(err,req,res,next)
const express = require("express");
const app = express();
// 定义路由
app.get("/", (req, res) => {
// 1.人为的制造一个错误
throw new Error("服务器内部发生异常 !"); // 此时发请求服务器是崩溃状态
res.send("Home Page");
});
// 2.定义一个错误级别的中间件,捕获整个项目的异常错误,从而防止程序崩溃
app.use((err, req, res, next) => {
console.log("发生了错误," + err.message);
res.send("Error:" + err.message);
});
app.listen(80, () => {
console.log("express server running in http://127.0.0.1");
});
注意:错误级别中间件必须放在定义在路由之后
3.4express内置的中间件
自express4.16.0版本开始,express内置了3个常用的中间件,极大的提高了express项目的开发效率和体验
express.static快速托管静态资源的内置中间件,例如:HTML文件,图片,CSS样式等(无兼容性,任何版本express都可用)express.json解析JSON格式的请求体数据(有兼容性,仅在4.16.0+版本可用)express.urlencoded解析URL-encoded格式的请求体数据(有兼容性,仅在4.16.0+版本可用)
代码示例(express.json && express.urlencoded)
const express = require("express");
const app = express();
//配置解析json中间件
// 1.通过express.json解析表单中的json格式请求体数据
app.use(express.json());
// 2.通过express.urlencoded()解析表单中url-encoded数据
//{extended:false}为要传递的默认参数
app.use(express.urlencoded({ extended: false }));
// 定义路由
app.post("/user", (req, res) => {
// 在服务器可以使用 req.body 这个属性,来接受客户端发送过来的请求体数据
// 默认情况下,如果不配置解析表单数据的中间件,则req.body默认等于undefined
console.log(req.body); // undefined
res.send("OK! ");
});
// 定义路由
app.post("/book", (req, res) => {
// 在服务器可以使用 req.body来获取JSON格式的表单数据和url-encoded格式的数据
// 默认情况下,如果不配置解析中间件,则req.body默认等于undefined
console.log(req.body); // undefined
res.send("OK! ");
});
app.listen(80, () => {
console.log("express server running in http://127.0.0.1");
});
比如 在express@4.16.0版本出现之前,我们经常会使用body-parser这个第三方的中间件,来解析请求体数据,使用步骤如下:
- 运行
npm install body-parser安装中间件 - 通过require导入中间件
- 调用
app.use()注册并使用中间件
const express = require("express");
// 导入body-parse第三方中间件
const parser = require("body-parser");
const app = express();
//使用中间件
app.use(parser.urlencoded({ extended: false }));
// 定义路由
app.post("/user", (req, res) => {
// 没有配置任何解析表单数据的中间件,默认req.body为undefined
console.log(req.body); // undefined
res.send("OK! ");
});
app.listen(80, () => {
console.log("express server running in http://127.0.0.1");
});
4.自定义中间件
自己手动模拟一个类似于express.urlencoded这样的中间件,来解析POST提交到服务器的表单数据
- 使用app.use定义一个全局中间件
- 监听req.data事件
- 监听req.end事件
- 使用
querystring模块解析请求体数据 - 将解析出来的数据对象挂载为req.body
- 将自定义中间件封装为模块
注意
- 在中间件中,需要监听req对象的data事件,来获取客户端发送到服务器的数据,如果数据量比较大,无法一次性发送完毕,则客户端会把
数据切割之后,分批发送到服务器,每一次触发data事件时,获取到数据只是完整数据的一部分,需要手动对接收到的数据进行拼接 - 当请求体数据
接收完毕之后,会自动触发req的end事件,因此我们可以在req的end事件中拿到并处理完整的请求体数据 - Node.js内置了一个
querystring模块,专门用来处理查询字符串,通过该模块的parse()函数,可以轻松的把查询字符串解析成对象的格式 - 由于上游的中间件和下游的中间件及路由之间,共享同一分req和res,因此可以将解析出来的数据挂载为req的自定义属性,供下游使用
// 自定义以解析表单数据的中间件
const express = require("express");
const app = express();
// 导入querystring模块
const qs = require("querystring");
//定义解析表单数据的中间件
app.use((req, res, next) => {
//中间件的业务逻辑
//1.定义一个str字符串,专门用来存储客户端发送过来的请求体事件
let str = "";
//2.监听req的data事件
req.on("data", (chunk) => {
str += chunk;
});
// 3.监听req的end事件
req.on("end", () => {
// 在str中存放的是完整的请求体数据
console.log(str);
// 将字符串格式的请求数据,解析成对象格式, 调用qs.parse()方法
const body = qs.parse(str);
console.log("body", body);
// 挂载为req的自定义属性 req.body
req.body = body;
next();
});
});
// 定义路由
app.post("/user", (req, res) => {
// 没有配置任何解析表单数据的中间件,默认req.body为undefined
console.log(req.body); // undefined
res.send(req.body);
});
app.listen(80, () => {
console.log("express server running at http://127.0.0.1");
});
为了优化代码结构,我们也可以把自定义的中间件函数封装为独立的模块(自行拆分)
使用express编写接口
首先需要创建一个
基本的服务器和一个API路由模块
1.编写get和post接口
//api路由模块16-1apiRouter.js
const express = require("express");
const router = express.Router();
// 在这里挂载对应的路由模块
// 定义一个get接口
router.get("/get", (req, res) => {
// 通过req.query获取客户端通过查询字符串发送到服务器的数据
const query = req.query;
// 调用res.send()方法,向客户端处理相应的结果
res.send({
status: 0, // 0表示处理成功,1表示处理失败
msg: "Get请求成功",
data: query, // 需要响应给客户端的数据
});
});
// 定义一个post接口
router.post('/post',(req,res)=>{
// 通过req.body获取请求体中包含的url-encoded格式的数据
const body = req.body
// 调用res.send()向客户端响应结果
res.send({
status:0,
msg:'post请求成功',
data:body
})
})
module.exports = router;
//在创建服务器的页面调用api路由模块
const express = require('express')
const app = express()
// 配置解析表单数据的中间件
app.use(express.urlencoded({ extended: false }))
// 导入路由模块
const router = require('./16-1apiRouter')
// 把路由模块注册在app上
app.use('/api',router)
app.listen(80,()=>{
console.log('express server running at http://127.0.0.1')
})
2.接口的跨域问题
测试跨域示例代码:点击按钮会发现接口并没有成功响应,是因为发生跨域(协议,域名,端口号任一不同会产生)
那是由于我们在浏览器打开当前html页面调取接口是使用的file协议(file:///E:/02-learn/nodejs/18-express/17-测试接口跨域问题.html),,而接口是http协议,因为会跨域
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
</head>
<body>
<button id="btnGet">GET</button>
<button id="btnPost">POST</button>
<script>
$(function () {
// 1.测试get接口
$('#btnGet').on('click', function () {
$.ajax({
type: 'GET',
url: 'http://127.0.0.1/api/get',
data: { name: 'stephen', age: 34 },
success: function (res) {
console.log('res', res)
}
})
})
// 2.测试post接口
$('#btnPost').on('click', function () {
$.ajax({
type: 'POST',
url: 'http://127.0.0.1/api/post',
data: { bookname: '水浒传', aguthor: '施耐庵' },
success: function (res) {
console.log('res', res)
}
})
})
})
</script>
</body>
</html>
3.使用cors解决接口跨域问题
由于jsonp解决跨域之针对get请求有效,所以不推荐使用
cors是express的一个第三方中间件,通过安装和配置cors中间件,可以很方便的解决跨域问题
- 使用
npm install cors安装中间件 - 使用
const cors = require('cors')导入中间件 - 在路由之前调用
app.use(cors())配置中间件
// 一定要在路由之前配置cors这个中间件,从而解决跨域的问题
const cors = require("cors");
app.use(cors());
3.1什么是cors
cors(Cross-Origin Resource Sharing,跨域资源共享)由一系列的
HTTP响应头组成,这些HTTP响应头决定浏览器是否阻止前端JS代码跨域获取资源
默认情况下浏览器的同源安全策略默认会阻止网页跨域获取资源,但如果接口服务器配置了cors相关的http响应头,就可以解除浏览器端的跨域访问限制
注意: cors主要在服务器端进行配置,客户端无需做任何配置,即可请求开启了cors的接口,cors在浏览器中有见同行,只支持XMLHttpRequest Level2 的浏览器,才能正常访问开启了cors的服务端接口(比如IE10+,Chrome4+,FileFox3.5+)
3.2 cors响应头部
3.2.1 cors响应头部-Access-Control-Allow-Origin
响应头部中可以携带一个Access-Control-Allow-Origin字段,其语法如下:
Access-Control-Allow-Origin: <origin> | *
// 其中,origin参数指定了`允许访问该资源的外域URL`
// 比如,下面的字段值将只允许来自http://itcast.cn的请求:
res.setHeader('Access-Control-Allow-Origin','http://itcast.cn')
// 如果指定了字段值为 *,表示允许来自任何域的请求,*为通配符
res.setHeader('Access-Control-Allow-Origin','*')
3.2.2 cors响应头部-Access-Control-Allow-Headers
默认情况下,cors仅支持客户端向服务器发送如下的9个请求头:
Accept , Accept-Language , Content-Language , DRP,Downlink , Save-Data,Viewport-Width, Width , Content-Type (值仅限于 text/plain , multipart/form-data , application/x-www-form-urlencoded 三者之一)
如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过Access-Control-Allow-Headers对额外的请求头进行声明,否则这次请求会失败
// 允许客户端额外向服务器发送Content-Type请求头和X-Custom-Header请求头
// 注意:多个请求头之间使用英文的逗号进行分割
res.setHeader('Access-Control-Allow-Headers','Content-Type,X-Custom-Header')
3.2.3 cors响应头部-Access-Control-Allow-Methods
默认情况下,cors仅支持客户端发起GET,POST,HEAD 请求,
如果客户端希望通过PUT,DELETE等方法请求服务器的资源,则需要在服务器端,通过Access-Control-Allow-Headers来指明实际请求所允许使用的HTTP方法
//只允许POST,GET,DELETE,HEAD请求方法
res.setHeader('Access-Control-Allow-Methods','POST,GET,DELETE,HEAD')
//允许所有的HTTP请求方法
res.setHeader('Access-Control-Allow-Methods','*')
3.3cors请求的分类
- 简单请求:
- 同时满足以下两大条件的请求: 请求方式为
GET,POST,HEAD之一,HTTP头部信息不超过默认的9个请求头
- 同时满足以下两大条件的请求: 请求方式为
- 预检请求:
- 向服务器发送了application/json格式的数据
- GET,POST,HEAD请求方式之外的请求类型
- 请求头中包含
自定义头部字段
在浏览器与服务器正式通信之前,浏览器会先发送OPTION请求进行遇见,以获知服务器是否允许该师级请求,所以这一次的OPTION请求被称作预检请求,服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据
简单请求和预检请求的区别
简单请求:客户端与服务器之间只会发生一次请求
预检请求:客户端与服务器之间会发生两次请求,OPTION预检请求成功之后,才会发起真正的请求
4.JSONP接口
浏览器通过<script>标签的src属性,请求服务器上的数据,同时,服务器返回一个函数的调用,这种请求数据的方式叫做JSONP
特点:
- JSONP不属于真正的Ajax请求,因为它没有使用
XMLHttpRequest这个对象 - JSONP仅支持GET请求,不支持其他请求
4.1 创建JSONP接口的注意事项
如果项目中配置了cors跨域资源共享为了防止冲突,必须在配置cors中间件之前声明JSONP的接口,否则JSONP接口会被处理成开启了cors的接口
// 必须在配置cors中间件之前,配置JSONP的接口
app.get('/api/jsonp',(req,res)=>{
// TODO:
})
// 一定要在路由之前配置cors这个中间件,从而解决跨域的问题
const cors = require("cors");
app.use(cors());
实现JSONP接口的步骤
获取客户端发送过来的回调函数的名字得到要通过JSONP形式发送给客户端的数据- 根据前两部得到的数据,
拼接出一个函数调用的字符串 - 将上一步得到的字符串,响应给客户端的
<script>标签进行执行
// 必须在配置cors中间件之前,配置JSONP的接口
app.get('/api/jsonp',(req,res)=>{
// TODO:定义JSONP接口的具体实现过程
//1.得到函数的名称
const funcName = req.query.callback
//2.定义要发送到客户端的数据对象
const data = {name:'zs',age:22}
//3.拼接出一个函数的调用
const scriptStr = `${funcName}(${JSON.stringify(data)})`
//4.把拼接的字符串,响应给客户端
res.send(scriptStr)
})
验证jsonp接口是否可以成功访问
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
</head>
<body>
<button id="btnJsonp">JSONP</button>
<script>
$(function () {
// 4.测试JSONP请求接口
$('#btnJsonp').on('click', function () {
$.ajax({
type: 'GET',
url: 'http://127.0.0.1/api/jsonp',
dataType:'jsonp',
success: function (res) {
console.log('res', res)
}
})
})
})
</script>
</body>
</html>