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指定了允许访问该资源的请求头,默认情况下包含以下九种请求头:
AcceptAccept-LanguageContent-LanguageDPRDownlinkSave-DataViewport-WidthWidthContent-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请求类型,并且允许*所有源发来的请求。
浏览器检测到自己符合条件,于是发送第二个数据请求。
