1。前言大家好,我是若川。欢迎关注我的公众号若川视界。最近组织了一个源码分享活动《1个月,200+人,一起读了4周源码》。有兴趣的可以加我微信ruochuan12参与长期交流学习。之前写的《学习源码整体架构系列》包括jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4的十多篇源码文章。最新的两篇文章是:Vue3.2发布了,游鱼溪是怎么发布Vue.js的?Vue3源码中初学者能看懂的实用基础工具功能,编写源码难度相对较大,耗费了自己的时间和精力,也得不到多少阅读点赞,其实是相当打击。从阅读量和读者利益来看,并不能促进作者持续的文章输出。所以换个思路,写一些比较通俗易懂的文章。其实源码并没有想象中那么难,至少很多人都能看懂。之前写过一篇koa源码文章,学习一下koa源码的整体结构。koa洋葱模型原理和co原理的分析比较长,读者朋友估计看不完。因此,本文从koa-compose的50行源码开始。本文涉及的koa-compose仓库文件,虽然整个index.js文件只有不到50行代码,测试用例test/test.js文件也有300多行,但是非常值得学习。歌德曾说过:读一本好书,就是与一个高尚的人交谈。可以得出同样的道理:阅读源码也是一种学习和与作者交流的方式。阅读本文后,您将学习到:1.熟悉koa-compose中间件源码,能够应对面试官相关问题2.学习使用测试用例调试源码3.学习jest的一些用法2.环境准备2.1clonekoa-compose项目仓库地址koa-compose-analysis,求一个star~#可以直接clone我的仓库,compose仓库的git记录保存在我的仓库gitclonehttps://github。com/lxchuan12/koa-compose-analysis.gitcdkoa-compose/composenpm我顺便说一句:如何保存compose仓库的git记录。#在github上新建一个仓库`koa-compose-analysis`并克隆gitclonehttps://github.com/lxchuan12/koa-compose-analysis.gitcdkoa-compose-analysisgitsubtreeadd--prefix=composehttps://github.com/koajs/compose.gitmain#这会将compose文件夹克隆到您自己的git存储库中。以及预留的git记录更多的git子树可以看这篇文章使用GitSubtree在多个Git项目间双向同步子项目,简明扼要的手册。那我们看看如何根据开源项目中提供的测试用例调试源码。2.2根据测试用例调试compose源码用VSCode打开项目(我的版本是1.60),找到compose/package.json,找到脚本和测试命令。//compose/package.json{"name":"koa-compose",//debug"scripts":{"eslint":"standard--fix.","test":"jest"},}应该有在脚本之上进行调试或调试。单击调试并选择测试。然后将执行测试用例test/test.js文件。终端输出如下图所示。然后我们调试compose/test/test.js文件。我们可以在第45行打个断点,再次点击package.json=>srcipts=>test进入调试模式。如下所示。然后按上面的按钮继续调试。在compose/index.js文件关键地方打断点,调试学习源码事半功倍。更多nodejs调试相关,可以查看官方文档,对后面几个调试相关的按钮进行详细说明。继续(F5):点击后,代码会直接执行到下一个断点所在的位置。如果没有下一个断点,则认为代码执行完毕。Stepover(F10):点击后会跳转到当前代码的下一行继续执行,不进入函数。单步调试(F11):点击进入当前函数的内部调试。比如在compose行执行单步调试,就会进入compose函数进行调试。跳出(Shift+F11):点击跳出当前正在调试的函数,对应单步调试。重启(Ctrl+Shift+F5):顾名思义。断开链接(Shift+F5):顾名思义。接下来,我们跟着测试用例学习源码。3.跟随测试用例学习源码,分享一个测试用例小技巧:我们可以只对测试用例添加修改。//比如it.only('shouldwork',async()=>{})这样我们就可以只执行当前的测试用例,不关心其他的,不会干扰调试。3.1正常流程打开compose/test/test.js文件,看到第一个测试用例。//compose/test/test.js'usestrict'/*eslint-envjest*/constcompose=require('..')constassert=require('assert')functionwait(ms){returnnewPromise((resolve)=>setTimeout(resolve,ms||1))}//Groupdescribe('KoaCompose',function(){it.only('shouldwork',async()=>{constarr=[]conststack=[]stack.push(async(context,next)=>{arr.push(1)awaitwait(1)awaitnext()awaitwait(1)arr.push(6)})stack.push(async(context,next)=>{arr.push(2)awaitwait(1)awaitnext()awaitwait(1)arr.push(5)})stack.push(async(context,next)=>{arr.push(3)awaitwait(1)awaitnext()awaitwait(1)arr.push(4)})awaitcompose(stack)({})//最终输出数组为[1,2,3,4,5,6]expect(arr).toEqual(expect.arrayContaining([1,2,3,4,5,6]))})}看完这个测试用例,上下文是什么,接下来是什么.koa文档上有一张很有代表性的中间件gif。compose函数的作用是按照上面gif的顺序执行添加到中间件数组中的函数。3.1.1Compose函数简单来说,compose函数主要做了两件事。接收一个参数,验证参数是一个数组,验证数组中的每一项都是一个函数。返回一个函数,接收context和next两个参数,最后返回一个Promise。/***组合`middleware`返回*一个完全有效的中间件,包含*所有通过的中间件。**@param{Array}middleware*@return{Function}*@apipublic*/functioncompose(middleware){//验证传入的参数是一个数组,验证数组中的每一项都是一个函数if(!Array.isArray(middleware))thrownewTypeError('Middlewarestackmustbeanarray!')for(constfnofmiddleware){if(typeoffn!=='function')thrownewTypeError('Middlewaremustbecomposedof函数!')}/***@param{Object}context*@return{Promise}*@apipublic*/returnfunction(context,next){//最后调用的中间件#letindex=-1returndispatch(0)functiondispatch(i){//省略,下面有说明}}}然后我们来看dispatch函数。3.1.2dispatchfunctionfunctiondispatch(i){//函数中多次调用报错//awaitnext()//awaitnext()if(i<=index)returnPromise.reject(newError('next()多次调用'))index=i//从数组中取出fn1,fn2,fn3...letfn=middleware[i]//最后相等,next未定义if(i===middleware.length)fn=next//直接returnPromise.resolve()if(!fn)returnPromise.resolve()try{returnPromise.resolve(fn(context,dispatch.bind(null,i+1)))}catch(err){returnPromise.reject(err)}}值得一提的是,bind函数返回了一个新函数。第一个参数是函数中的this点(如果函数不需要用到this,一般会写成null)。这句fn(context,dispatch.bind(null,i+1),i+1是让fn=middleware[i]取中间件中的下一个函数。即next是下一个中间件中的函数。还有可以解释上面gif图函数的执行顺序,测试用例中数组的最终顺序是[1,2,3,4,5,6]。3.1.3简化compose自己调试后会findcompose执行后会有类似这样的结构(省略trycatch判断)//这样可能更容易理解。//simpleKoaComposeconst[fn1,fn2,fn3]=stack;constfnMiddleware=function(context){返回Promise。resolve(fn1(context,functionnext(){returnPromise.resolve(fn2(context,functionnext(){returnPromise.resolve(fn3(context,functionnext(){returnPromise.resolve();}))}))}));};也就是说koa-compose返回的是一个Promise,是从middleware数组传进来的),取出第一个函数,传入context和第一个next函数执行。第一个next函数也返回一个Promise,从中间件(引入数组)中取出第二个函数,传入上下文和第二个next函数执行。第二个next函数也返回一个Promise,从中间件(传递的数组)中取出第三个函数,并传入上下文和第三个next函数来执行。第三个……等等。如果在最后一个中间件中调用了next函数,则返回Promise.resolve。如果不是,则不执行下一个函数。这将串联所有中间件。这就是我们常说的洋葱模型。不得不说非常了不起,“玩起来还是高手会玩”。3.2错误捕获it('shouldcatchdownstreamerrors',async()=>{constarr=[]conststack=[]stack.push(async(ctx,next)=>{arr.push(1)try{arr.push(6)awaitnext()arr.push(7)}catch(err){arr.push(2)}arr.push(3)})stack.push(async(ctx,next)=>{arr.push(4)thrownewError()})awaitcompose(stack)({})//输出顺序为[1,6,4,2,3]expect(arr).toEqual([1,6,4,2,3])})相信理解了第一个测试用例和compose函数之后,对这个测试用例的理解就更好了。这部分其实就是这里对应的代码。try{returnPromise.resolve(fn(context,dispatch.bind(null,i+1)))}catch(err){returnPromise.reject(err)}3.3next函数不能多次调用it('shouldthrow如果多次调用next()',()=>{returncompose([async(ctx,next)=>{awaitnext()awaitnext()}])({}).then(()=>{thrownewError('boom')},(err)=>{assert(/multipletimes/.test(err.message))})})对应于:index=-1dispatch(0)functiondispatch(i){if(i<=index)returnPromise.reject(newError('next()calledmultipletimes'))index=i}调用两次后,i和index都为1,所以会报错。compose/test/test.js文件一共300多行,里面有很多测试用例,可以按照文中的方法自行调试。4.小结虽然koa-compose的源码不到50行,但是如果是第一次调试源码还是有难度的。混合了高阶函数、闭包、Promise、bind等基础知识。通过这篇文章,我们熟悉了koa-compose中间件中经常提到的洋葱模型,学习了一些jest用法,也学习了如何使用现成的测试用例调试源码。相信在学会了通过测试用例调试源码之后,你会觉得源码并没有想象中那么难。开源项目一般都有非常全面的测试用例。除了给我们学习源码和调试源码带来方便之外,还可以给我们带来启发:工作中的项目也可以逐步引入测试工具,比如jest。另外,阅读开源项目的源码,是我们更好地学习行业巨头的设计思路和源码实现的途径。看完这篇文章后,非常希望能自己练习调试源码来学习,容易吸收和消化。另外,有余力的话可以继续看我的koa-compose源码文章:学习koa源码整体结构,分析koa洋葱模型原理和co原理
