<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          理解Koa中間件和洋蔥模型

          共 6375字,需瀏覽 13分鐘

           ·

          2020-11-05 15:03

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

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

          一、Koa 中間件

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

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

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

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

          function?wait(ms)?{
          ??return?new?Promise((resolve)?=>?setTimeout(resolve,?ms?||?1));
          }

          const?arr?=?[];
          const?stack?=?[];

          //?type?Middleware?=?(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)({});

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

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

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

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

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

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

          6.當(dāng)?shù)?1 個中間件執(zhí)行完成后,此時 arr 數(shù)組的值為 [1, 2, 3, 4, 5, 6]。

          為了更直觀地理解上述的執(zhí)行流程,我們可以把每個中間件當(dāng)做 1 個大任務(wù),然后在以 next 函數(shù)為分界點(diǎn),在把每個大任務(wù)拆解為 3 個 beforeNextnextafterNext 3 個小任務(wù)。

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

          1.1 任務(wù)注冊

          在 Koa 中,我們創(chuàng)建 Koa 應(yīng)用程序?qū)ο笾?,就可以通過調(diào)用該對象的 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`);
          });

          其實(shí) use 方法的實(shí)現(xiàn)很簡單,在 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 方法內(nèi)部會對 fn 參數(shù)進(jìn)行類型校驗(yàn),當(dāng)校驗(yàn)通過時,會把 fn 指向的中間件保存到 middleware 數(shù)組中,同時還會返回 this 對象,從而支持鏈?zhǔn)秸{(diào)用。

          1.2 任務(wù)編排

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

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

          //?統(tǒng)計請求處理時長的中間件
          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 任務(wù)調(diào)度

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

          /**
          ?*?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 函數(shù)接收一個參數(shù),該參數(shù)的類型是數(shù)組,調(diào)用該函數(shù)之后會返回一個新的函數(shù)。接下來我們將以前面的例子為例,來分析一下 await compose(stack)({}); 語句的執(zhí)行過程。

          1.3.1 dispatch(0)

          由上圖可知,當(dāng)在第一個中間件內(nèi)部調(diào)用 next 函數(shù),其實(shí)就是繼續(xù)調(diào)用 dispatch 函數(shù),此時參數(shù) i 的值為 1。

          1.3.2 dispatch(1)

          由上圖可知,當(dāng)在第二個中間件內(nèi)部調(diào)用 next 函數(shù),仍然是調(diào)用 dispatch 函數(shù),此時參數(shù) i 的值為 2。

          1.3.3 dispatch(2)

          由上圖可知,當(dāng)在第三個中間件內(nèi)部調(diào)用 next 函數(shù),仍然是調(diào)用 dispatch 函數(shù),此時參數(shù) i 的值為 3。

          1.3.4 dispatch(3)

          由上圖可知,當(dāng) middleware 數(shù)組中的中間件都開始執(zhí)行之后,如果調(diào)度時未顯式地設(shè)置 next 參數(shù)的值,則會開始返回 next 函數(shù)之后的語句繼續(xù)往下執(zhí)行。當(dāng)?shù)谌齻€中間件執(zhí)行完成后,就會返回第二中間件 next 函數(shù)之后的語句繼續(xù)往下執(zhí)行,直到所有中間件中定義的語句都執(zhí)行完成。

          分析完 compose 函數(shù)的實(shí)現(xiàn)代碼,我們來看一下 Koa 內(nèi)部如何利用 compose 函數(shù)來處理已注冊的中間件。

          const?Koa?=?require('koa');
          const?app?=?new?Koa();

          //?響應(yīng)
          app.use(ctx?=>?{
          ??ctx.body?=?'大家好,我是阿寶哥';
          });

          app.listen(3000);

          利用以上的代碼,我就可以快速啟動一個服務(wù)器。其中 use 方法我們前面已經(jīng)分析過了,所以接下來我們來分析 listen 方法,該方法的實(shí)現(xiàn)如下所示:

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

          很明顯在 listen 方法內(nèi)部,會先通過調(diào)用 Node.js 內(nèi)置 HTTP 模塊的 createServer 方法來創(chuàng)建服務(wù)器,然后開始監(jiān)聽指定的端口,即開始等待客戶端的連接。

          另外,在調(diào)用 http.createServer 方法創(chuàng)建 HTTP 服務(wù)器時,我們傳入的參數(shù)是 this.callback(),該方法的具體實(shí)現(xiàn)如下所示:

          //?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 方法內(nèi)部,我們終于見到了久違的 compose 方法。當(dāng)調(diào)用 callback 方法之后,會返回 handleRequest 函數(shù)對象用來處理 HTTP 請求。每當(dāng) Koa 服務(wù)器接收到一個客戶端請求時,都會調(diào)用 handleRequest 方法,在該方法會先創(chuàng)建新的 Context 對象,然后在執(zhí)行已注冊的中間件來處理已接收的 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 中間件的內(nèi)容已經(jīng)基本介紹完了,對 Koa 內(nèi)核感興趣的小伙伴,可以自行研究一下。接下來我們來介紹洋蔥模型及其應(yīng)用。

          二、洋蔥模型

          2.1 洋蔥模型簡介

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

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

          2.2 洋蔥模型應(yīng)用

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

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

          上圖中所述的中間件,一般是與業(yè)務(wù)無關(guān)的通用功能代碼,比如用于設(shè)置響應(yīng)時間的中間件:

          //?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");
          }

          其實(shí),對于每個中間件來說,前置處理器和后置處理器都是可選的。比如以下中間件用于設(shè)置統(tǒng)一的響應(yīng)內(nèi)容:

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

          盡管以上介紹的兩個中間件都比較簡單,但你也可以根據(jù)自己的需求來實(shí)現(xiàn)復(fù)雜的邏輯。Koa 的內(nèi)核很輕量,麻雀雖小五臟俱全。它通過提供了優(yōu)雅的中間件機(jī)制,讓開發(fā)者可以靈活地擴(kuò)展 Web 服務(wù)器的功能,這種設(shè)計思想值得我們學(xué)習(xí)與借鑒。

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

          三、參考資源

          • Koa 官方文檔
          • Egg 官方文檔
          聚焦全棧,專注分享 TypeScript、Web API、前端架構(gòu)等技術(shù)干貨。

          瀏覽 67
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  美穴在线观看 | 性爱网址色婷婷丁香五月 | 欧美久久在线观看 | 青娱乐福利| 久久免费精品视频 |