日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

如何更好地理解中间件和洋葱模型

發布時間:2024/1/8 编程问答 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 如何更好地理解中间件和洋葱模型 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

相信用過 Koa、Redux 或 Express 的小伙伴對中間件都不會陌生,特別是在學習 Koa 的過程中,還會接觸到 “洋蔥模型”

本文阿寶哥將跟大家一起來學習 Koa 的中間件,不過這里阿寶哥不打算一開始就亮出廣為人知的 ?“洋蔥模型圖”,而是先來介紹一下 Koa 中的中間件是什么?

一、Koa 中間件

在 @types/koa-compose 包下的 index.d.ts 頭文件中我們找到了中間件類型的定義:

//?@types/koa-compose/index.d.ts declare?namespace?compose?{type?Middleware<T>?=?(context:?T,?next:?Koa.Next)?=>?any;type?ComposedMiddleware<T>?=?(context:?T,?next?:?Koa.Next)?=>?Promise<void>; }//?@types/koa/index.d.ts?=>?Koa.Next type?Next?=?()?=>?Promise<any>;

通過觀察 Middleware 類型的定義,我們可以知道在 Koa 中,中間件就是普通的函數,該函數接收兩個參數:context 和 next。其中 context 表示上下文對象,而 next 表示一個調用后返回 Promise 對象的函數對象。

了解完 Koa 的中間件是什么之后,我們來介紹 Koa 中間件的核心,即 compose 函數:

function?wait(ms)?{return?new?Promise((resolve)?=>?setTimeout(resolve,?ms?||?1)); }const?arr?=?[]; const?stack?=?[];//?type?Middleware<T>?=?(context:?T,?next:?Koa.Next)?=>?any; stack.push(async?(context,?next)?=>?{arr.push(1);await?wait(1);await?next();await?wait(1);arr.push(6); });stack.push(async?(context,?next)?=>?{arr.push(2);await?wait(1);await?next();await?wait(1);arr.push(5); });stack.push(async?(context,?next)?=>?{arr.push(3);await?wait(1);await?next();await?wait(1);arr.push(4); });await?compose(stack)({});

對于以上的代碼,我們希望執行完 compose(stack)({}) 語句之后,數組 arr 的值為 [1, 2, 3, 4, 5, 6]。這里我們先不關心 compose 函數是如何實現的。我們來分析一下,如果要求數組 arr 輸出期望的結果,上述 3 個中間件的執行流程:

1.開始執行第 ?1 個中間件,往 arr 數組壓入 1,此時 arr 數組的值為 [1],接下去等待 1 毫秒。為了保證 arr 數組的第 1 項為 2,我們需要在調用 next 函數之后,開始執行第 2 個中間件。

2.開始執行第 2 個中間件,往 arr 數組壓入 2,此時 arr 數組的值為 [1, 2],繼續等待 1 毫秒。為了保證 arr 數組的第 2 項為 3,我們也需要在調用 next 函數之后,開始執行第 3 個中間件。

3.開始執行第 3 個中間件,往 arr 數組壓入 3,此時 arr 數組的值為 [1, 2, 3],繼續等待 1 毫秒。為了保證 arr 數組的第 3 項為 4,我們要求在調用第 3 個中間的 next 函數之后,要能夠繼續往下執行。

4.當第 3 個中間件執行完成后,此時 arr 數組的值為 [1, 2, 3, 4]。因此為了保證 arr 數組的第 4 項為 5,我們就需要在第 3 個中間件執行完成后,返回第 2 個中間件 next 函數之后語句開始執行。

5.當第 2 個中間件執行完成后,此時 arr 數組的值為 [1, 2, 3, 4, 5]。同樣,為了保證 arr 數組的第 5 項為 6,我們就需要在第 2 個中間件執行完成后,返回第 1 個中間件 next 函數之后語句開始執行。

6.當第 1 個中間件執行完成后,此時 arr 數組的值為 [1, 2, 3, 4, 5, 6]。

為了更直觀地理解上述的執行流程,我們可以把每個中間件當做 1 個大任務,然后在以 next 函數為分界點,在把每個大任務拆解為 3 個 beforeNext、next 和 afterNext 3 個小任務。

在上圖中,我們從中間件一的 beforeNext 任務開始執行,然后按照紫色箭頭的執行步驟完成中間件的任務調度。在 77.9K 的 Axios 項目有哪些值得借鑒的地方 這篇文章中,阿寶哥從 任務注冊、任務編排和任務調度 3 個方面去分析 Axios 攔截器的實現。同樣,阿寶哥將從上述 3 個方面來分析 Koa 中間件機制。

1.1 任務注冊

在 Koa 中,我們創建 Koa 應用程序對象之后,就可以通過調用該對象的 use 方法來注冊中間件:

const?Koa?=?require('koa'); const?app?=?new?Koa();app.use(async?(ctx,?next)?=>?{const?start?=?Date.now();await?next();const?ms?=?Date.now()?-?start;console.log(`${ctx.method}?${ctx.url}?-?${ms}ms`); });

其實 use 方法的實現很簡單,在 lib/application.js 文件中,我們找到了它的定義:

//?lib/application.js module.exports?=?class?Application?extends?Emitter?{??constructor(options)?{super();//?省略部分代碼?this.middleware?=?[];}use(fn)?{if?(typeof?fn?!==?'function')?throw?new?TypeError('middleware?must?be?a?function!');//?省略部分代碼?this.middleware.push(fn);return?this;} }

由以上代碼可知,在 use 方法內部會對 fn 參數進行類型校驗,當校驗通過時,會把 fn 指向的中間件保存到 middleware 數組中,同時還會返回 this 對象,從而支持鏈式調用。

1.2 任務編排

在 77.9K 的 Axios 項目有哪些值得借鑒的地方 這篇文章中,阿寶哥參考 Axios 攔截器的設計模型,抽出以下通用的任務處理模型:

在該通用模型中,阿寶哥是通過把前置處理器和后置處理器分別放到 CoreWork 核心任務的前后來完成任務編排。而對于 Koa 的中間件機制來說,它是通過把前置處理器和后置處理器分別放到 await next()?語句的前后來完成任務編排。

//?統計請求處理時長的中間件 app.use(async?(ctx,?next)?=>?{const?start?=?Date.now();await?next();const?ms?=?Date.now()?-?start;console.log(`${ctx.method}?${ctx.url}?-?${ms}ms`); });

1.3 任務調度

通過前面的分析,我們已經知道了,使用 app.use 方法注冊的中間件會被保存到內部的 middleware 數組中。要完成任務調度,我們就需要不斷地從 middleware 數組中取出中間件來執行。中間件的調度算法被封裝到 koa-compose 包下的 compose 函數中,該函數的具體實現如下:

/***?Compose?`middleware`?returning*?a?fully?valid?middleware?comprised*?of?all?those?which?are?passed.**?@param?{Array}?middleware*?@return?{Function}*?@api?public*/ function?compose(middleware)?{//?省略部分代碼return?function?(context,?next)?{//?last?called?middleware?#let?index?=?-1;return?dispatch(0);function?dispatch(i)?{if?(i?<=?index)return?Promise.reject(new?Error("next()?called?multiple?times"));index?=?i;let?fn?=?middleware[i];if?(i?===?middleware.length)?fn?=?next;if?(!fn)?return?Promise.resolve();try?{return?Promise.resolve(fn(context,?dispatch.bind(null,?i?+?1)));}?catch?(err)?{return?Promise.reject(err);}}}; }

compose 函數接收一個參數,該參數的類型是數組,調用該函數之后會返回一個新的函數。接下來我們將以前面的例子為例,來分析一下 await compose(stack)({}); 語句的執行過程。

1.3.1 dispatch(0)

由上圖可知,當在第一個中間件內部調用 next 函數,其實就是繼續調用 dispatch 函數,此時參數 i 的值為 1。

1.3.2 dispatch(1)

由上圖可知,當在第二個中間件內部調用 next 函數,仍然是調用 dispatch 函數,此時參數 i 的值為 2。

1.3.3 dispatch(2)

由上圖可知,當在第三個中間件內部調用 next 函數,仍然是調用 dispatch 函數,此時參數 i 的值為 3。

1.3.4 dispatch(3)

由上圖可知,當 middleware 數組中的中間件都開始執行之后,如果調度時未顯式地設置 next 參數的值,則會開始返回 next 函數之后的語句繼續往下執行。當第三個中間件執行完成后,就會返回第二中間件 next 函數之后的語句繼續往下執行,直到所有中間件中定義的語句都執行完成。

分析完 compose 函數的實現代碼,我們來看一下 Koa 內部如何利用 compose 函數來處理已注冊的中間件。

const?Koa?=?require('koa'); const?app?=?new?Koa();//?響應 app.use(ctx?=>?{ctx.body?=?'大家好,我是阿寶哥'; });app.listen(3000);

利用以上的代碼,我就可以快速啟動一個服務器。其中 use 方法我們前面已經分析過了,所以接下來我們來分析 listen 方法,該方法的實現如下所示:

//?lib/application.js module.exports?=?class?Application?extends?Emitter?{??listen(...args)?{debug('listen');const?server?=?http.createServer(this.callback());return?server.listen(...args);} }

很明顯在 listen 方法內部,會先通過調用 Node.js 內置 HTTP 模塊的 createServer 方法來創建服務器,然后開始監聽指定的端口,即開始等待客戶端的連接。

另外,在調用 http.createServer 方法創建 HTTP 服務器時,我們傳入的參數是 this.callback(),該方法的具體實現如下所示:

//?lib/application.js const?compose?=?require('koa-compose');module.exports?=?class?Application?extends?Emitter?{??callback()?{const?fn?=?compose(this.middleware);if?(!this.listenerCount('error'))?this.on('error',?this.onerror);const?handleRequest?=?(req,?res)?=>?{const?ctx?=?this.createContext(req,?res);return?this.handleRequest(ctx,?fn);};return?handleRequest;} }

在 callback 方法內部,我們終于見到了久違的 compose 方法。當調用 callback 方法之后,會返回 handleRequest 函數對象用來處理 HTTP 請求。每當 Koa 服務器接收到一個客戶端請求時,都會調用 handleRequest 方法,在該方法會先創建新的 Context 對象,然后在執行已注冊的中間件來處理已接收的 HTTP 請求:

module.exports?=?class?Application?extends?Emitter?{??handleRequest(ctx,?fnMiddleware)?{const?res?=?ctx.res;res.statusCode?=?404;const?onerror?=?err?=>?ctx.onerror(err);const?handleResponse?=?()?=>?respond(ctx);onFinished(res,?onerror);return?fnMiddleware(ctx).then(handleResponse).catch(onerror);} }

好的,Koa 中間件的內容已經基本介紹完了,對 Koa 內核感興趣的小伙伴,可以自行研究一下。接下來我們來介紹洋蔥模型及其應用。

二、洋蔥模型

2.1 洋蔥模型簡介

(圖片來源:https://eggjs.org/en/intro/egg-and-koa.html)

在上圖中,洋蔥內的每一層都表示一個獨立的中間件,用于實現不同的功能,比如異常處理、緩存處理等。每次請求都會從左側開始一層層地經過每層的中間件,當進入到最里層的中間件之后,就會從最里層的中間件開始逐層返回。因此對于每層的中間件來說,在一個 請求和響應 周期中,都有兩個時機點來添加不同的處理邏輯。

2.2 洋蔥模型應用

除了在 Koa 中應用了洋蔥模型之外,該模型還被廣泛地應用在 Github 上一些不錯的項目中,比如 koa-router 和阿里巴巴的 midway、umi-request 等項目中。

介紹完 Koa 的中間件和洋蔥模型,阿寶哥根據自己的理解,抽出以下通用的任務處理模型:

上圖中所述的中間件,一般是與業務無關的通用功能代碼,比如用于設置響應時間的中間件:

//?x-response-time async?function?responseTime(ctx,?next)?{const?start?=?new?Date();await?next();const?ms?=?new?Date()?-?start;ctx.set("X-Response-Time",?ms?+?"ms"); }

其實,對于每個中間件來說,前置處理器和后置處理器都是可選的。比如以下中間件用于設置統一的響應內容:

//?response async?function?respond(ctx,?next)?{await?next();if?("/"?!=?ctx.url)?return;ctx.body?=?"Hello?World"; }

盡管以上介紹的兩個中間件都比較簡單,但你也可以根據自己的需求來實現復雜的邏輯。Koa 的內核很輕量,麻雀雖小五臟俱全。它通過提供了優雅的中間件機制,讓開發者可以靈活地擴展 Web 服務器的功能,這種設計思想值得我們學習與借鑒。

好的,這次就先介紹到這里,后面有機會的話,阿寶哥在單獨介紹一下 Redux 或 Express 的中間件機制。

三、參考資源

  • Koa 官方文檔

  • Egg 官方文檔

關于奇舞周刊

《奇舞周刊》是360公司專業前端團隊「奇舞團」運營的前端技術社區。關注公眾號后,直接發送鏈接到后臺即可給我們投稿。

總結

以上是生活随笔為你收集整理的如何更好地理解中间件和洋葱模型的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。