头图

50 lines of code serial Promise, how is the koa onion model originally implemented?

1 Introduction

Hello everyone, my name is . Welcome to follow my 161567fad7facd public , and recently organized the source code reading activity "1 month, 200+ people, read the source code together for 4 weeks" , interested can add me 161567 Participate, exchange and learn for a long time.

The previously written "Learning Source Code Overall Architecture Series" contains jQuery , underscore , lodash , vuex , sentry , axios , redux , koa , vue-devtools , vuex4 The two latest ones are:

Vue 3.2 is released. How did You Yuxi release Vue.js?

Those practical basic tool functions in the Vue3 source code that beginners can understand

Writing relatively difficult source code consumes my time and energy, and I didn't get many reading likes. In fact, it was quite a blow. From the perspective of reading volume and reader benefits, it cannot promote the author's continuous output of articles.

So change your mind and write some relatively easy-to-understand articles. is not as difficult as imagined. At least many of them can understand .

I wrote the koa source code article learn the overall architecture of the koa source code before, analyze the koa onion model principle and the co principle koa-compose , so this article describes from the source code of 061567fad7fbaa 50 lines.

koa-compose warehouse file involved in this article, the entire index.js file has less than 50 lines of code, and the test case test/test.js file 300 more than 6 lines, but it is worth learning.

Goethe once said: Reading a good book is talking to a noble person. The same is true: reading the source code can also be regarded as a way of learning and communicating with the author.

After reading this article, you will learn:

1. 熟悉 koa-compose 中间件源码、可以应对面试官相关问题
2. 学会使用测试用例调试源码
3. 学会 jest 部分用法

2. Environmental preparation

2.1 Clone the koa-compose project

This article warehouse address koa-compose-analysis , ask for a star ~

# 可以直接克隆我的仓库,我的仓库保留的 compose 仓库的 git 记录
git clone https://github.com/lxchuan12/koa-compose-analysis.git
cd koa-compose/compose
npm i

By the way: how do I keep the git record in the compose

# 在 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 记录

For more information about git subtree , you can read this article Use Git Subtree to synchronize sub-projects between multiple Git projects in both directions, with a concise user manual

Then we look at how to debug the source code according to the test cases provided in the open source project.

2.2 Debug compose source code based on test cases

Open the project with VSCode (my version is 1.60 compose/package.json , find scripts and test commands.

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

scripts there should be debug or debugging words. Click debug (Debug) and select test .

VSCode 调试

Then the test case test/test.js file will be executed. The terminal output is shown in the figure below.

koa-compose 测试用例输出结果

Then we debug the compose/test/test.js file.
We can hit a breakpoint on line package.json => srcipts => test enter debug mode.
As shown below.

koa-compose 调试

Then press the button above to continue debugging. compose/index.js breakpoints in key places in the 061567fad7fe2d file, and debug and learn the source code with less effort.

more nodejs debugging related, please refer to the official document

By the way, explain in detail the next few debugging related buttons.

    1. Continue (F5): After clicking, the code will be executed directly to the location of the next breakpoint. If there is no next breakpoint, it is considered that the code execution is completed.
    1. Single step skip (F10): After clicking, it will jump to the next line of the current code to continue execution without entering the function.
    1. Single step debugging (F11): Click to enter the internal debugging of the current function. For example, if you execute single step debugging compose compose function for debugging.
    1. Step out (Shift + F11): Click to jump out of the currently debugged function, which corresponds to single step debugging.
    1. Restart (Ctrl + Shift + F5): As the name implies.
    1. Disconnect link (Shift + F5): As the name implies.

Next, we follow the test case to learn the source code.

3. Follow the test case to learn the source code

Share a test case tip: We can add only modification to the test case.

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

In this way, we can only execute the current test case, do not care about the others, and will not interfere with debugging.

3.1 Normal process

Open the compose/test/test.js file and see the first test case.

// 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]))
  })
}

After reading this test case, context is next and what is 061567fad80022.

In koa document has a very typical middleware gif map.

中间件 gif 图

The compose functions added to the middleware array in the order of the gif

3.1.1 compose function

Simply put, the compose function mainly does two things.

    1. Receive a parameter, the check parameter is an array, and each item in the check array is a function.
    1. Return a function, this function receives two parameters, which are context and next , this function finally returns 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 = -1
    return dispatch(0)
    function dispatch(i){
      // 省略,下文讲述
    }
  }
}

Then we look at the dispatch function.

3.1.2 dispatch function

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 为 undefined
  if (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)
  }
}

It is worth mentioning that: The bind function returns a new function. The first parameter is the this point in the function (if the function does not need to use this , it will generally be written as null ).
This sentence fn(context, dispatch.bind(null, i + 1) , i + 1 is for let fn = middleware[i] take the next function in middleware
That is, next is the function in the next middleware. Can also explain the above gif graph function execution order.
The final order of the array in the test case is [1,2,3,4,5,6] .

3.1.3 Simplify compose for easy understanding

After debugging by yourself, you will find that compose is similar to this structure after execution (omit try catch judgment).

// 这样就可能更好理解了。
// 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();
                  })
              )
          })
        )
    })
  );
};
That is to say, koa-compose returns a Promise . Take the first function from the middleware (the incoming array) context and the first next function to execute.

The first next function also returns a Promise . Take the second function from the middleware (the incoming array), and pass in the context and the second next function to execute.

The second next function also returns a Promise , the third function is taken middleware (the incoming array) context and the third next function are passed in for execution.

The third...

And so on. next function 061567fad802b4 is called in the last middleware, it returns Promise.resolve . If not, the next function is not executed.
In this way, all middleware are connected in series. This is what we often call the onion model.

洋葱模型图如下图所示:

to be said to be very amazing, "It's still a great god to play" .

3.2 Error trap

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])
})

I believe that if I understand the first test case and the compose function, it is also a better understanding of this test case. This part is actually the corresponding code here.

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

3.3 The next function cannot be called multiple times

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))
  })
})

This piece corresponds to:

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

After two calls, i and index are both 1 , so an error will be reported.

compose/test/test.js are a total of more than 300 lines in the 061567fad803b4 file, and there are many test cases that can be debugged according to the method in the article.

4. Summary

Although the koa-compose less than 50 lines, if it is the first time to look at the source code and debug the source code, it will still be difficult. There are mixed basic knowledge of higher-order functions, closures, Promise , bind and so on.

Through this article, we are familiar with the koa-compose by the 061567fad80407 middleware, learned part of the jest usage, and also learned how to use the ready-made test cases to debug the source code.

believes that after learning to debug the source code through test cases, you will feel that the source code is not as difficult as you imagined .

Open source projects generally have very comprehensive test cases. In addition to the convenience of learning source code and debugging source code, it can also give us inspiration: You can gradually introduce testing tools for your projects, such as jest .

In addition, reading the source code of open source projects is a better way for us to learn the design ideas and source code implementation of the industry's big cows.

After reading this article, I very much hope that I can practice debugging the source code to learn, easy to absorb and digest. In addition, if you have more energy, you can continue to read my koa-compose source code article: Learn the overall architecture of koa source code, analyze the principle of koa onion model and co principle


若川视野公众号
发布前端技术文章和随笔等。公众号若川视野,公众号经常更新,欢迎关注,长期交流学习。

你好,我是若川。写有 《学习源码整体架构系列》 20余篇。

7k 声望
3.2k 粉丝
0 条评论
推荐阅读
尤雨溪推荐神器 ni ,能替代 npm/yarn/pnpm ?简单好用!源码揭秘!
想学源码,极力推荐之前我写的《学习源码整体架构系列》 包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、vue-next-release、vue-this、create-vue等十余...

若川6阅读 6.3k

从零搭建 Node.js 企业级 Web 服务器(零):静态服务
过去 5 年,我前后在菜鸟网络和蚂蚁金服做开发工作,一方面支撑业务团队开发各类业务系统,另一方面在自己的技术团队做基础技术建设。期间借着 Node.js 的锋芒做了不少 Web 系统,有的至今生气蓬勃、有的早已夭折...

乌柏木140阅读 11.9k评论 10

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木60阅读 5.9k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs39阅读 6.1k评论 12

封面图
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木39阅读 7.1k评论 6

CSS 绘制一只思否猫
欢迎关注我的公众号:前端侦探练习 CSS 有一个比较有趣的方式,就是发挥想象,绘制各式各样的图案,比如来绘制一只思否猫?思否猫,SegmentFault 思否的吉祥物,是一只独一无二、特立独行、热爱自由的(&gt;^ω^&lt...

XboxYan41阅读 2.8k评论 14

封面图
还在用 JS 做节流吗?CSS 也可以防止按钮重复点击
举个例子:一个保存按钮,为了避免重复提交或者服务器考虑,往往需要对点击行为做一定的限制,比如只允许每300ms提交一次,这时候我想大部分同学都会到网上直接拷贝一段throttle函数,或者直接引用lodash工具库

XboxYan34阅读 2.2k评论 2

封面图

你好,我是若川。写有 《学习源码整体架构系列》 20余篇。

7k 声望
3.2k 粉丝
宣传栏