50行代碼串行Promise,koa洋蔥模型原來這么有趣?

1. 前言

大家好,我是若川,最近組織了源碼共讀活動《1個月,200+人,一起讀了4周源碼》,感興趣的可以加我微信 ruochuan12 參與,長期交流學習。

之前寫的《學習源碼整體架構系列》 包含jQueryunderscorelodashvuexsentryaxiosreduxkoavue-devtoolsvuex4十余篇源碼文章。其中最新的兩篇是:

Vue 3.2 發布了,那尤雨溪是怎么發布 Vue.js 的?

初學者也能看懂的 Vue3 源碼中那些實用的基礎工具函數

寫相對很難的源碼,耗費了自己的時間和精力,也沒收獲多少閱讀點贊,其實是一件挺受打擊的事情。從閱讀量和讀者受益方面來看,不能促進作者持續輸出文章。

所以轉變思路,寫一些相對通俗易懂的文章。其實源碼也不是想象的那么難,至少有很多看得懂

之前寫過 koa 源碼文章學習 koa 源碼的整體架構,淺析koa洋蔥模型原理和co原理比較長,讀者朋友大概率看不完,所以本文從koa-compose50行源碼講述。

本文涉及到的 koa-compose 倉庫[1] 文件,整個index.js文件代碼行數雖然不到 50 行,而且測試用例test/test.js文件 300 余行,但非常值得我們學習。

歌德曾說:讀一本好書,就是在和高尚的人談話。同理可得:讀源碼,也算是和作者的一種學習交流的方式。

閱讀本文,你將學到:

1.?熟悉?koa-compose?中間件源碼、可以應對面試官相關問題
2.?學會使用測試用例調試源碼
3.?學會?jest?部分用法

2. 環境準備

2.1 克隆 koa-compose 項目

本文倉庫地址 koa-compose-analysis[2],求個star~

#?可以直接克隆我的倉庫,我的倉庫保留的?compose?倉庫的?git?記錄
git?clone?https://github.com/lxchuan12/koa-compose-analysis.git
cd?koa-compose/compose
npm?i

順帶說下:我是怎么保留 compose 倉庫的 git 記錄的。

#?在?github?上新建一個倉庫?`koa-compose-analysis`?克隆下來
git?clone?https://github.com/lxchuan12/koa-compose-analysis.git
cd?koa-compose-analysis
git?subtree?add?--prefix=compose?https://github.com/koajs/compose.git?main
#?這樣就把 compose 文件夾克隆到自己的 git 倉庫了。且保留的 git 記錄

關于更多 git subtree,可以看這篇文章用 Git Subtree 在多個 Git 項目間雙向同步子項目,附簡明使用手冊[3]

接著我們來看怎么根據開源項目中提供的測試用例調試源碼。

2.2 根據測試用例調試 compose 源碼

VSCode(我的版本是 1.60 )打開項目,找到 compose/package.json,找到 scriptstest 命令。

//?compose/package.json
{"name":?"koa-compose",//?debug?(調試)"scripts":?{"eslint":?"standard?--fix?.","test":?"jest"},
}

scripts上方應該會有debug或者調試字樣。點擊debug(調試),選擇 test

VSCode 調試

接著會執行測試用例test/test.js文件。終端輸出如下圖所示。

koa-compose 測試用例輸出結果

接著我們調試 compose/test/test.js 文件。我們可以在 45行 打上斷點,重新點擊 package.json => srcipts => test 進入調試模式。如下圖所示。

koa-compose 調試

接著按上方的按鈕,繼續調試。在compose/index.js文件中關鍵的地方打上斷點,調試學習源碼事半功倍。

更多 nodejs 調試相關 可以查看官方文檔[4]

順便提一下幾個調試相關按鈕。

  1. 繼續(F5)

  1. 單步跳過(F10)

  1. 單步調試(F11)

  1. 單步跳出(Shift + F11)

  1. 重啟(Ctrl + Shift + F5)

  2. 斷開鏈接(Shift + F5)

接下來,我們跟著測試用例學源碼。

3. 跟著測試用例學源碼

分享一個測試用例小技巧:我們可以在測試用例處加上only修飾。

//?例如
it.only('should?work',?async?()?=>?{})

這樣我們就可以只執行當前的測試用例,不關心其他的,不會干擾調試。

3.1 正常流程

打開 compose/test/test.js 文件,看第一個測試用例。

//?compose/test/test.js
'use?strict'/*?eslint-env?jest?*/const?compose?=?require('..')
const?assert?=?require('assert')function?wait?(ms)?{return?new?Promise((resolve)?=>?setTimeout(resolve,?ms?||?1))
}
//?分組
describe('Koa?Compose',?function?()?{it.only('should?work',?async?()?=>?{const?arr?=?[]const?stack?=?[]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)({})//?最后輸出數組是?[1,2,3,4,5,6]expect(arr).toEqual(expect.arrayContaining([1,?2,?3,?4,?5,?6]))})
}

大概看完這段測試用例,context是什么,next又是什么。

在`koa`的文檔[5]上有個非常代表性的中間件 gif 圖。

中間件 gif 圖

compose函數作用就是把添加進中間件數組的函數按照上面 gif 圖的順序執行。

3.1.1 compose 函數

簡單來說,compose 函數主要做了兩件事情。

  1. 接收一個參數,校驗參數是數組,且校驗數組中的每一項是函數。

    1. 返回一個函數,這個函數接收兩個參數,分別是contextnext,這個函數最后返回Promise

    /***?Compose?`middleware`?returning*?a?fully?valid?middleware?comprised*?of?all?those?which?are?passed.**?@param?{Array}?middleware*?@return?{Function}*?@api?public*/
    function?compose?(middleware)?{//?校驗傳入的參數是數組,校驗數組中每一項是函數if?(!Array.isArray(middleware))?throw?new?TypeError('Middleware?stack?must?be?an?array!')for?(const?fn?of?middleware)?{if?(typeof?fn?!==?'function')?throw?new?TypeError('Middleware?must?be?composed?of?functions!')}/***?@param?{Object}?context*?@return?{Promise}*?@api?public*/return?function?(context,?next)?{//?last?called?middleware?#let?index?=?-1return?dispatch(0)function?dispatch(i){//?省略,下文講述}}
    }
    

    接著我們來看 dispatch 函數。

    3.1.2 dispatch 函數

    function?dispatch?(i)?{//?一個函數中多次調用報錯//?await?next()//?await?next()if?(i?<=?index)?return?Promise.reject(new?Error('next()?called?multiple?times'))index?=?i//?取出數組里的?fn1,?fn2,?fn3...let?fn?=?middleware[i]//?最后?相等,next?為?undefinedif?(i?===?middleware.length)?fn?=?next//?直接返回?Promise.resolve()if?(!fn)?return?Promise.resolve()try?{return?Promise.resolve(fn(context,?dispatch.bind(null,?i?+?1)))}?catch?(err)?{return?Promise.reject(err)}
    }
    

    值得一提的是:bind函數是返回一個新的函數。第一個參數是函數里的this指向(如果函數不需要使用this,一般會寫成null)。這句fn(context, dispatch.bind(null, i + 1)i + 1 是為了 let fn = middleware[i]middleware中的下一個函數。也就是 next 是下一個中間件里的函數。也就能解釋上文中的 gif圖函數執行順序。測試用例中數組的最終順序是[1,2,3,4,5,6]

    3.1.3 簡化 compose 便于理解

    自己動手調試之后,你會發現 compose 執行后就是類似這樣的結構(省略 try catch 判斷)。

    //?這樣就可能更好理解了。
    //?simpleKoaCompose
    const?[fn1,?fn2,?fn3]?=?stack;
    const?fnMiddleware?=?function(context){return?Promise.resolve(fn1(context,?function?next(){return?Promise.resolve(fn2(context,?function?next(){return?Promise.resolve(fn3(context,?function?next(){return?Promise.resolve();}))}))}));
    };
    

    也就是說koa-compose返回的是一個Promise,從中間件(傳入的數組)中取出第一個函數,傳入context和第一個next函數來執行。
    第一個next函數里也是返回的是一個Promise,從中間件(傳入的數組)中取出第二個函數,傳入context和第二個next函數來執行。
    第二個next函數里也是返回的是一個Promise,從中間件(傳入的數組)中取出第三個函數,傳入context和第三個next函數來執行。
    第三個...
    以此類推。最后一個中間件中有調用next函數,則返回Promise.resolve。如果沒有,則不執行next函數。這樣就把所有中間件串聯起來了。這也就是我們常說的洋蔥模型。

    洋蔥模型圖如下圖所示:

    不得不說非常驚艷,“玩還是大神會玩”

    3.2 錯誤捕獲

    it('should?catch?downstream?errors',?async?()?=>?{const?arr?=?[]const?stack?=?[]stack.push(async?(ctx,?next)?=>?{arr.push(1)try?{arr.push(6)await?next()arr.push(7)}?catch?(err)?{arr.push(2)}arr.push(3)})stack.push(async?(ctx,?next)?=>?{arr.push(4)throw?new?Error()})await?compose(stack)({})//?輸出順序?是?[?1,?6,?4,?2,?3?]expect(arr).toEqual([1,?6,?4,?2,?3])
    })
    

    相信理解了第一個測試用例和 compose 函數,也是比較好理解這個測試用例了。這一部分其實就是對應的代碼在這里。

    try?{return?Promise.resolve(fn(context,?dispatch.bind(null,?i?+?1)))
    }?catch?(err)?{return?Promise.reject(err)
    }
    

    3.3 next 函數不能調用多次

    it('should?throw?if?next()?is?called?multiple?times',?()?=>?{return?compose([async?(ctx,?next)?=>?{await?next()await?next()}])({}).then(()?=>?{throw?new?Error('boom')},?(err)?=>?{assert(/multiple?times/.test(err.message))})
    })
    

    這一塊對應的則是:

    index?=?-1
    dispatch(0)
    function?dispatch?(i)?{if?(i?<=?index)?return?Promise.reject(new?Error('next()?called?multiple?times'))index?=?i
    }
    

    調用兩次后 iindex 都為 1,所以會報錯。

    compose/test/test.js文件中總共 300余行,還有很多測試用例可以按照文中方法自行調試。

    4. 總結

    雖然koa-compose源碼 50行 不到,但如果是第一次看源碼調試源碼,還是會有難度的。其中混雜著高階函數、閉包、Promisebind等基礎知識。

    通過本文,我們熟悉了 koa-compose 中間件常說的洋蔥模型,學會了部分 `jest`[6] 用法,同時也學會了如何使用現成的測試用例去調試源碼。

    相信學會了通過測試用例調試源碼后,會覺得源碼也沒有想象中的那么難

    開源項目,一般都會有很全面的測試用例。除了可以給我們學習源碼調試源碼帶來方便的同時,也可以給我們帶來的啟發:自己工作中的項目,也可以逐步引入測試工具,比如 jest

    此外,讀開源項目源碼是我們學習業界大牛設計思想和源碼實現等比較好的方式。

    看完本文,非常希望能自己動手實踐調試源碼去學習,容易吸收消化。另外,如果你有余力,可以繼續看我的 koa-compose 源碼文章:學習 koa 源碼的整體架構,淺析koa洋蔥模型原理和co原理

    參考資料

    [1]

    koa-compose 倉庫: https://github.com/koajs/compose

    [2]

    本文倉庫地址 koa-compose-analysis: https://github.com/lxchuan12/koa-compose-analysis.git

    [3]

    用 Git Subtree 在多個 Git 項目間雙向同步子項目,附簡明使用手冊: https://segmentfault.com/a/1190000003969060

    [4]

    更多 nodejs 調試相關 可以查看官方文檔: https://code.visualstudio.com/docs/nodejs/nodejs-debugging

    [5]

    koa的文檔: https://github.com/koajs/koa/blob/master/docs/guide.md#writing-middleware

    [6]

    jest: https://github.com/facebook/jest

    最近組建了一個江西人的前端交流群,如果你是江西人可以加我微信?ruochuan12?私信 江西?拉你進群。


    推薦閱讀

    1個月,200+人,一起讀了4周源碼
    我讀源碼的經歷

    老姚淺談:怎么學JavaScript?

    我在阿里招前端,該怎么幫你(可進面試群)

    ·················?若川簡介?·················

    你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》多篇,在知乎、掘金收獲超百萬閱讀。
    從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
    同時,活躍在知乎@若川,掘金@若川。致力于分享前端開發經驗,愿景:幫助5年內前端人走向前列。

    識別方二維碼加我微信、拉你進源碼共讀

    今日話題

    略。歡迎分享、收藏、點贊、在看我的公眾號文章~

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/275441.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/275441.shtml
英文地址,請注明出處:http://en.pswp.cn/news/275441.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

如何定位死循環或高CPU使用率(linux)

如何定位死循環或高CPU使用率(linux) 確定是CPU過高 使用top觀察是否存在CPU使用率過高現象 找出線程 對CPU使用率過高的進程的所有線程進行排序 ps H -e -o pid,tid,pcpu,cmd --sortpcpu |grep xxx 得到如下結果,其中線程2909使用了7.8%的CPU. 2907 2913 0.0 ./xxx 2907…

js 用迭代器模式優雅的處理遞歸問題

2019獨角獸企業重金招聘Python工程師標準>>> 什么是迭代器 循環數組或對象內每一項值&#xff0c;在 js 里原生已經提供了一個迭代器。 var arr [1, 2, 3] arr.forEach(function (item) {console.log(item) })實現一個迭代器 var iterator function (arr, cb) {fo…

如何抓取html請求,請求獲取網頁的response,獲取網頁的html 怎么那么慢

HttpEntity multipart builder.build();httppost.setEntity(multipart);long start System.currentTimeMillis();// 發送請求response httpclient.execute(httppost);long end System.currentTimeMillis();System.out.println("查詢upsfreight消耗的時間是(毫秒):&quo…

Serverless 究竟是什么?

大家好&#xff0c;我是若川。說起 Serverless&#xff0c;我想你應該并不陌生&#xff0c;作為一種云開發的架構模式&#xff0c;在近兩年里&#xff0c;伴隨著云原生概念的推廣愈發火爆。作為一名 Serverless 的擁躉&#xff0c;在跟大家推薦的過程中&#xff0c;我經常能看到…

instagram.apk_評論:Instagram Reels vs.TikTok

instagram.apkWith all the attention to the newly debuted Instagram Reels from Facebook and the hilarious, bizarre world of TikTok, here’s a first impression on the two platforms and how they compare from a designer’s perspective.所有人都在關注Facebook新近…

240多個jQuery常用到的插件

概述 jQuery 是繼 prototype 之后又一個優秀的 Javascript 框架。其宗旨是—寫更少的代碼,做更多的事情。它是輕量級的 js 庫(壓縮后只有21k) &#xff0c;這是其它的 js 庫所不及的&#xff0c;它兼容 CSS3&#xff0c;還兼容各種瀏覽器&#xff08;IE 6.0, FF 1.5, Safari 2.…

華為首款鴻蒙設備正式入網,華為首款鴻蒙設備正式入網:麒麟9000+挖孔全面屏,價格感人!...

作為國內電子產品領域的巨頭之一&#xff0c;華為這兩年的快速發展是大眾有目共睹的&#xff0c;除了手機業務外&#xff0c;華為的平板業務同樣有亮眼表現&#xff0c;無獨有偶&#xff0c;在近期各方媒體的不斷披露之下&#xff0c;又有一款華為平板被基本確認&#xff0c;這…

早上讀英語

#早上讀英語# Keep your eyes on the stars and your feet on the ground. 轉載于:https://www.cnblogs.com/da3j/p/10646531.html

myeclipse深色模式_完善深色模式的調色板

myeclipse深色模式Apps largely have a limited color palette which may already map well to dark mode. However, some colors produce optical vibrations when viewed on a dark background, straining the user’s eyes. So, certain apps need to map to a slightly des…

微軟悄悄發布了 Web 版的 VsCode

大家好&#xff0c;我是若川&#xff0c;最近組織了源碼共讀活動《1個月&#xff0c;200人&#xff0c;一起讀了4周源碼》&#xff0c;感興趣的可以加我微信 ruochuan12 參與&#xff0c;長期交流學習。在8月31日&#xff0c;微軟發了一個介紹他們新發布的功能的帖子介紹&#…

html中樣式表的三種形式,CSS樣式表有幾種存在方式

外部樣式&#xff1a;將網頁鏈接到外部樣式表。內頁樣式&#xff1a;在網頁上創建嵌入的樣式表。行內標簽樣式&#xff1a;應用內嵌樣式到各個網頁元素標簽內。每一種方法均有其優缺點&#xff1a;當要在站點上所有或部份的網頁上一致地應用相同樣式時&#xff0c;可使用外部樣…

figma設計_設計原型的最簡單方法:Figma速成課程

figma設計It doesn’t matter if you haven’t used any prototyping tools before or you’re transitioning from other ones (like Sketch, Adobe XD); This guide is for beginners and professionals alike. So for a university assignment, I had to prepare a presenta…

《大話數據結構》讀后總結(九)

線性表 順序存儲結構的插入與刪除 獲得元素操作 #define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 typedef int Status; /* Status是函數的類型&#xff0c;其值是函數結果狀態代碼&#xff0c;如OK等 */ /* 初始條件&#xff1a;順序線性表L已存在&#xff0c;1≤…

關于router name 的url重寫 --frontname rewrite frontname重寫!

對于tag 也就是在url中有tag的url&#xff0c;希望改成wholesale等其他方式&#xff0c; 1 參看blog插件方式&#xff1a; 在etc/config.xml中添加事件&#xff1a; <events> <sitemap_add_xml_block_to_the_end> <observers> …

初中級工程師如何快速成長和尋求突破

大家好&#xff0c;我是若川&#xff0c;最近組織了源碼共讀活動《1個月&#xff0c;200人&#xff0c;一起讀了4周源碼》&#xff0c;已經有超50人提交了筆記&#xff0c;群里已經有超1200人&#xff0c;感興趣的可以加我微信 ruochuan12 參與。前言寫這篇文章的初衷是因為看到…

ajax使用html()后樣式無效,jquery.ajax使用字符串拼接后內聯css樣式失效

問題所在:是這樣的,我使用ajax調用了一串json數據,使用字符串拼接的方法動態插入div容器.結果css并沒有對動態插入的內容加css樣式.代碼描述:css使用的內聯,在head部分, jquery使用外聯,在body后.我嘗試過:$(function(){}) //入口函數加載window.onload function(){} //原生do…

ios 按鈕圖片充滿按鈕_iOS有一些非常危險的按鈕-UX評論

ios 按鈕圖片充滿按鈕I recently bought a cool thing off Amazon. It’s an adapter for iPhone, making it easy to transfer photos from your big bulky camera to your phone. The adapter itself is very easy to use: simply insert your SD card and plug the adapter …

URLScan工具配置方法第1/2頁

本文分步說明如何配置 URLScan 工具以防止 Web 服務器受到攻擊和利用。 如何配置 URLScan 工具察看本文應用于的產品文章編號 : 326444最后修改 : 2007年3月14日修訂 : 5.3我們強烈建議所有運行 Microsoft Windows Server 2003 的用戶將 Microsoft Internet 信息服務 (IIS) 升級…

poj 1809

///一個點的坐標只有四種可能&#xff0c;用0表示 偶數&#xff0c;1表示奇數 &#xff0c;則四種可能為&#xff08;0,0&#xff09;&#xff0c;&#xff08;0,1&#xff09; ///&#xff08;1,0&#xff09;&#xff0c;&#xff08;1,1&#xff09;。觀察公式A|x1y2 - y1…

swiftui_SwiftUI的混合包

swiftui介紹 (Introduction) SwiftUI introduced us to a whole new way of designing and coding interfaces. Gone are the old ways of subclassing UIKit (or AppKit) classes and hardwiring layout constraints. Instead, we now have a nice, declarative way of struct…