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年内前端人走向前列。

    识别方二维码加我微信、拉你进源码共读

    今日话题

    略。欢迎分享、收藏、点赞、在看我的公众号文章~

50行代码串行Promise,koa洋葱模型原来这么有趣?相关推荐

  1. 学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

    前言 这是学习源码整体架构系列第七篇.整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现.本篇文章学习的是实际仓库的代码. 学习源码整体 ...

  2. 模拟koa洋葱模型实现

    koa洋葱模型即是注册的中间件采取先进后出的运行策略. 问题 app.use(async next => {console.log(1);await next();console.log(2); ...

  3. 【koa系列】koa洋葱模型及其compose原理解析

    什么是洋葱模型 先来看一个 demo const Koa = require('koa'); const app = new Koa();// 中间件1 app.use((ctx, next) =&g ...

  4. python实现50行代码_50行代码实现python计算器主要功能

    实现功能:计算带有括号和四则运算的式子 3*( 4+ 50 )-(( 100 + 40 )*5/2- 3*2* 2/4+9)*((( 3 + 4)-4)-4) 基本思路:使用正则表达式提取出每一层小括 ...

  5. 几行代码构建全功能对象检测模型,这位杜克大学学生做到了!

    作者 | Alan Bi 译者 | 武明利 责编 | Carol 出品 | AI科技大本营(ID:rgznai100) 如今,机器学习和计算机视觉已成为一种热潮.我们都看过关于自动驾驶汽车和面部识别的 ...

  6. python200行代码_200行Python代码实现2048

    200行Python代码实现2048 一.实验说明 1. 环境登录 无需密码自动登录,系统用户名shiyanlou 2. 环境介绍 本实验环境采用带桌面的Ubuntu Linux环境,实验中会用到桌面 ...

  7. python编写游戏300行代码_300行代码实现Python游戏:俄罗斯方块

    本文代码基于 python3.6 和 pygame1.9.4. 俄罗斯方块是儿时最经典的游戏之一,刚开始接触 pygame 的时候就想写一个俄罗斯方块.但是想到旋转,停靠,消除等操作,感觉好像很难啊, ...

  8. 洋葱模型php,理解Koa洋葱模型

    中间件特性 | | | middleware 1 | | | | +-----------------------------------------------------------+ | | | ...

  9. koa - 洋葱模型浅析

    1.什么是koa? Koa是一个精简的node框架,被认为是第二代Node框架,其最大的特点就是独特的中间件流程控制,是一个典型的洋葱模型,它的核心工作包括下面两个方面: 将node原生的req和re ...

最新文章

  1. 2021年大数据常用语言Scala(四):基础语法学习 声明变量
  2. ListView与DateGridView
  3. dockerfile各种命令解析
  4. 自定义注解:通过监控文件的变化,实时更改工厂管理的实例
  5. 传统公司部署OpenStack(t版)简易介绍(六)——neutron模块部署
  6. iOS-仿支付宝加载web网页添加进度条
  7. 1039. Course List for Student (25)
  8. 在线JSON在线对比差异工具
  9. ps数位板绘画遇到问题总结
  10. Tableau Desktop 2020 Mac支持M1芯片big sur 解决M1芯片安装Tableau闪退问题教程Tableau Public
  11. 加多宝首度披露"换头手术"的详细内幕
  12. 如何制作一个漂亮的网页
  13. iOS--通过assetURL获取到视频
  14. 读 John Tosh 之《史学导论:现代历史学的目标、方法和新方向》
  15. 如何让一个PNG图片背景透明
  16. C++实验题21 破解简单密码
  17. 渠道分析 之 渠道分析的价值 -3
  18. win10无线投屏_win10电脑投屏要无线投屏器吗?
  19. 从游戏到赚钱,区块链如何改变电子游戏的面貌?
  20. 编程之余对人品的感悟

热门文章

  1. Unity 协程原理探究与实现
  2. Python在mysql中进行操作是十分容易和简洁的
  3. 制作镜像包时遇到的模块加载错误的问题
  4. ubuntu16.04 编译出错:fatal error: SDL/SDL.h: No such file or directory
  5. STM32安装Keil5、芯片支持包、startup启动文件(启动过程分析)、建立工程、烧写
  6. Ubuntu磁盘扩容及启动问题整理
  7. mysql redis hbase_MySQL之基本介绍
  8. activiti idea 请假流程_IDEA开发流程Activiti需要注意的一些坑
  9. linux 搜索 文件 内容,Linux 文件查找及文件内容查找
  10. mysql rename所标时间_mysql rename命令