在前面的章節(jié)中,我們介紹了 Egg 是基于 Koa 實(shí)現(xiàn)的,所以 Egg 的中間件形式和 Koa 的中間件形式是一樣的,都是基于洋蔥圈模型。每次我們編寫一個(gè)中間件,就相當(dāng)于在洋蔥外面包了一層。
編寫中間件
寫法
我們先來通過編寫一個(gè)簡單的 gzip 中間件,來看看中間件的寫法。
// app/middleware/gzip.js const isJSON = require('koa-is-json'); const zlib = require('zlib');
async function gzip(ctx, next) { await next();
// 后續(xù)中間件執(zhí)行完成后將響應(yīng)體轉(zhuǎn)換成 gzip let body = ctx.body; if (!body) return; if (isJSON(body)) body = JSON.stringify(body);
// 設(shè)置 gzip body,修正響應(yīng)頭 const stream = zlib.createGzip(); stream.end(body); ctx.body = stream; ctx.set('Content-Encoding', 'gzip'); }
|
可以看到,框架的中間件和 Koa 的中間件寫法是一模一樣的,所以任何 Koa 的中間件都可以直接被框架使用。
配置
一般來說中間件也會(huì)有自己的配置。在框架中,一個(gè)完整的中間件是包含了配置處理的。我們約定一個(gè)中間件是一個(gè)放置在 app/middleware 目錄下的單獨(dú)文件,它需要 exports 一個(gè)普通的 function,接受兩個(gè)參數(shù):
- options: 中間件的配置項(xiàng),框架會(huì)將 app.config[${middlewareName}] 傳遞進(jìn)來。
- app: 當(dāng)前應(yīng)用 Application 的實(shí)例。
我們將上面的 gzip 中間件做一個(gè)簡單的優(yōu)化,讓它支持指定只有當(dāng) body 大于配置的 threshold 時(shí)才進(jìn)行 gzip 壓縮,我們要在 app/middleware 目錄下新建一個(gè)文件 gzip.js
// app/middleware/gzip.js const isJSON = require('koa-is-json'); const zlib = require('zlib');
module.exports = options => { return async function gzip(ctx, next) { await next();
// 后續(xù)中間件執(zhí)行完成后將響應(yīng)體轉(zhuǎn)換成 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);
// 設(shè)置 gzip body,修正響應(yīng)頭 const stream = zlib.createGzip(); stream.end(body); ctx.body = stream; ctx.set('Content-Encoding', 'gzip'); }; };
|
使用中間件
中間件編寫完成后,我們還需要手動(dòng)掛載,支持以下方式:
在應(yīng)用中使用中間件
在應(yīng)用中,我們可以完全通過配置來加載自定義的中間件,并決定它們的順序。
如果我們需要加載上面的 gzip 中間件,在 config.default.js 中加入下面的配置就完成了中間件的開啟和配置:
module.exports = { // 配置需要的中間件,數(shù)組順序即為中間件的加載順序 middleware: [ 'gzip' ],
// 配置 gzip 中間件的配置 gzip: { threshold: 1024, // 小于 1k 的響應(yīng)體不壓縮 }, };
|
該配置最終將在啟動(dòng)時(shí)合并到 app.config.appMiddleware。
在框架和插件中使用中間件
框架和插件不支持在 config.default.js 中匹配 middleware,需要通過以下方式:
// app.js module.exports = app => { // 在中間件最前面統(tǒng)計(jì)請求時(shí)間 app.config.coreMiddleware.unshift('report'); };
// app/middleware/report.js module.exports = () => { return async function (ctx, next) { const startTime = Date.now(); await next(); // 上報(bào)請求時(shí)間 reportTime(Date.now() - startTime); } };
|
應(yīng)用層定義的中間件(app.config.appMiddleware)和框架默認(rèn)中間件(app.config.coreMiddleware)都會(huì)被加載器加載,并掛載到 app.middleware 上。
router 中使用中間件
以上兩種方式配置的中間件是全局的,會(huì)處理每一次請求。 如果你只想針對單個(gè)路由生效,可以直接在 app/router.js 中實(shí)例化和掛載,如下:
module.exports = app => { const gzip = app.middleware.gzip({ threshold: 1024 }); app.router.get('/needgzip', gzip, app.controller.handler); };
|
框架默認(rèn)中間件
除了應(yīng)用層加載中間件之外,框架自身和其他的插件也會(huì)加載許多中間件。所有的這些自帶中間件的配置項(xiàng)都通過在配置中修改中間件同名配置項(xiàng)進(jìn)行修改,例如框架自帶的中間件中有一個(gè) bodyParser 中間件(框架的加載器會(huì)將文件名中的各種分隔符都修改成駝峰形式的變量名),我們想要修改 bodyParser 的配置,只需要在 config/config.default.js 中編寫
module.exports = { bodyParser: { jsonLimit: '10mb', }, };
|
注意:框架和插件加載的中間件會(huì)在應(yīng)用層配置的中間件之前,框架默認(rèn)中間件不能被應(yīng)用層中間件覆蓋,如果應(yīng)用層有自定義同名中間件,在啟動(dòng)時(shí)會(huì)報(bào)錯(cuò)。
使用 Koa 的中間件
在框架里面可以非常容易的引入 Koa 中間件生態(tài)。
以 koa-compress 為例,在 Koa 中使用時(shí):
const koa = require('koa'); const compress = require('koa-compress');
const app = koa();
const options = { threshold: 2048 }; app.use(compress(options));
|
我們按照框架的規(guī)范來在應(yīng)用中加載這個(gè) 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 中間件不符合入?yún)⒁?guī)范,則可以自行處理下:
// 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); }
|
通用配置
無論是應(yīng)用層加載的中間件還是框架自帶中間件,都支持幾個(gè)通用的配置項(xiàng):
- enable:控制中間件是否開啟。
- match:設(shè)置只有符合某些規(guī)則的請求才會(huì)經(jīng)過這個(gè)中間件。
- ignore:設(shè)置符合某些規(guī)則的請求不經(jīng)過這個(gè)中間件。
enable
如果我們的應(yīng)用并不需要默認(rèn)的 bodyParser 中間件來進(jìn)行請求體的解析,此時(shí)我們可以通過配置 enable 為 false 來關(guān)閉它
module.exports = { bodyParser: { enable: false, }, };
|
match 和 ignore
match 和 ignore 支持的參數(shù)都一樣,只是作用完全相反,match 和 ignore 不允許同時(shí)配置。
如果我們想讓 gzip 只針對 /static 前綴開頭的 url 請求開啟,我們可以配置 match 選項(xiàng)
module.exports = { gzip: { match: '/static', }, };
|
match 和 ignore 支持多種類型的配置方式
- 字符串:當(dāng)參數(shù)為字符串類型時(shí),配置的是一個(gè) url 的路徑前綴,所有以配置的字符串作為前綴的 url 都會(huì)匹配上。 當(dāng)然,你也可以直接使用字符串?dāng)?shù)組。
- 正則:當(dāng)參數(shù)為正則時(shí),直接匹配滿足正則驗(yàn)證的 url 的路徑。
- 函數(shù):當(dāng)參數(shù)為一個(gè)函數(shù)時(shí),會(huì)將請求上下文傳遞給這個(gè)函數(shù),最終取函數(shù)返回的結(jié)果(true/false)來判斷是否匹配。
module.exports = { gzip: { match(ctx) { // 只有 ios 設(shè)備才開啟 const reg = /iphone|ipad|ipod/i; return reg.test(ctx.get('user-agent')); }, }, };
|
有關(guān)更多的 match 和 ignore 配置情況,詳見 egg-path-matching.
更多建議: