中间件(Middleware),特指业务流程的中间处理环节。

当一个请求到达Express的服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理。

Express中间件的调用流程如下:

Express 中间件的调用流程

Express的中间件,本质上就是一个处理函数。Express中间件的格式如下:

Expres 中间件的格式

注:中间件函数的形参列表中,必须包含next函数。而路由的处理函数中只包含requestres)和responseres)。

利用中间件,可以实现后端中Filter(拦截器)或Interceptor(拦截器)等功能。

在中间件调用流程中,next()函数就是实现多个中间件连续调用的关键。next()表示将流转关系转交给下一个中间件或路由

中间件的作用:通过中间件,可以在多个中间件之间共享同一份requestresponse。在开发中,可以通过在上游的中间件中,统一为requestresponse对象添加自定义属性或方法(在上游做统一的处理),供下游的中间件或路由进行使用。


全局中间件

全局中间件是指客户端发起的任何请求,到达服务器之后,都会触发的中间件。通过app.user(globalMiddleware),就可以定义一个全局生效的中间件。这个全局生效的中间件,因为使用app.user()定义,没有指定请求方式、请求地址,所以任何到达服务器的请求,都会经过这个中间件。

如下所示:

const express = require('express');
const app = express()

// 定义中间件函数
const globalMiddleware = (req, res, next) => {
    console.log('Middleware function...');
    // 把流转关系转交给下一个中间件或路由
    next()
}

// 注册全局生效的中间件
app.use(globalMiddleware)

// 全局中间件无论什么请求方式、请求路径,都会被调用
app.get('/', (req, res) => {
    res.send('This is home page...')
})

app.post('/user', (req, res) => {
    res.send('Post user...')
})

app.listen(80, () => {
    console.log('Server running at http://127.0.0.1/');
})

定义中间件,可以直接在app.use()中定义并注册中间件函数:

app.use((req, res, next) => {
    console.log('Middleware function...');
    next()
})

全局中间件还可通过app.all()来进行定义和注册:

app.all('/*', (res, req, next) => {
    console.log('Middleware function...');
    next()
})

局部中间件

局部生效的中间件是指在特定的请求方式或请求路径下才会生效的中间件。

  • 特定请求路径生效的中间件:

    const express = require('express');
    const app = express()
    
    // 局部生效的中间件(只在请求路径为 /user 下才生效)
    app.use('/user', (req, res, next) => {
        console.log('Middleware function...');
        next()
    })
    
    app.get('/', (req, res) => {
        res.send('This is home page...')
    })
    
    // 局部中间件只有在特定的请求方式、请求路径下才能生效
    app.post('/user', (req, res) => {
        res.send('Post user...')
    })
    
    app.get('/user/:id', (req, res) => {
        res.send(`Get user by id ${req.params}...`)
    })
    
    app.listen(80, () => {
        console.log('Server running at http://127.0.0.1/');
    })
    
  • 特定请求方式生效的中间件:

    const express = require('express');
    const app = express()
    
    // 局部生效的中间件(只在请求方式为 GET 下才生效)
    app.get('/*', (req, res, next) => {
        console.log('Middleware function...');
        // 把流转关系转交给下一个中间件或路由
        next()
    })
    
    // 局部中间件只有在特定的请求方式、请求路径下才能生效
    app.get('/', (req, res) => {
        res.send('This is home page...')
    })
    
    app.post('/user', (req, res) => {
        res.send('Post user...')
    })
    
    app.get('/user/:id', (req, res) => {
        res.send(`Get user ${req.params['id']}...`)
    })
    
    app.listen(80, () => {
        console.log('Server running at http://127.0.0.1/');
    })
    

    使用app.get()app.post()定义处理所有请求路径的中间件,需要使用通配符*来定义请求URL。即/*匹配所有进入服务器的请求路径;app.get('/*', ...)匹配所有进入服务器的GET请求;app.post('/*', ...)匹配所有进入服务器的POST请求。

注:一般使用中间件,都是对某一部分请求进行处理。如果指定了请求方式,使用app.METHOD()注册中间件时,一般都要在请求URL中使用通配符匹配某一部分请求;如果使用app.all()注册中间件时,也同样需要使用通配符。app.use()则不需要,因为app.use()会匹配当前目录的任何子目录。

Express中的通配符*能匹配所有字符串,包括/*通配符通常有以下几种用法:

  • /*:匹配所有的请求路径。

    例如:

    • http://127.0.0.1/
    • http://127.0.0.1/dir
    • http://127.0.0.1/dir/index.html
  • /dir/*:匹配所有以/dir开头,但不包括/dir的请求路径。

    例如:

    • http://127.0.0.1/dir/a
    • http://127.0.0.1/dir/b
    • http://127.0.0.1/dir/index.html

    但不匹配http://127.0.0.1/dir本身。

  • /dir*:匹配所有以/dir开头,且包括/dir的请求路径。

    例如:

    • http://127.0.0.1/dir
    • http://127.0.0.1/dir/a
    • http://127.0.0.1/dir/index.html
    • http://127.0.0.1/dir123

进行统一处理

通过中间件,可以为到达服务器的请求进行一些统一的处理。在同一条处理链上的中间件共享同一份requestresponse对象。通过在上游的中间件中,统一为requestresponse对象添加自定义属性或方法(在上游做统一的处理),供下游的中间件或路由进行使用。

const express = require('express');
const app = express()

// 注册全局生效的中间件
app.use((req, res, next) => {
    // 获取请求到达服务器的时间
    const time = new Date(Date.now())
    // 为 request 对象挂载自定义属性,从而把时间共享给后面的所有路由
    req.startTime = time
    // 把流转关系转交给下一个中间件或路由
    next()
})

app.get('/', (req, res) => {
    res.send(`[${req.startTime}] This is home page...`)
})

app.post('/user', (req, res) => {
    res.send(`[${req.startTime}] Post user...`)
})

app.listen(80, () => {
    console.log('Server running at http://127.0.0.1/');
})

中间件调用链

可以连续多次定义同一种请求方式、请求路径的中间件。请求到达服务器后,会按照中间件被定义的先后顺序,依次调用这些中间件。

const express = require('express');
const app = express()

// 连续注册全局中间件
app.use((req, res, next) => {
    console.log('Middleware 1 running...');
    next()
})

app.use((req, res, next) => {
    console.log('Middleware 2 running...');
    next()
})

app.use((req, res, next) => {
    console.log('Middleware 3 running...');
    next()
})

app.get('/', (req, res) => {
    res.send('This is home page...')
})

app.listen(80, () => {
    console.log('Server running at http://127.0.0.1/');
})

启动服务器,通过 GET http://127.0.0.1/请求后,终端打印内容如下:

Middleware 1 running...
Middleware 2 running...
Middleware 3 running...

如果注册的多个中间件,它们的请求方式或请求路径各有不同,Express会按照它们各自的条件进行匹配。如果一个同时被多个中间件匹配到,Express也是按照它们被定义的顺序去执行这些中间件。例如:

const express = require('express');
const app = express()

// 连续注册中间件
app.use('/user/*', (req, res, next) => {
    const msg = 'Through /user/* middleware...\n'
    req.msg = req.msg ? req.msg + msg : msg
    next()
})

app.use((req, res, next) => {
    const msg = 'Through /* middleware...\n'
    req.msg = req.msg ? req.msg + msg : msg
    next()
})

app.use('/user*', (req, res, next) => {
    const msg = 'Through /user* middleware...\n'
    req.msg = req.msg ? req.msg + msg : msg
    next()
})

app.get('/user/:id', (req, res) => {
    res.send(req.msg + `Get user by id ${req.params['id']}...`)
})

app.listen(80, () => {
    console.log('Server running at http://127.0.0.1/');
})

启动服务器,发送 GET http://127.0.0.1/user/1,客户端接收到的数据如下:

Through /user/* middleware...
Through /* middleware...
Through /user* middleware...
Get user by id 1...

app.use()app.METHOD()app.all()都支持同时注册多个回调函数,所以可以使用下列方式来连续定义多个中间件(以app.use()为例):

  • app.use(mw1, mw2, ...)

    const mw1 = (req, res, next) => {
        console.log('Middleware 1 running...');
        next()
    }
    
    const mw2 = (req, res, next) => {
        console.log('Middleware 2 running...');
        next()
    }
    
    app.use(mw1, mw2, (req, res, next) => {
        console.log('Middleware 3 running...');
        next()
    })
    
  • app.use([mw1, mw2, ...], ...)

    const mw1 = (req, res, next) => {
        console.log('Middleware 1 running...');
        next()
    }
    
    const mw2 = (req, res, next) => {
        console.log('Middleware 2 running...');
        next()
    }
    
    app.use([mw1, mw2], (req, res, next) => {
        console.log('Middleware 3 running...');
        next()
    })
    

注意事项

使用中间件时,有以下注意事项:

  • 一定要在路由之前注册中间件。
  • 客户端发送过来的请求,可以连续调用多个中间件进行处理。
  • 连续调用多个中间件时,多个中间件之间,共享requestresponse对象。
  • 执行完中间件的业务代码之后,不要忘记调用next()函数。
  • 为了防止代码透辑混乱,调用next()函数后不要再写额外的代码。
  • 调用res.send()会终止业务逻辑处理,后续的中间件都不会被执行。

中间件分类

Express官方将常见的中间件用法分成了5大类:

  • 应用级别的中间件:通过app.use()app.METHOD()等,绑定到app实例上的中间件。
  • 路由级别的中间件:绑定到express.Router()实例上的中间件。路由级别中间件的用法与应用级别中间件的用法没有任何区别,仅仅是绑定的对象不同。
  • 错误级别的中间件:专门用来捕获项目中发生的异常错误,从而防止项目异常崩溃的问题。错误级别中间件的处理函数中,必须包含4个形参,形参顺序从前到后分别是 (err, req, res, next)
  • Express内置的中间件
  • 第三方的中间件

路由级别中间件

通过在express.Router()实例上绑定全局中间件,可以为该路由下的所有请求进行统一处理。例如:

  • user.js

    /**
     * User 路由模块
    */
    
    const express = require('express');
    const router = express.Router()
    
    // 统一处理 /user 请求,所有到达该路由的请求都会先经过此中间件
    router.use((req, res, next) => {
        req.msg = "Through user's Middleware...\n"
        next()
    })
    
    /**
     * 访问 GET /user,会经过该模块的全局中间件
     * 客户端获得的响应数据如下:
     * Through user's Middleware...
     * This is user page...
     */
    router.get('/', (req, res) => {
        res.send(req.msg + 'This is user page...')
    })
    
    /**
     * 访问 GET /user/1,会经过该模块的全局中间件
     * 客户端获得的响应数据如下:
     * Through user's Middleware...
     * Get user by id 1 ...
     */
    router.get('/:id', (req, res) => {
        res.send(req.msg + `Get user by id ${req.params['id']} ...`)
    })
    
    /**
     * 访问 POST /user/add,会经过该模块的全局中间件
     * 客户端获得的响应数据如下:
     * Through user's Middleware...
     * Add new user...
     */
    router.post('/add', (req, res) => {
        res.send(req.msg + 'Add new user...')
    })
    
    // 向外导出路由对象
    module.exports = router
    
  • app.js

    const express = require('express');
    const app = express()
    
    const router = require('./user');
    app.use('/user', router)
    
    // 请求 GET / 不会通过 user.js 中定义的中间件
    app.get('/', (req, res) => {
        res.send('This is home page...')
    })
    
    app.listen(80, () => {
        console.log('Server running at http://127.0.0.1/');
    })
    

错误级别中间件

错误级别中间件是专门用来捕获项目中发生的异常错误,从而防止项目异常崩溃的问题。错误级别中间件的处理函数中,形参顺序从前到后分别是 (err, req, res, next)。例如:

const express = require('express');
const app = express()

app.get('/', (req, res) => {
    throw new Error('Server running error!')    // 抛出自定义异常
    res.send('This is home page')
})

// 定义并注册错误级别中间件
app.use((err, req, res, next) => {
    console.log('[error]: ' + err.message);
    res.send('Error! ' + err.message)
})

app.listen(80, () => {
    console.log('Server running at http://127.0.0.1/');
})

启动服务器,发送 GET http://127.0.0.1/ 请求,客户端接收到的数据如下:

Error! Server running error!

终端打印:

[error]: Server running error!

注:错误级别的中间件必须注册在所有路由之后!

注:在正常情况下,服务器发生异常时,发送给客户端的是一个异常页面。例如:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: Server running error!<br> &nbsp; &nbsp;at /root/study-js/express/middleware-error.js:5:11<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/root/study-js/express/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at next (/root/study-js/express/node_modules/express/lib/router/route.js:137:13)<br> &nbsp; &nbsp;at Route.dispatch (/root/study-js/express/node_modules/express/lib/router/route.js:112:3)<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/root/study-js/express/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at /root/study-js/express/node_modules/express/lib/router/index.js:281:22<br> &nbsp; &nbsp;at Function.process_params (/root/study-js/express/node_modules/express/lib/router/index.js:341:12)<br> &nbsp; &nbsp;at next (/root/study-js/express/node_modules/express/lib/router/index.js:275:10)<br> &nbsp; &nbsp;at expressInit (/root/study-js/express/node_modules/express/lib/middleware/init.js:40:5)<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/root/study-js/express/node_modules/express/lib/router/layer.js:95:5)</pre>
</body>
</html>

在使用了错误级别的中间件后,服务器发生异常时,可以自定义客户端显示的内容。

Express 内置中间件

自Express4.16.0版本开始,Express内置了3个常用的中间件,极大的提高了Express项目的开发效率和体验:

  • express.static():快速托管静态资源的内置中间件,例如:HTML、图片、CSS等(无兼客性要求,旧版亦可使用)。

  • express.json():解析JSON格式的请求体数据(有兼容性要求,仅在4.16.0及其后续版本中可用)。

    使用方式如下:

    // 配置解析 application/json 格式数据的内置中间件
    app.use(express.json())
    

    配置了express.json()中间件之后,就可以在其它中间件中,使用request.body获取从客户端发送过来的JSON数据。示例如下:

    const express = require('express');
    const app = express()
    
    // 配置 express.json(),解析JSON格式请求体数据
    app.use(express.json())
    
    /**
     * 使用 request.body 接收客户端发送的请求体数据
     * 默认情况下,若不配置解析表单数据的中间件,则 request.body 默认为 undefined
     */
    app.post('/user', (req, res) => {
        console.log(req.body);
        res.send('OK')
    })
    
    app.listen(80, () => {
        console.log('Server running at http://127.0.0.1/');
    })
    
  • express.urlencoded():解析URL-encoded格式的请求体数据(有兼容性要求)。

    使用方式如下:

    // 配置解析 application/x-www-form-urlencoded 格式数据的内置中间件
    app.use(express.urlencoded({ extended: false }))
    

    express.urlencoded()配置完成后,同样也是使用request.body获取数据。示例如下:

    const express = require('express');
    const app = express()
    
    // 配置 express.urlencoded(),解析 form-data 中的 urlencoded 格式数据
    app.use(express.urlencoded({ extended: false }))
    
    // urlencoded 格式数据也是使用 request.body 接收,默认也为 undefined
    app.post('/book', (req, res) => {
        console.log(req.body);
        res.send('OK')
    })
    
    app.listen(80, () => {
        console.log('Server running at http://127.0.0.1/');
    })
    

注:express.json()express.urlencoded()可以同时配置,两者并没有冲突。

第三方中间件

第三方中间件是指非Express官方内置的,而是由其它第三方团队(用户)所开发出来的中间件。在开发过程中,根据项目需求使用一些第三方中间件,可以提高项目开发的效率。

例如body-parser这个第三方中间件,在Express 4.16.0之前的版本中,经常被人们用来解析请求体数据。body-parser使用步骤如下:

  1. 安装body-parser

    npm i body-parser
    
  2. 使用require()导入中间件。

  3. 调用app.use()注册并使用中间件。

示例如下:

const express = require('express');
const app = express()

// 导入并注册 body-parse
const parser = require('body-parser');
app.use(parser.json())
app.use(parser.urlencoded({ extended: false }))

// body-parser 同样是使用 request.body 来获取请求体数据
app.post('/user', (req, res) => {
    console.log(req.body);
    res.send('OK')
})

app.listen(80, () => {
    console.log('Server running at http://127.0.0.1/');
})

自定义中间件

以手动模拟一个类似于express.urlencoded()的解析POST表单数据的中间件为例,实现步骤如下:

  1. 定义中间件:

    const qs = require('querystring');
    
    function urlencoded(req, res, next) {
        /* 请求数据处理过程... */
    }
    
  2. 监听requestdata事件。

    要解析POST表单数据,首先需要获取表单数据,所以需要监听requestdata事件来获取客户端发送到服务器的数据。

    如果数据量比较大,无法一次性发送完毕,客户端会把数据切割后,分批发送到服务器。所以data事件可能会触发多次,每一次触发datā事件时,获取到的数据可能只是完整数据的一部分,需要手动对接收到的数据进行拼接。

    let dataStr = ''
    /**
     * 监听 request 的 data 事件
     * chunk 用于获取从客户端接收到的数据
     * chunk 获取到的数据可能不完整,需要手动拼接
     */
    req.on('data', (chunk) => {
        dataStr += chunk
    })
    
  3. 监听requestend事件。

    当请求体数据接收完毕之后,会自动触发requestend事件。因此可以在requestend事件中,拿到处理完成的请求体数据。

  4. 使用querystring模块解析请求体数据。

    Node.js内置了一个querystring模块,专门用来处理查询字符。通过这个横块提供的parse()函数,可以轻松把查询字符串解析成对象格式。

  5. 将解析出来的数据对象挂载为request.body

    上游中间件与下游中间件之间都是共享同一份requestresponse。因此可以将解析出来的数据挂载为request的自定义属性(例如request.body)供下游使用。

    步骤3-4的实现:

    /**
     * 监听 request 的 end 事件
     */
    req.on('end', () => {
        // TODO: 把字符串格式的请求体数据,解析成对象格式
        const body = qs.parse(dataStr)
        req.body = body
        next()  // 解析完请求体之后,调用 next() 将请求流转到下游中间件
    })
    
  6. 将自定义中间件封装为模块。

    模块(custom-body-parser)的完整实现如下:

    const qs = require('querystring');
    
    function urlencoded(req, res, next) {
        let dataStr = ''
    
        /**
         * 监听 request 的 data 事件
         * chunk 用于获取从客户端接收到的数据
         * chunk 获取到的数据可能不完整,需要手动拼接
         */
        req.on('data', (chunk) => {
            dataStr += chunk
        })
    
        /**
         * 监听 request 的 end 事件
         */
        req.on('end', () => {
            // console.log(dataStr);
            // TODO: 把字符串格式的请求体数据,解析成对象格式
            const body = qs.parse(dataStr)
            // console.log(body);
            req.body = body
            next()
        })
    }
    
    module.exports = { urlencoded }
    

    对模块进行测试:

    const express = require('express');
    const app = express()
    
    const parser = require('./custom-body-parser');
    /**
     * 注册自定义的解析表单数据中间件
     */
    app.use(parser.urlencoded)
    
    app.post('/user', (req, res) => {
        res.send(req.body)
    })
    
    app.listen(80, () => {
        console.log('Server running at http://127.0.0.1/');
    })