淺析洋蔥模型
前言
使用過koa的小伙伴們都應(yīng)該對洋蔥模型有所了解,koa的獨(dú)特的中間件流程控制就是通過洋蔥模型來實(shí)現(xiàn)的,那么洋蔥模型是什么,又是如何實(shí)現(xiàn)的呢?
洋蔥模型介紹
介紹
上圖是洋蔥模型比較經(jīng)典的一個圖,通過這個圖可以看到,洋蔥模型就像一個洋蔥一個,是分成多層的,而一個請求進(jìn)入的時候,會從外到內(nèi)依次經(jīng)過每一層,到最內(nèi)側(cè)之后又從內(nèi)到外依次經(jīng)過每一次,而我們就可以在這每一層上面做自己需要做的操作。
比如一個請求,在koa接收到請求的時候,首先需要鑒權(quán),然后需要對請求參數(shù)解析等等,完成請求之后,需要處理異常,添加請求頭等等操作,而這些操作就可以放到洋蔥模型的每一層上面做處理。
示例代碼
如下代碼為koa的一段示例代碼:
- const Koa = require('koa');
- const app = new Koa();
- app.use(async (ctx, next) => {
- console.log(1);
- await next();
- console.log(2);
- });
- app.use(async (ctx, next) => {
- console.log(3);
- await next();
- console.log(4);
- });
- app.use(async (ctx, next) => {
- console.log(5);
- await next();
- console.log(6);
- });
- app.listen(8000);
執(zhí)行上面的代碼,會發(fā)現(xiàn)輸出的數(shù)字順序?yàn)?,3,5,6,4,2,與上面介紹的洋蔥模型的執(zhí)行順序是一致的。
洋蔥模型實(shí)現(xiàn)
首先我們先分析一下上面的代碼app.use傳入了一個異步的函數(shù),而且app.use可以被使用多次,而這些函數(shù)在請求進(jìn)入的時候會依次被調(diào)用,這是不是與發(fā)布訂閱者模式是一致的,那首先我們來實(shí)現(xiàn)一個app.use
實(shí)現(xiàn)一個app.use
- export interface Middleware {
- (...rest: any): Promise<any>;
- }
- export default class Onion {
- middlewares: Middleware[] = [];
- constructor(middlewares: Middleware[] = []) {
- this.middlewares = middlewares;
- }
- use(middleware: Middleware) {
- this.middlewares.push(middleware);
- }
- }
上面代碼我們定義了一個Onion類,通過這個類我們就可以將訂閱函數(shù)進(jìn)行收集,如下代碼所示
- const onion = new Onion()
- onion.use(async(params, next)=> {
- console.log(1)
- await next()
- console.log(2)
- })
發(fā)布者在收集完訂閱函數(shù)后需要有觸發(fā)的時機(jī),這時候就需要再給Onion添加一個執(zhí)行函數(shù)
完善Onion
有小伙伴想到發(fā)布訂閱者的實(shí)現(xiàn)代碼,可能就會想到這樣做:
- export default class Onion {
- middlewares: Middleware[] = [];
- constructor(middlewares: Middleware[] = []) {
- this.middlewares = middlewares;
- }
- use(middleware: Middleware) {
- this.middlewares.push(middleware);
- }
- execute(params: any) {
- this.middlewares.forEach(fn => {
- fn(params)
- })
- }
- }
但是這樣做的話,就無法滿足洋蔥模型的先入后出的順序,那我們應(yīng)該怎么做呢?
1.定義 compose函數(shù)
- function compose(middlewares: Array<Middleware>) {
- if (!Array.isArray(middlewares)) {
- throw new Error('中間件必須是數(shù)組');
- }
- for (let i = 0; i < middlewares.length; i++) {
- if (typeof middlewares[i] !== 'function') {
- throw new Error('中間件的每一項都必須是函數(shù)');
- }
- }
- return (params: any) => {
- let index = 0;
- function dispatch(fn: Middleware | undefined) {
- if (!fn) {
- return Promise.resolve();
- }
- const next = () => dispatch(middlewares[++index]);
- return Promise.resolve(fn(params, next));
- }
- return dispatch(middlewares[index]);
- };
- }
2.實(shí)現(xiàn)execute
- export default class Onion {
- middlewares: Middleware[] = [];
- constructor(middlewares: Middleware[] = []) {
- this.middlewares = middlewares;
- }
- use(middleware: Middleware) {
- this.middlewares.push(middleware);
- }
- execute(params: any) {
- const fn = compose(this.middlewares);
- return fn(params);
- }
- }
通過定義componse函數(shù),可以將中間件函數(shù)依次按照順序來執(zhí)行。
- const onion = new Onion();
- onion.use(async (params: any, next: Middleware) => {
- console.log(1);
- await next();
- console.log(2);
- });
- onion.use(async (params: any, next: Middleware) => {
- console.log(3);
- await next();
- console.log(4);
- });
- onion.use(async (params: any, next: Middleware) => {
- console.log(5);
- await next();
- console.log(6);
- });
- onion.execute({});
這樣我們就實(shí)現(xiàn)了一個簡易版的洋蔥模型。
本文轉(zhuǎn)載自微信公眾號「前端有的玩」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系前端有的玩公眾號。