中间件
中间件
在NodeJS中,中间件主要是指封装所有Http请求细节处理的方法。 一次Http请求通常包含很多工作,如记录日志、ip过滤、查询字符串、请求体解析、Cookie处理、权限验证、参数验证、异常处理等, 但对于Web应用而言,并不希望接触到这么多细节性的处理, 因此引入中间件来简化和隔离这些基础设施与业务逻辑之间的细节,让开发者能够关注在业务的开发上,以达到提升开发效率的目的。
Egg
是基于 Koa 实现的,所以 Egg
的中间件形式和 Koa 的中间件形式是一样的,都是基于洋葱圈模型, 每次编写一个中间件,就相当于在洋葱外面包了一层。
基础写法
编写一个简单的 gzip 中间件
// app/middleware/gzip.js
const zlib = require('node:zlib')
const isJSON = require('koa-is-json')
async function gzip(ctx, next) {
await next()
// 后续中间件执行完成后将响应体转换成 gzip
let body = ctx.body
if (!body)
return
if (isJSON(body))
body = JSON.stringify(body)
// 设置 gzip body,修正响应头
const stream = zlib.createGzip()
stream.end(body)
ctx.body = stream
ctx.set('Content-Encoding', 'gzip')
}
// 导出
module.export = gzip
框架的中间件和 Koa 的中间件写法是一模一样的,所以任何 Koa 的中间件都可以直接被框架使用。
配置说明
一般来说中间件也会有自己的配置。在框架中,一个完整的中间件是包含了配置处理的。我们约定一个中间件是一个放置在 app/middleware 目录下的单独文件,它需要 exports 一个普通的 function,接受两个参数:
- options: 中间件的配置项,框架会将
app.config[${middlewareName}]
传递进来。 - app: 当前应用 Application 的实例。
gzip
中间件做一个简单的优化,让它支持指定只有当 body
大于配置的 threshold
时才进行 gzip
压缩, 要在 app/middleware
目录下新建一个文件 gzip.js
// app/middleware/gzip.js
const zlib = require('node:zlib')
const isJSON = require('koa-is-json')
module.exports = (options) => {
return async function gzip(ctx, next) {
await next()
// 后续中间件执行完成后将响应体转换成 gzip
let body = ctx.body
if (!body)
return
// 支持 options.threshold
if (options.threshold && ctx.length < options.threshold)
return
if (isJSON(body))
body = JSON.stringify(body)
// 设置 gzip body,修正响应头
const stream = zlib.createGzip()
stream.end(body)
ctx.body = stream
ctx.set('Content-Encoding', 'gzip')
}
}
使用中间件
中间件编写完成后,需要手动挂载,使用情况分为:
- 在应用中使用中间件
- 在框架和插件中使用中间件
- 在
router
中使用中间件
在应用中使用
在应用中,可以完全通过配置来加载自定义的中间件,并决定它们的顺序
在 config.default.js
中加入下面的配置就完成了中间件的开启和配置:
module.exports = {
// 配置需要的中间件,数组顺序即为中间件的加载顺序
middleware: ['gzip'],
// 配置 gzip 中间件的配置
gzip: {
threshold: 1024, // 小于 1k 的响应体不压缩
},
}
配置最终将在启动时合并到 app.config.appMiddleware
在框架和插件中使用
框架和插件不支持在 config.default.js
中匹配 middleware
,需要通过以下方式:
// app.js
module.exports = (app) => {
// 在中间件最前面统计请求时间【unshift() 方法可向数组的开头添加一个或更多元素,并返回新的长度】
app.config.coreMiddleware.unshift('report')
}
// app/middleware/report.js
module.exports = () => {
return async function (ctx, next) {
const startTime = Date.now()
await next()
// 上报请求时间
reportTime(Date.now() - startTime)
}
}
应用层定义的中间件(app.config.appMiddleware
)和框架默认中间件(app.config.coreMiddleware
)都会被加载器加载,并挂载到 app.middleware
上
router中使用
在框架、应用、插件中使用中间件,作用范围都是全局的,会处理每一次请求。即便是有ignore、match的配置,但是在处理只针对单个路由生效的问题时,就需要直接在router上使用中间件, 可以直接在 app/router.js
中实例化和挂载。
module.exports = (app) => {
// 获取中间件
const gzip = app.middleware.gzip({ threshold: 1024 })
// 路由调用方法之前,使用中间件
app.router.get('/needgzip', gzip, app.controller.handler)
}
框架默认中间件
除了应用层加载中间件之外,框架自身和其他的插件也会加载许多中间件。 所有的这些自带中间件的配置项都通过在配置中修改中间件同名配置项进行修改。
例如:框架自带的中间件中有一个 bodyParser 中间件(框架的加载器会将文件名中的各种分隔符都修改成驼峰形式的变量名), 想要修改bodyParser
的配置,只需要在 config/config.default.js
中编写
module.exports = {
bodyParser: {
// 限制json大小
jsonLimit: '10mb',
},
}
框架和插件加载的中间件会在应用层配置的中间件之前,框架默认中间件不能被应用层中间件覆盖, 如果应用层有自定义同名中间件,在启动时会报错。
使用Koa框架的中间件
因为Egg
框架实在Koa的基础上进行封装的,所有Koa框架中的中间件, 都可以在Egg
框架中使用,可以非常方便、常容易的引入 Koa 中间件生态。
以 koa-compress
为例,在 Koa 中使用时:
const koa = require('koa')
const compress = require('koa-compress')
const app = koa()
const options = { threshold: 2048 }
app.use(compress(options))
按照框架的规范来在应用中加载这个 Koa
的中间件:
// app/middleware/compress.js
// koa-compress 暴露的接口(`(options) => middleware`)和框架对中间件要求一致
module.exports = require('koa-compress')
// config/config.default.js
module.exports = {
middleware: ['compress'],
compress: {
threshold: 2048,
},
}
如果使用到的 Koa
中间件不符合入参规范,则可以自行处理:
// config/config.default.js
module.exports = {
webpack: {
compiler: {},
others: {},
},
}
// app/middleware/webpack.js
const webpackMiddleware = require('some-koa-middleware')
module.exports = (options, app) => {
return webpackMiddleware(options.compiler, options.others)
}
中间件的通用配置
无论是应用层加载的中间件还是框架自带中间件,都支持几个通用的配置项:
enable
:控制中间件是否开启。match
:设置只有符合某些规则的请求才会经过这个中间件。ignore
:设置符合某些规则的请求不经过这个中间件。
enable
如果应用并不需要默认的 bodyParser
中间件来进行请求体的解析,此时可以通过配置 enable 为 false 来关闭它
module.exports = {
bodyParser: {
// 关闭
enable: false,
},
}
match 和 ignore
match
和 ignore
支持的参数都一样,只是作用完全相反,match
和 ignore
不允许同时配置。
如果想让 gzip
只针对 /static
前缀开头的 url
请求开启,我们可以配置 match
选项
module.exports = {
gzip: {
match: '/static',
},
}
match
和 ignore
支持多种类型的配置方式
- 字符串:当参数为字符串类型时,配置的是一个 url 的路径前缀, 所有以配置的字符串作为前缀的 url 都会匹配上。 当然,你也可以直接使用字符串数组。
- 正则:当参数为正则时,直接匹配满足正则验证的 url 的路径。
- 函数:当参数为一个函数时,会将请求上下文传递给这个函数,最终取函数返回的结果(
true/false
)来判断是否匹配。
//
module.exports = {
gzip: {
match(ctx) {
// 针对 ios 设备才开启
const reg = /iphone|ipad|ipod/i
return reg.test(ctx.get('user-agent'))
},
},
}
match
和 ignore
配置情况,详见 egg-path-matching.