Node.js:Express 中间件 & CORS
- 中间件
- 全局中间件
- 局部中间件
- 分类
- 错误级中间件
- 内置中间件
- CORS
- 原理
- 预检请求
中间件
中间件是不直接接收请求,也不直接发送响应,而是在这之间处理一个中间过程的组件。
当一个请求到来,会经过多个中间件进行处理,后一个中间件拿到前一个中间件的处理结果,进行再处理,处理完毕后发给下一个中间件,以此类推。直到所有任务执行完毕,最后得到一个响应,再发送回给客户端。
在express
中,所谓的中间件不过就是一个函数,接收参数返回结果。
全局中间件函数定义:
function (req, res, next){next()
}
其接收三个参数,前两个参数与请求的响应函数一致,next
是中间件必须有的参数,并且在中间件函数的末尾,必须调用next()
方法,这样才会调用下一个中间件函数。
全局中间件
定义好中间件函数后,可以通过app.use
将其注册到服务中。
app.use(Middleware)
其中Middleware
是一个中间件函数。
这种被直接注册到app.use
上的中间件,称为全局生效中间件
,客户端发起的任何请求,都会触发全局中间件。
app.use(function (req, res, next){console.log("Middleware running...")next()
})app.get('/', function (req, res){console.log("get / success")
})app.get('/index.html', function (req, res){console.log("get /index.html success")
})app.listen(80, () => {console.log("create web server success")
})
以上服务,定义了一个匿名的中间件函数,并且注册到app.use
中,两个响应函数分别响应/
和/index.html
。
在浏览器中访问这两个地址,查看控制台:
Middleware running...
get / success
Middleware running...
get /index.html success
两个请求都触发了中间件,并且中间件比路由先执行。
中间件之间又要如何传递参数?在从收到请求到发送响应期间,所有的中间件共享同一个req
和res
对象!
因此上游的中间件可以把属性或方法添加到这两个对象中,然后下游的中间件只需要访问这两个对象就可以拿到参数。
示例:
app.use(function (req, res, next){console.log("Middleware running...")req.sendStr = 'hello world!'next()
})app.get('/', function (req, res){console.log("get / success")res.send(req.sendStr)
})app.get('/index.html', function (req, res){console.log("get /index.html success")res.send(req.sendStr)
})
这个代码,在第一个中间件处,给req
添加了一个对象sendStr = 'hello world!'
,在最后的路由函数中,就可以直接获取req.sendStr
并发送出去。
如果要定义多个中间件,只需要多次使用app.use
注册即可:
app.use(function (req, res, next){console.log("Middleware 1 running...")next()
})app.use(function (req, res, next){console.log("Middleware 2 running...")next()
})app.get('/', function (req, res){console.log("get / success")
})
多个中间件会以定义的顺序依次执行,访问/
的输出结果:
Middleware 1 running...
Middleware 2 running...
get / success
可以看到,先执行了Middleware 1
后执行Middleware 2
,最后执行路由函数。
局部中间件
如果不使用app.use
注册中间件,而是把中间件注册到某个路由上,称为局部中间件
,这种中间件只在某个路由触发时执行。
注册局部中间件直接将中间件函数写入到get
,post
方法中:
app.get('url', Middleware, function(){})
app.post('url', Middleware, function(){})
示例:
const vm1 = function(req, res, next){console.log("Middleware 1 running...")next()
}app.get('/', vm1, function (req, res){console.log("get / success")
})app.get('/index.html', function (req, res){console.log("get /index.html success")
})app.listen(80, () => {console.log("create web server success")
})
以上代码为get /
路由绑定了中间件vm1
,但是get /index.html
没有绑定。
访问get /
:
Middleware 1 running...
get / success
访问get /index.html
:
get /index.html success
此时只有get /
触发了局部中间件。
如果要定义多个局部中间件,有两种形式:
app.get('url', Middleware1, Middleware2, function(){})
app.post('url', [Middleware1, Middleware2], function(){})
第一种是直接传入多个中间件函数,第二种是把多个中间件函数作为一个数组进行传入。执行顺序从前往后。
一些中间件的注意事项:
- 中间件必须在路由之前注册
- 所有中间件必须调用
next()
方法 next()
方法后面不要再写其他逻辑,作为整个函数的结尾
分类
Express
官方将中间件的用法,分为了五大类:
- 应用级中间件
- 路由级中间件
- 错误级中间件
Express
内置中间件- 第三方中间件
应用级中间件:
只要中间件被绑定到app
上,就是应用级中间件,先前讲解的两个全局和局部中间件,都属于应用级中间件。
路由级中间件:
如果中间件被绑定到express.Router
对象上,那么就是路由级中间件。
示例:
const app = express()
const router = express.Router()// 路由级中间件
router.use(function (req, res, next){next()
})app.use('/', router)
在博客 [Node.js:Express 服务 & 路由] 讲解路由模块化时,讲解过这个对象,如果想把路由进行模块化,就在一个新的模块中专门绑定路由到这个Router
对象上,然后再把这个对象共享给外部。
错误级中间件
错误级中间件专门用于捕获整个项目发送的异常错误,防止项目崩溃。
函数格式:
function (err, req, res, next){next()
}
在基本的中间件函数上,第一个参数增加一个err
参数,用于捕获全局的异常。
示例:
const express = require('express')
const app = express()app.get('/', function (req, res){throw new Error(' / create a error!') // 抛出异常res.send('success')
})// 注册错误级中间件
app.use(function (err, req, res, next){res.send('something happen: ' + err.message)
})app.listen(80, () => {console.log("create web server success")
})
以上代码,在访问get /
时,会抛出一个异常,如果不处理项目就崩溃了。
随后为该服务注册了一个错误级中间件,在中间件内部err
就是异常对象,直接把异常信息发送回给客户端。
注意:只有错误级别的中间件才可以在路由之后注册,其余的中间件都必须在路由前注册。
输出结果:
可以看到,此处得到的结果是错误信息,说明错误被处理了。
内置中间件
Express
内置了三个中间件,这些中间件可以快速完成某些功能:
express.static
:托管静态资源express.json
:解析json
格式的请求数据express.urlencoded
:解析URL-encoded
格式的请求数据
其中第一个中间件已经在之前详细讲解过了,接下来看看后两个中间件的功能:
启动如下服务:
const express = require('express')
const app = express()app.post('/user', function (req, res){console.log(req.body)
})app.listen(80, () => {console.log("create web server success")
})
其中post /user
路由,会把收到的请求的请求体输出到控制台。
使用postman
发送一个POST
请求,请求内容为一个json
字符串:
{"name": "张三","age": 18
}
控制台输出结果:
undefined
奇怪了,明明发送了一个json
字符串,为什么请求体得到的是一个undefined
?
如果不配置解析数据的中间件,那么req.body = undefined
。
而这个解析数据的中间件,就是express.json
或者express.urlencoding
。
express.json:
想要解析刚才的json
格式数据,只需要将express.json
注册到服务上即可:
const express = require('express')
const app = express()app.use(express.json()) // 注册处理数据的中间件app.post('/user', function (req, res){console.log(req.body)
})app.listen(80, () => {console.log("create web server success")
})
再次发送相同的请求,控制台输出结果就是正确的字符串了。
express.urlencoded:
在postman
发送以下数据:
以键值对的形式发送数据,如果依然使用express.json
进行解析,虽然req.body
不是undefined
了,但是由于检测不到json
字符串,最后会得到一个空对象。
这种键值对形式的数据,就需要express.urlencoded
中间件了:
const express = require('express')
const app = express()app.use(express.urlencoded({ extended: false }))app.post('/user', function (req, res){console.log(req.body)
})app.listen(80, () => {console.log("create web server success")
})
使用urlencoded
时,要传入一个对象,属性值固定为extended: false
。
发起同样的请求,输出结果:
最后发送的数据,就被转化为了一个对象。
CORS
现有以下服务:
const express = require('express')
const app = express()app.get('/user', function (req, res){res.send(req.query)
})app.post('/user', express.urlencoded({ extended: false }),function (req, res){res.send(req.body)})app.listen(80, () => {console.log("create web server success")
})
这个服务接收一个get /usr
或者post /usr
请求,并把请求参数发送回给客户端。但是这样无法解决跨域问题。
在test.html
中编写以下代码:
<button id="btnGET">GET</button>
<button id="btnPOST">POST</button><script>// 1. 测试GET接口$('#btnGET').on('click', function () {$.ajax({type: 'GET',url: 'http://127.0.0.1/user',data: { name: '张三', age: 20 },success: function (res) {console.log(res)},})})// 2. 测试POST接口$('#btnPOST').on('click', function () {$.ajax({type: 'POST',url: 'http://127.0.0.1/user',data: { name: '张三', age: 20 },success: function (res) {console.log(res)},})})
</script>
通过点击按钮,分别发送get /user
和post /user
。
输出结果:
此时两个请求都失败了,因为打开文件采用的是file
协议,而请求的url
是http
协议,两者协议不同,构成跨域。
第三方封装了一个Express
中间件,提供了非常方便的跨域解决方案CORS
。
- 安装中间件:
npm i -g cors
- 导入
cors
中间件并注册到服务上:
const express = require('express')
const app = express()// 导入core中间件
const cors = require('cors')
app.use(cors()) // 注册app.get('/user', function (req, res){res.send(req.query)
})app.post('/user', express.urlencoded({ extended: false }), function (req, res){res.send(req.body)
})app.listen(80, () => {console.log("create web server success")
})
再次访问:
跨域问题瞬间就解决了。
原理
CORS
全称跨域资源共享,是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他源,使得浏览器允许这些源访问加载自己的资源。
在正常跨域访问时,服务器会正常收到来自浏览器的请求,并发出响应:
但是当浏览器检测到响应跨域,依照同源策略,那么就会拦截这个响应,导致客户端接收不到这个响应。
引入cors
中间件后,cors
会修改HTTP
响应头,解除浏览器的跨域访问限制。
Access-Control-Allow-Origin 响应头:
Access-Control-Allow-Origin
指定了允许访问该资源的外域URL
,只有符合要求的地址,才允许请求当前服务器。
res.setHeader('Access-Control-Allow-Origin', 'https://example')
以上代码,可以指定只有https://example
可以访问当前服务器。如果不希望限制任何客户端对服务的访问,那么第二个参数填入通配符*
。
res.setHeader('Access-Control-Allow-Origin', '*')
Access-Control-Allow-Headers 响应头:
Access-Control-Allow-Headers
指定了允许访问该资源的请求头,默认情况下包含以下九种请求头:
Accept
Accept-Language
Content-Language
DPR
Downlink
Save-Data
Viewport-Width
Width
Content-Type
如果请求头不在这九种类型中,就会请求失败。
如果希望服务端能够接收其他类型的请求,就需要通过Access-Control-Allow-Headers
属性。
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header')
在设置头部时,第二个参数填入允许被请求的头部,以逗号分隔,这些头部就可以被申请了。
Access-Control-Allow-Methods 响应头:
默认情况下,CORS
只允许客户端发起GET
、POST
、HEAD
请求。如果客户端希望使用其它的请求类型,比如PUT
、DELETE
,就需要使用Access-Control-Allow-Methods
。
res.setHeader('Access-Control-Allow-Methods ', 'PUT, DELETE')
同样的,将允许访问的方法填写到第二个参数中,以逗号分隔多个方法。
如果允许所有方法的访问,那么第二个参数指定为通配符"*"
。
那么以上三个响应头有什么用?
当浏览器接收到响应时,如果该响应跨域了,就会去检测上述响应头部,查看自己的请求是否符合要求,如果符合要求,那么允许客户端接收该响应。
而cors
这个包,就是修改了以上内容,使得客户端可以跨域访问服务端资源。
预检请求
在使用CORS
发起请求时,分为简单请求和预检请求。
如果满足以下条件,则为简单请求:
- 请求方式为
GET
、POST
、HEAD
之一 HTTP
头部信息不超过之前的九个字段- 该请求是
XMLHttpRequest
对象,且没有使用setRequestHeader()
方法注册自定义头部
当一个CORS
请求不符合简单请求的条件时,那么该请求就是预检请求。
注意:只有使用CORS
发起请求时,才分为简单请求和预检请求,如果没有使用CORS
,或者请求是同源的,那么不属于以上分类。
在CORS
中,浏览器要依据响应报文的头部字段,判断自己的请求是否合法,如果不合法那么就会触发同源策略,不允许客户端接收这个响应。
如果HTTP
的请求比较复杂,而这个响应由不符合条件,不被服务器接收,那么这个数据传输就是无效的,浪费了网络资源。
为此,对于较为复杂的请求,浏览器会先发送一个OPTION
预检请求,这个请求不携带任何内容。当服务器响应之后,读取响应头部中的字段,查看自己是否可以请求对应的资源,如果可以请求,那么再发送真正要请求的报文。
示例:
在html
页面中增加一个delete
按钮,发送DELETE
请求:
$('#btnDelete').on('click', function () {$.ajax({type: 'DELETE',url: 'http://127.0.0.1/user',success: function (res) {console.log(res)},})
})
在服务端配置接收DELETE
请求的路由:
const express = require('express')
const app = express()const cors = require('cors')
app.use(cors())app.delete('/user', express.urlencoded({ extended: false }), function (req, res){res.send(req.body)
})app.listen(80, () => {console.log("create web server success")
})
此处别忘了要绑定app.use(cors())
,否则接收不到这个请求
点击按钮发送请求,后台监控网络:
可以看到,总共发送了两个请求,第一个请求的大小是0 B
,这是预检请求,不携带任何数据,第二个请求才是真正的请求内容。
查看预检请求:
这个请求的类型是OPTION
,请求收到的响应中,包含两个重要字段:
access-control-allow-methods: GET,HEAD,PUT,PATCH,POST,DELETE
access-control-allow-origin: *
这代表服务器允许接受DELETE
请求类型,并且允许*
所有源发来的请求。
浏览器检测到自己符合条件,于是发送第二个数据请求。