一,什么是Express?
Express 是一个简洁而灵活的 node.js Web应用框架, 提供一系列强大特性帮助你创建各种Web应用。Express 不对 node.js 已有的特性进行二次抽象,我们只是在它之上扩展了Web应用所需的功能。丰富的HTTP工具以及来自Connect框架的中间件随取随用,创建强健、友好的API变得快速又简单。
官网:Express - 基于 Node.js 平台的 web 应用开发框架 - Express中文文档 | Express中文网
二,安装Express
npm install express --save
三,使用实例
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
四,基本路由
路由的概念:路由就是映射关系
express的路由:指的是客户端的请求与服务器处理函数之间的映射关系。
express中的路由分3部分组成,分别是请求的类型、请求的url地址、请求函数
格式:
app.请求类型(请求地址,请求函数)
//用Hello World回应!在主页上:
app.get('/', function (req, res) {
res.send('Hello World!')
})
//响应根路由(/)上的POST请求,应用程序的主页:
app.post('/', function (req, res) {
res.send('Got a POST request')
})
//响应对/user路由的PUT请求:
app.put('/user', function (req, res) {
res.send('Got a PUT request at /user')
})
//响应对/user路由的DELETE请求:
app.delete('/user', function (req, res) {
res.send('Got a DELETE request at /user')
})
模块化路由:
为了方便对路由进行模块化管理,express不建议将路由直接挂载到app上,而是推荐将路由抽离为单独的模块。
步骤如下:
(1)创建路由模块对应的js文件
(2)调用express.Router()函数创建路由实例对象
(3)向路由对象上挂载具体的路由
(4)使用module.exports向外共享路由
(5)使用app.use()函数注册路由模块
下面是路由模块的js代码(router.js)
const express = require('express')
// 1、创建路由对象
const router = express.Router()
// 2、挂载路由
router.get('/user/list', (req, res) => {
res.send('get user list')
})
router.post('/user/add', (req, res) => {
res.send('add new user')
})
// 3、向外到处路由
module.exports = router
五,利用Express托管静态文件
为了提供诸如图像、CSS 文件和 JavaScript 文件之类的静态文件,请使用 Express 中的 express.static
内置中间件函数。
此函数特征如下:
express.static(root, [options])
根参数指定为静态资产提供服务的根目录。有关options参数的更多信息,请参见express.static。
例如,通过如下代码就可以将 public
目录下的图片、CSS 文件、JavaScript 文件对外开放访问了:
app.use(express.static('public'))
现在,你就可以访问 public
目录中的所有文件了:
http://localhost:3000/images/kitten.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/app.js
http://localhost:3000/images/bg.png
http://localhost:3000/hello.html
Express 在静态目录查找文件,因此,存放静态文件的目录名不会出现在 URL 中。
如果要使用多个静态资源目录,请多次调用 express.static
中间件函数:
app.use(express.static('public'))
app.use(express.static('files'))
访问静态资源文件时,express.static
中间件函数会根据目录的添加顺序查找所需的文件。
注意:为了获得最佳效果,请使用反向代理缓存来提高为静态资产提供服务的性能。
要为express.static函数提供服务的文件创建虚拟路径前缀(文件系统中实际上不存在路径),请为静态目录指定装载路径,如下所示:
app.use('/static', express.static('public'))
现在,你就可以通过带有 /static
前缀地址来访问 public
目录中的文件了。
http://localhost:3000/static/images/kitten.jpg
http://localhost:3000/static/css/style.css
http://localhost:3000/static/js/app.js
http://localhost:3000/static/images/bg.png
http://localhost:3000/static/hello.html
然而,提供给express.static函数的路径是相对于启动节点进程的目录的。如果您从另一个目录运行express应用程序,使用您要服务的目录的绝对路径会更安全:
const path = require('path')
app.use('/static', express.static(path.join(__dirname, 'public')))
六,开发中间件
编写用于Express应用程序的中间件
概述
中间件函数是可以访问请求对象(req)、响应对象(res)和应用程序请求-响应周期中的下一个函数的函数。下一个函数是Express路由器中的一个函数,当被调用时,它将执行当前中间件之后的中间件。
中间件功能可以执行以下任务:
1. 执行任何代码。2.对请求和响应对象进行更改。3.结束请求-响应周期。4.调用堆栈中的下一个中间件。
如果当前的中间件函数没有结束请求-响应周期,它必须调用next()将控制权传递给下一个中间件函数。否则,请求将被挂起。
下图显示了中间件函数调用的元素:
应用中间件功能的HTTP方法。(.get)
中间件功能应用的路径(路由)('/')。
中间件功能。(function)
中间件函数的回调参数,按惯例称为“next”。(next)
中间件函数的HTTP响应参数,按惯例称为“res”。(res)
中间件函数的HTTP请求参数,按惯例称为“req”。(req)
从Express5开始,返回Promise的中间件函数在拒绝或抛出错误时将调用next(value)。next将使用被拒绝的值或抛出的Error进行调用。
实例
下面是一个简单的“Hello World”Express应用程序的示例。本文的其余部分将定义三个中间件函数并将其添加到应用程序中:一个名为myLogger,用于打印简单的日志消息,一个称为requestTime,用于显示HTTP请求的时间戳,另一个名称为validateCookies,用于验证传入的cookie。
var express = require('express')
var app = express()
app.get('/', function (req, res) {
res.send('Hello World!')
})
app.listen(3000)
中间件函数myLogger
下面是一个名为“myLogger”的中间件函数的简单示例。当应用程序的请求通过该函数时,该函数只打印“LOGGED”。中间件函数被分配给一个名为myLogger的变量。
var myLogger = function (req, res, next) {
console.log('LOGGED')
next()
}
注意上面对next()的调用。调用此函数会调用应用程序中的下一个中间件函数。next()函数不是Node.js或Expressneneneba API的一部分,而是传递给中间件函数的第三个参数。next()函数可以被命名为任何名称,但按照惯例,它总是被命名为“next”。为了避免混淆,请始终使用此约定。
要加载中间件函数,请调用app.use(),指定中间件函数。例如,以下代码在路由到根路径(/)之前加载myLogger中间件函数。
var express = require('express')
var app = express()
var myLogger = function (req, res, next) {
console.log('LOGGED')
next()
}
app.use(myLogger)
app.get('/', function (req, res) {
res.send('Hello World!')
})
app.listen(3000)
每次应用程序收到请求时,都会将消息“LOGGED”打印到终端。
中间件加载的顺序很重要:首先加载的中间件功能也会首先执行。
如果myLogger是在路由到根路径后加载的,则请求永远不会到达它,应用程序也不会打印“LOGGED”,因为根路径的路由处理程序会终止请求-响应周期。
中间件函数myLogger只需打印一条消息,然后通过调用next()函数将请求传递给堆栈中的下一个中间件函数。
中间件函数requestTime
接下来,我们将创建一个名为“requestTime”的中间件函数,并向请求对象添加一个称为requestTime的属性。
var requestTime = function (req, res, next) {
req.requestTime = Date.now()
next()
}
该应用程序现在使用requestTime中间件功能。此外,根路径路由的回调函数使用中间件函数添加到req(请求对象)中的属性。
var express = require('express')
var app = express()
var requestTime = function (req, res, next) {
req.requestTime = Date.now()
next()
}
app.use(requestTime)
app.get('/', function (req, res) {
var responseText = 'Hello World!<br>'
responseText += '<small>Requested at: ' + req.requestTime + '</small>'
res.send(responseText)
})
app.listen(3000)
当您向应用程序的根目录发出请求时,应用程序现在会在浏览器中显示您请求的时间戳。
中间件功能验证Cookies
最后,我们将创建一个中间件函数,用于验证传入的cookie,并在cookie无效时发送400响应。
下面是一个使用外部异步服务验证cookie的示例函数。
async function cookieValidator (cookies) {
try {
await externallyValidateCookie(cookies.testCookie)
} catch {
throw new Error('Invalid cookies')
}
}
在这里,我们使用cookie解析器中间件来解析req对象中传入的cookie,并将它们传递给我们的cookieValidator函数。validateCookies中间件返回一个Promise,一旦被拒绝,它将自动触发我们的错误处理程序。
var express = require('express')
var cookieParser = require('cookie-parser')
var cookieValidator = require('./cookieValidator')
var app = express()
async function validateCookies (req, res, next) {
await cookieValidator(req.cookies)
next()
}
app.use(cookieParser())
app.use(validateCookies)
// error handler
app.use(function (err, req, res, next) {
res.status(400).send(err.message)
})
app.listen(3000)
注意在等待cookieValidator(req.cookies)之后是如何调用next()的。这确保了如果cookieValidater解析,堆栈中的下一个中间件将被调用。如果您将任何内容传递给next()函数(字符串“route”或“router”除外),Express会将当前请求视为错误,并将跳过任何剩余的非错误处理路由和中间件函数。
因为您可以访问请求对象、响应对象、堆栈中的下一个中间件函数以及整个Node.js API,所以中间件函数的可能性是无限的。
有关Express中间件的更多信息,请参阅:使用Express中间件。
可配置中间件
如果您需要中间件是可配置的,请导出一个接受选项对象或其他参数的函数,然后根据输入参数返回中间件实现。
文件:my-midleware.js
module.exports = function (options) {
return function (req, res, next) {
// Implement the middleware function based on the options object
next()
}
}
现在可以使用中间件,如下所示。
var mw = require('./my-middleware.js')
app.use(mw({ option1: '1', option2: '2' }))
有关可配置中间件的示例,请参阅cookie会话和压缩。
七,使用中间件
使用中间件
Express是一个路由和中间件web框架,其自身的功能很少:Express应用程序本质上是一系列中间件函数调用。
中间件功能是可以访问请求对象(req)、响应对象(res)和应用程序请求-响应周期中的下一个中间件功能的功能。下一个中间件函数通常由一个名为next的变量表示。
中间件功能可以执行以下任务:
1.执行任何代码。2.对请求和响应对象进行更改。3.结束请求-响应周期。4.调用堆栈中的下一个中间件函数。
如果当前的中间件函数没有结束请求-响应周期,它必须调用next()将控制权传递给下一个中间件函数。否则,请求将被挂起。
Express应用程序可以使用以下类型的中间件:
1.应用程序级中间件;2.路由器级中间件;3.错误处理中间件;4.内置中间件;5.第三方中间件
您可以使用可选的装载路径加载应用程序级和路由器级中间件。您还可以一起加载一系列中间件功能,这将在挂载点创建中间件系统的子堆栈。
应用程序级中间件
使用app.use()和app将应用程序级中间件绑定到应用程序对象的实例。METHOD()函数,其中METHOD是中间件函数以小写形式处理的请求(如GET、PUT或POST)的HTTP方法。
此示例显示了一个没有装载路径的中间件函数。每当应用程序收到请求时,都会执行该功能。
var express = require('express')
var app = express()
app.use(function (req, res, next) {
console.log('Time:', Date.now())
next()
})
此示例显示了安装在/user/:id路径上的中间件函数。该函数针对/user/:id路径上的任何类型的HTTP请求执行。
app.use('/user/:id', function (req, res, next) {
console.log('Request Type:', req.method)
next()
})
此示例显示了一个路由及其处理程序功能(中间件系统)。该函数处理对/user/:id路径的GET请求。
app.get('/user/:id', function (req, res, next) {
res.send('USER')
})
这里是一个在装载点加载一系列中间件功能的示例,带有装载路径。它展示了一个中间件子堆栈,它将任何类型的HTTP请求的请求信息打印到/user/:id路径。
app.use('/user/:id', function (req, res, next) {
console.log('Request URL:', req.originalUrl)
next()
}, function (req, res, next) {
console.log('Request Type:', req.method)
next()
})
路由处理程序使您能够为一条路径定义多个路由。下面的示例定义了GET请求到/user/:id路径的两个路由。第二条路由不会引起任何问题,但它永远不会被调用,因为第一条路由结束了请求-响应周期。
此示例显示了一个中间件子堆栈,用于处理对/user/:id路径的GET请求。
app.get('/user/:id', function (req, res, next) {
console.log('ID:', req.params.id)
next()
}, function (req, res, next) {
res.send('User Info')
})
// handler for the /user/:id path, which prints the user ID
app.get('/user/:id', function (req, res, next) {
res.send(req.params.id)
})
要跳过路由器中间件堆栈中的其余中间件函数,请调用next('route')将控制权传递给下一个路由。注意:next(“route”)将仅在使用应用程序加载的中间件功能中工作。METHOD()或路由器。METHOD()函数。
此示例显示了一个中间件子堆栈,用于处理对/user/:id路径的GET请求。
app.get('/user/:id', function (req, res, next) {
// if the user ID is 0, skip to the next route
if (req.params.id === '0') next('route')
// otherwise pass the control to the next middleware function in this stack
else next()
}, function (req, res, next) {
// send a regular response
res.send('regular')
})
// handler for the /user/:id path, which sends a special response
app.get('/user/:id', function (req, res, next) {
res.send('special')
})
中间件也可以在数组中声明,以便于重用。
此示例显示了一个带有中间件子堆栈的数组,该子堆栈处理对/user/:id路径的GET请求
function logOriginalUrl (req, res, next) {
console.log('Request URL:', req.originalUrl)
next()
}
function logMethod (req, res, next) {
console.log('Request Type:', req.method)
next()
}
var logStuff = [logOriginalUrl, logMethod]
app.get('/user/:id', logStuff, function (req, res, next) {
res.send('User Info')
})
路由器级中间件
路由器级中间件的工作方式与应用程序级中间件相同,只是它绑定到express的实例。路由器()。
var router = express.Router()
使用router.use()和router加载路由器级中间件。METHOD()函数。
以下示例代码通过使用路由器级中间件复制了上面所示的应用程序级中间件的中间件系统:
var express = require('express')
var app = express()
var router = express.Router()
//一个没有挂载路径的中间件功能。对路由器的每个请求都执行此代码
router.use(function (req, res, next) {
console.log('Time:', Date.now())
next()
})
//中间件子堆栈向/user/:id路径显示任何类型的HTTP请求的请求信息
router.use('/user/:id', function (req, res, next) {
console.log('Request URL:', req.originalUrl)
next()
}, function (req, res, next) {
console.log('Request Type:', req.method)
next()
})
//一个中间件子堆栈,用于处理对/user/:id路径的GET请求
router.get('/user/:id', function (req, res, next) {
//如果用户ID为0,则跳到下一个路由器
if (req.params.id === '0') next('route')
//否则,将控制权传递给该堆栈中的下一个中间件函数
else next()
}, function (req, res, next) {
//呈现常规页面
res.render('regular')
})
///user/:id路径的处理程序,用于呈现特殊页面
router.get('/user/:id', function (req, res, next) {
console.log(req.params.id)
res.render('special')
})
//在应用程序上安装路由器
app.use('/', router)
要跳过路由器的其余中间件功能,请调用next('router')将控制权传递回路由器实例。
此示例显示了一个中间件子堆栈,用于处理对/user/:id路径的GET请求。
var express = require('express')
var app = express()
var router = express.Router()
//在需要时用检查和保释来断言路由器
router.use(function (req, res, next) {
if (!req.headers['x-auth']) return next('router')
next()
})
router.get('/user/:id', function (req, res) {
res.send('hello, user!')
})
//使用路由器和401任何失败的东西
app.use('/admin', router, function (req, res) {
res.sendStatus(401)
})
错误处理中间件
错误处理中间件总是需要四个参数。您必须提供四个参数来将其标识为错误处理中间件函数。即使不需要使用下一个对象,也必须指定它来维护签名。否则,下一个对象将被解释为常规中间件,并且将无法处理错误。
以与其他中间件函数相同的方式定义错误处理中间件函数,除了使用四个参数而不是三个参数,特别是使用签名(err,req,res,next):
app.use(function (err, req, res, next) {
console.error(err.stack)
res.status(500).send('Something broke!')
})
有关错误处理中间件的详细信息,请参阅:错误处理。
内置中间件
从4.x版本开始,Express不再依赖Connect。以前包含在Express中的中间件功能现在位于单独的模块中;请参阅中间件功能列表。
Express具有以下内置中间件功能:
express.static提供静态资产,如HTML文件、图像等。
express.json使用json有效载荷解析传入请求。注:Express 4.16.0提供+
express.urlencoded使用URL编码的有效载荷解析传入请求。注:Express 4.16.0提供+
第三方中间件
使用第三方中间件为Express应用程序添加功能。
为所需的功能安装Node.js模块,然后在应用程序级别或路由器级别将其加载到应用程序中。
下面的示例说明如何安装和加载cookie解析中间件函数cookie解析器。
$ npm install cookie-parser
var express = require('express')
var app = express()
var cookieParser = require('cookie-parser')
//加载cookie解析中间件
app.use(cookieParser())
有关Express常用的第三方中间件功能的部分列表,请参阅:第三方中间软件。
八,错误处理
错误处理是指Express如何捕获和处理同步和异步发生的错误。Express附带了一个默认的错误处理程序,因此您不需要编写自己的错误处理软件即可开始。
捕捉错误
确保Express捕获运行路由处理程序和中间件时发生的所有错误非常重要。
在路由处理程序和中间件内部的同步代码中发生的错误不需要额外的工作。如果同步代码抛出错误,Express将捕获并处理它。例如:
app.get('/', function (req, res) {
throw new Error('BROKEN') // Express will catch this on its own.
})
对于路由处理程序和中间件调用的异步函数返回的错误,必须将它们传递给next()函数,Express将在其中捕获并处理它们。例如:
app.get('/', function (req, res, next) {
fs.readFile('/file-does-not-exist', function (err, data) {
if (err) {
next(err) // Pass errors to Express.
} else {
res.send(data)
}
})
})
从Express5开始,返回Promise的路由处理程序和中间件将在拒绝或抛出错误时自动调用next(value)。例如:
app.get('/user/:id', async function (req, res, next) {
var user = await getUserById(req.params.id)
res.send(user)
})
如果getUserById抛出错误或拒绝,那么next将使用抛出的错误或拒绝的值进行调用。如果没有提供拒绝的值,next将使用Express路由器提供的默认Error对象进行调用。
如果您将任何内容传递给next()函数(字符串“route”除外),Express会将当前请求视为错误,并将跳过任何剩余的非错误处理路由和中间件函数。
如果序列中的回调不提供数据,只提供错误,则可以按如下方式简化此代码:
app.get('/', [
function (req, res, next) {
fs.writeFile('/inaccessible-path', 'data', next)
},
function (req, res) {
res.send('OK')
}
])
在上面的示例中,next作为fs.writeFile的回调提供,无论调用时是否有错误。如果没有错误,则执行第二个处理程序,否则Express将捕获并处理该错误。
您必须捕获路由处理程序或中间件调用的异步代码中发生的错误,并将它们传递给Express进行处理。例如:
app.get('/', function (req, res, next) {
setTimeout(function () {
try {
throw new Error('BROKEN')
} catch (err) {
next(err)
}
}, 100)
})
上面的例子使用了一个try。。。catch块捕获异步代码中的错误并将其传递给Express。如果尝试。。。catch块被省略,Express将不会捕获错误,因为它不是同步处理程序代码的一部分。
使用承诺来避免尝试的开销。。。catch块或使用返回promise的函数时。例如:
app.get('/', function (req, res, next) {
Promise.resolve().then(function () {
throw new Error('BROKEN')
}).catch(next) // Errors will be passed to Express.
})
由于promise会自动捕获同步错误和被拒绝的promise,所以您可以简单地提供next作为最终捕获处理程序,Express将捕获错误,因为捕获处理程序将错误作为第一个参数。
您还可以使用一系列处理程序来依赖同步错误捕获,将异步代码简化为一些琐碎的事情。例如:
app.get('/', [
function (req, res, next) {
fs.readFile('/maybe-valid-file', 'utf-8', function (err, data) {
res.locals.data = data
next(err)
})
},
function (req, res) {
res.locals.data = res.locals.data.split(',')[1]
res.send(res.locals.data)
}
])
上面的例子有几个来自readFile调用的琐碎语句。如果readFile导致错误,那么它会将错误传递给Express,否则您将在链中的下一个处理程序中快速返回到同步错误处理的世界。然后,上面的示例尝试处理数据。如果失败,则同步错误处理程序将捕获它。如果您在readFile回调内完成了此处理,则应用程序可能会退出,Express错误处理程序也不会运行。
无论使用哪种方法,如果希望调用Express错误处理程序并使应用程序继续运行,都必须确保Express接收到错误。
默认错误处理程序
Express提供了一个内置的错误处理程序,可以处理应用程序中可能遇到的任何错误。这个默认的错误处理中间件功能添加在中间件功能堆栈的末尾。
如果您将错误传递给next(),而您没有在自定义错误处理程序中处理它,它将由内置的错误处理程序处理;错误将与堆栈跟踪一起写入客户端。堆栈跟踪不包括在生产环境中。
将环境变量NODE_ENV设置为生产,以在生产模式下运行应用程序。
写入错误时,会将以下信息添加到响应中:
res.statusCode是从err.status(或err.statusCode)设置的。如果此值超出4xx或5xx范围,则会设置为500。
res.statusMessage是根据状态代码设置的。
当在生产环境中时,正文将是状态代码消息的HTML,否则将是err.stack。
在err.headers对象中指定的任何标头。
如果在开始写入响应后调用next()并返回错误(例如,如果在将响应流式传输到客户端时遇到错误),Express默认错误处理程序将关闭连接并使请求失败。
因此,当添加自定义错误处理程序时,当标头已发送到客户端时,必须委托给默认的Express错误处理程序:
function errorHandler (err, req, res, next) {
if (res.headersSent) {
return next(err)
}
res.status(500)
res.render('error', { error: err })
}
请注意,如果您多次调用代码中的next()来处理错误,即使有自定义的错误处理中间件,也会触发默认的错误处理程序。
写入错误处理程序
以与其他中间件函数相同的方式定义错误处理中间件函数,只是错误处理函数有四个参数而不是三个:(err,req,res,next)。例如:
app.use(function (err, req, res, next) {
console.error(err.stack)
res.status(500).send('Something broke!')
})
在其他app.use()和路由调用之后,您最后定义了错误处理中间件;例如:
var bodyParser = require('body-parser')
var methodOverride = require('method-override')
app.use(bodyParser.urlencoded({
extended: true
}))
app.use(bodyParser.json())
app.use(methodOverride())
app.use(function (err, req, res, next) {
// logic
})
中间件函数中的响应可以是任何格式,例如HTML错误页面、简单消息或JSON字符串。
出于组织(和更高级别的框架)的目的,您可以定义几个错误处理中间件函数,就像使用常规中间件函数一样。例如,为使用XHR发出的请求和不使用XHR的请求定义错误处理程序:
var bodyParser = require('body-parser')
var methodOverride = require('method-override')
app.use(bodyParser.urlencoded({
extended: true
}))
app.use(bodyParser.json())
app.use(methodOverride())
app.use(logErrors)
app.use(clientErrorHandler)
app.use(errorHandler)
在本例中,通用logErrors可能会将请求和错误信息写入stderr,例如:
function logErrors (err, req, res, next) {
console.error(err.stack)
next(err)
}
同样在本例中,clientErrorHandler的定义如下:;在这种情况下,错误会显式地传递给下一个错误。
请注意,在错误处理函数中不调用“next”时,您负责编写(和结束)响应。否则,这些请求将“挂起”,并且不符合垃圾收集的条件。
function clientErrorHandler (err, req, res, next) {
if (req.xhr) {
res.status(500).send({ error: 'Something failed!' })
} else {
next(err)
}
}
实现“catch-all”errorHandler函数如下
function errorHandler (err, req, res, next) {
res.status(500)
res.render('error', { error: err })
}
如果您有一个具有多个回调函数的路由处理程序,则可以使用route参数跳到下一个路由处理程序。例如:
app.get('/a_route_behind_paywall',
function checkIfPaidSubscriber (req, res, next) {
if (!req.user.hasPaid) {
// continue handling this request
next('route')
} else {
next()
}
}, function getPaidContent (req, res, next) {
PaidContent.find(function (err, doc) {
if (err) return next(err)
res.json(doc)
})
})
在本例中,将跳过getPaidContent处理程序,但应用程序中/a_route_behind_paywall的任何剩余处理程序都将继续执行。
对next()和next(err)的调用指示当前处理程序已完成以及处于何种状态。next(err)将跳过链中的所有剩余处理程序,但如上所述设置为处理错误的处理程序除外。
九,注意事项
使用中间件的5个注意事项;
(1)一定要在路由之前注册中间件
(2)客户端发送过来的请求,可以连续调用多个中间件进行处理
(3)执行完中间件的业务代码之后,必须要调用next()函数
(4)在next()函数后,不要写其他的代码
(5)连续调用多个中间件时,多个中间件之间,共享req和res对象