Egg 中間件(Middleware)

2020-02-06 14:10 更新

前面的章節(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 支持多種類型的配置方式

  1. 字符串:當(dāng)參數(shù)為字符串類型時(shí),配置的是一個(gè) url 的路徑前綴,所有以配置的字符串作為前綴的 url 都會(huì)匹配上。 當(dāng)然,你也可以直接使用字符串?dāng)?shù)組。
  2. 正則:當(dāng)參數(shù)為正則時(shí),直接匹配滿足正則驗(yàn)證的 url 的路徑。
  3. 函數(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.


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號