我把Koa 源码核心思想应用到了公司项目

共 6082字,需浏览 13分钟

 ·

2023-08-25 12:15

本文你可以学到

  1. 理解koa2 洋葱模型核心源码 compose 函数实现
  2. 理解函数式编程衍生范式——面向切面编程
  3. 除了看源码,实战把洋葱模型思想应用到 SDK 项目中

Koa2 源码实现

0abf2559a51776615a64c276949ebdea.webp

我想很多小伙伴应该都知 Koa 有一个洋葱模型的概念,

通过它控制中间件内部内容的执行顺序。先来复习一下中间件结构。

      function logger(ctx,next){}

类似这种的每个函数都是一个中间件。然后这些中间件会被存放到一个 middlewares 中间件数组中。

然后依赖的核心库是 koa-compose,去完成整个 middlewares 数组中函数的执行,他不是普通的遍历依次执行过程,里面有一些特殊实现,重点关注下 源码中 dispatch 函数实现。

源码如下,对核心代码部分进行了注释讲解

      // compose 函数参数是前面提到的 middlewares 中间件数组
function compose(middleware{
  // 参数校验:判断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!");
  }

  // 返回一个方法,这个方法就是compose的结果
  // 外部可以通过调用这个方法来开中间件数组的遍历
  // 参数形式和普通中间件一样,都是context和next
  return function (context, next{
    return dispatch(0); // 开始中间件执行,从数组第一个开始

    // 中间件执行函数
    function dispatch(i{
      let fn = middleware[i]; // 取出需要执行的中间件

      // 如果i等于数组长度,说明数组已经执行完了
      if (i === middleware.length) {
        fn = next; // fn等于外部传进来的next,结束执行
      }
            // 如果外部没有传结束执行的next,直接就resolve
      if (!fn) {
        return Promise.resolve();
      }

      // 执行中间件,注意传给中间件接收的参数应该是context和next
      // 传给下一个中间件的next是函数,一定注意这里是使用的bind dispatch.bind(null, i + 1)
      // 所以中间件里面调用 next 的时候其实调用的是dispatch(i + 1),也就是执行下一个中间件
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

对这段代码的实现进行了详细的注释,再次强调一下代码的核心部分

      return Promise.resolve(fn(context, dispatch.bind(null,i+1)))

fn 执行的第二个参数实际是中间件数组中的函数引用(使用了 bind 函数),在中间件内部调用 next 实际调用的是 dispatch(i+1),也就是下一个中间件。

洋葱模型思想在项目中的应用

需求描述

我们要提供一个 Node.jsSDK,在这个 SDK 中我们提供了一系列功能,本文要讲的是其中一个小部分:请求函数中聚合中间件实现,SDK 使用者发起一个请求会调用 SDKrequest 请求函数,这个函数我们应该怎么封装呢?

SDK.request 调用理论上会执行下面的一系列中间件函数。

51eb7c6932f0870127b21fbc94f181f1.webp

如图所示,包括的功能有 危险字符过滤日志记录响应处理等等(这里就不一一列举了),它的实现正需要一个洋葱模型的机制,上分支是洋葱进入时前期处理的函数,然后交给并等待其他中间件处理面,下分支是扒开洋葱后期处理的过程,

理论科普:洋葱模型也叫面向切面编程。AOP 为 Aspect Oriented Programming 的缩写,中文意思为:面向切面编程,它是函数式编程的一种衍生范式。面向切面编程的是在现有程序中,加入或减去一些功能(函数中间件)不影响原有的代码功能。比如我们的 request 需求中去除 log 记录中间件。

分析与代码实现

  1. 首先我们定义一个存放中间件的 moduleList
  2. 定义compose函数
  3. 执行 dispatch(0) 以及在 dispatch 函数内部调用 fn.call(null,context,dispatch.bind(null,i+1))
      const moduleList = [
    require('./dangerQuery'),
    require('./log'),
    ...// 省略一部分
    require('./resHandler')
];
export const compliations = (ctx:Context,next:Next)=>{
    const context = {ctx,next};
    cosnt composeFn = compose(moduleList);
    composeFn(context);
}

function compose = (list:Array<Function>)=>{
    if(!Array.isArray(list)){
         throw new TypeError("ModuleList must be an array!");
    }
    for(const fn of list){
        if(typeof fn !== 'function'){
              throw new TypeError("ModuleList must be composed of functions!");
        }
    }
    return function(context:{ctx:Context,next:Next}){
        const dispatch = async (i:number)=>{
            if(list.length>i){
                const fn = list[i];
                await fn.call(null,context,dispatch.bind(null,i+1))
            }
        }
        return dispatch(0)
    }
}

中间件实现很多注意的点,本文只是想把洋葱模型部分思想理解,并应用起来,实际每个中间件内部要支持可插拔机制和开关机制的;并且 moduleList 中最后一个中间件函数,实际函数的第二个 next 参数已经为空了,不要再次执行,如果SDK是基于egg,midway等进行封装的,最后一个中间件内部应使用 await context.next()

感悟

  1. 好东西就要用起来,除了这部分,自己项目中用到洋葱模型思想的还比较多的,并且开源项目中也很多,比如WebpackRedux。可以看看他们的使用有哪些巧妙之处。
  2. 我们在看源码的过程中。不要为了看源码而看源码,最好看懂后应用起来才会真的掌握
  3. 面试过程中如果写到了 koa,洋葱模型肯定是一个必考项,如果能把原理说清楚,并举例将思想用到了自己项目中我觉得也是一个加分项。

参考文章

  1. https://juejin.cn/post/7078905984772489247
  2. https://github.com/koajs/koa
    

浏览 24
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报