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.mzph.cn/news/275441.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

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;这…

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;微软发了一个介绍他们新发布的功能的帖子介绍&#…

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…

初中级工程师如何快速成长和寻求突破

大家好&#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 …

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…

三年经验前端社招——有赞

大家好&#xff0c;我是若川&#xff0c;祝大家中秋节快乐。最近组织了源码共读活动《1个月&#xff0c;200人&#xff0c;一起读了4周源码》&#xff0c;已经有超50人提交了笔记&#xff0c;群里已经有超1200人&#xff0c;感兴趣的可以点此链接扫码加我微信 ruochuan12 参与。…

html的 button点击事件无效,InfoWindow里面加button,监听button点击事件无效 求解啊...

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼点击infoWindw中的button按钮&#xff0c;无效果&#xff1b;覆盖默认的dom结构html,body,#container {width: 100%;height: 100%;margin: 0px;}p.my-desc {margin: 5px 0;line-height: 150%;}//创建地图var map new AMap.Map(con…

数据挖掘 点击更多 界面_8(更多)技巧,可快速改善用户界面

数据挖掘 点击更多 界面重点 (Top highlight)Creating beautiful, usable, and efficient UIs takes time, with many design revisions along the way. Making those constant tweaks to produce something that your clients, users, and yourself are truly happy with. I k…

三年经验前端社招——腾讯微保

大家好&#xff0c;我是若川。祝大家中秋节快乐。最近组织了源码共读活动《1个月&#xff0c;200人&#xff0c;一起读了4周源码》&#xff0c;已经有超50人提交了笔记&#xff0c;群里已经有超1200人&#xff0c;感兴趣的可以点此链接扫码加我微信 ruochuan12 参与。本文经作者…

matlab绘制路线图_绘制国际水域路线图

matlab绘制路线图Two years ago, Shopify was only available in English. Few people in Germany or Japan had heard about us. We had only just formed the international growth team to make Shopify available to people in their native tongue.两年前&#xff0c;Shop…

2021年江苏高考各科成绩查询,江苏2021年高考总分及各科分数

江苏2021年高考总分及各科分数2021-04-16 08:46:02文/董月江苏高考将实施“33”模式&#xff0c;即语数外三门必考&#xff0c;然后在物理、化学、生物、历史、政治、地理六门学科中任选三门进行考试&#xff0c;并计入总分。“6选3”中的3门以等级确定&#xff0c;折算成分数计…

figma下载_通过构建7个通用UI动画来掌握Figma中的动画

figma下载Originally published on my personal blog.最初发布在我的 个人博客上 。 Most designers will spend many hours perfecting every pixel of their static UI designs but will barely spend any time perfecting the transitions between these pages.大多数设计人…