Bootstrap

Node.js:Express 中间件 & 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

两个请求都触发了中间件,并且中间件比路由先执行。

中间件之间又要如何传递参数?在从收到请求到发送响应期间,所有的中间件共享同一个reqres对象

因此上游的中间件可以把属性或方法添加到这两个对象中,然后下游的中间件只需要访问这两个对象就可以拿到参数。

示例:

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注册中间件,而是把中间件注册到某个路由上,称为局部中间件,这种中间件只在某个路由触发时执行。

注册局部中间件直接将中间件函数写入到getpost方法中:

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(){})

第一种是直接传入多个中间件函数,第二种是把多个中间件函数作为一个数组进行传入。执行顺序从前往后。

一些中间件的注意事项:

  1. 中间件必须在路由之前注册
  2. 所有中间件必须调用next()方法
  3. next()方法后面不要再写其他逻辑,作为整个函数的结尾

分类

Express官方将中间件的用法,分为了五大类:

  1. 应用级中间件
  2. 路由级中间件
  3. 错误级中间件
  4. Express内置中间件
  5. 第三方中间件

应用级中间件

只要中间件被绑定到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 /userpost /user

输出结果:

在这里插入图片描述

此时两个请求都失败了,因为打开文件采用的是file协议,而请求的urlhttp协议,两者协议不同,构成跨域。

第三方封装了一个Express中间件,提供了非常方便的跨域解决方案CORS

  1. 安装中间件:
npm i -g cors
  1. 导入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指定了允许访问该资源的请求头,默认情况下包含以下九种请求头:

  1. Accept
  2. Accept-Language
  3. Content-Language
  4. DPR
  5. Downlink
  6. Save-Data
  7. Viewport-Width
  8. Width
  9. Content-Type

如果请求头不在这九种类型中,就会请求失败。

如果希望服务端能够接收其他类型的请求,就需要通过Access-Control-Allow-Headers属性。

res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header')

在设置头部时,第二个参数填入允许被请求的头部,以逗号分隔,这些头部就可以被申请了。

Access-Control-Allow-Methods 响应头

默认情况下,CORS只允许客户端发起GETPOSTHEAD请求。如果客户端希望使用其它的请求类型,比如PUTDELETE,就需要使用Access-Control-Allow-Methods

res.setHeader('Access-Control-Allow-Methods ', 'PUT, DELETE')

同样的,将允许访问的方法填写到第二个参数中,以逗号分隔多个方法。

如果允许所有方法的访问,那么第二个参数指定为通配符"*"

那么以上三个响应头有什么用?

当浏览器接收到响应时,如果该响应跨域了,就会去检测上述响应头部,查看自己的请求是否符合要求,如果符合要求,那么允许客户端接收该响应

cors这个包,就是修改了以上内容,使得客户端可以跨域访问服务端资源。


预检请求

在使用CORS发起请求时,分为简单请求和预检请求。

如果满足以下条件,则为简单请求:

  1. 请求方式为GETPOSTHEAD之一
  2. HTTP头部信息不超过之前的九个字段
  3. 该请求是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请求类型,并且允许*所有源发来的请求。

浏览器检测到自己符合条件,于是发送第二个数据请求。


;