JavaScript 设计模式之洋葱模型原理及实践应用

目录
  • 前言
  • 洋葱模型
  • 实践
  • 总结

前言

先来听听一个故事吧,今天产品提了一个业务需求:用户在一个编辑页面,此时用户点击退出登录,应用需要提示用户当前有编辑内容未保存,是否保存;当用户操作完毕后再提示用户是否退出登录。

流程如下:

因为退出登录是属于公共部分由另一位同学维护,此时和他交流后“善良”的把需求仍给了他。并告知他可以通过某某方法获取我当前是否有编辑内容。然后我继续摸鱼,他开始疯狂输出

const handlerLogout = async () => {
    if (window.location.href === 'xxx') {
        if (getEditState() === 'xxx') {
            await editConfirm()
        }
    }
    await logoutConfirm();
}

功能如约上线,新需求也如约到达:产品期望用户在VIP充值页面退出登录的时候,先弹出一个VIP充值广告,当用户关闭广告后再提示用户是否退出登录。

流程如下:

然后熟悉的场景、熟悉的人,在一番交流过后,那位同学略微暴躁的又开始疯狂输出,然后我继续摸鱼

const pages = {
    editPage: async () => {
        if (getEditState() === 'xxx') {
            await editConfirm()
        }
    },
    vipPage: async () => {
        if (getUserVipState() === 'xxx') {
            await vipConfirm()
        }
    }
}
const handlerLogout = async () => {
    const curPage = getPage();
    await pages[curPage];
    await logoutConfirm();
}

然后的然后功能又如约上线,然后需求又来了,一个场景中有多个弹窗业务,优先级不同,如果弹窗1不满足弹出条件,就使用弹窗2依此类推。众所周知产品的需求怎么做的完,他终于受不了了,开始思考怎么样自己才能摸摸鱼。与似乎邪恶的想法油然而生,如果自己维护的退出登录就只关注处理退出登录的业务,而其他业务的各种弹窗让业务方自己去处理那我就可以摸鱼啦。想法有了,拆解一下逻辑,底层逻辑就是在触发时需要有很多中间层的处理,等中间层处理完成后再处理自己的。那这不就像是洋葱模型吗。

洋葱模型

提到洋葱模型,koa的实现简单且优雅。koa中主要使用koa-compose来实现该模式。核心内容只有十几行,但是却涉及到高阶函数、闭包、递归、尾调用优化等知识,不得不说非常惊艳没有一行是多余的。简单来说,koa-compose暴露出一个compose方法,该方法接受一个中间件数组,并返回一个Promise函数。源码如下

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) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

源码中compose主要做了三件事

  • 第一步:进行入参校验
  • 第二步:返回一个函数,并利用闭包保存middleware和index的值
  • 第三步:调用时,执行dispatch(0),默认从第一个中间件执行

dispatch函数的作用(dispatch其实就是next函数)

  • 第一步:通过i <= index来避免在同一个中间件中连续next调用
  • 第二步:设置index的值为当前中间件位置的值,并且拿到当前中间件函数
  • 第三步:判断当前是否还有中间件,没有返回Promise.resolve()
  • 第四步:返回Promise.resolve并把当前中间件执行结果做为返回,且传入context和next(dispatch)方法。这里利用尾调优化,避免了fn重新创建新的栈帧,同时提升了速度和节省了内存(大佬就是大佬)

我们可以通过其测试用例了解到执行的过程,有条件的读者可以通过下载源码进行断点调试,更能理解每一步的过程

  it('should work', async () => {
    const arr = []
    const stack = []
    stack.push(async (context, next) => {
      arr.push(1) // 步骤1
      await wait(1) // 步骤2
      await next() //  步骤3
      await wait(1) // 步骤14
      arr.push(6) // 步骤15
    })
    stack.push(async (context, next) => {
      arr.push(2) // 步骤4
      await wait(1) // 步骤5
      await next() // 步骤6
      await wait(1) // 步骤12
      arr.push(5) // 步骤13
    })
    stack.push(async (context, next) => {
      arr.push(3) // 步骤7
      await wait(1) // 步骤8
      await next() // 步骤9
      await wait(1) // 步骤10
      arr.push(4) // 步骤11
    })
    await compose(stack)({})
    expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6]))
  })

compose接收一个参数,该参数是一个Promise数组,注入中间件后返回了一个执行函数并执行。此时会按照上诉我标记的步骤进行执行。配置koa文档中的gif示例和流程图更好理解。通过不断的递归加上Promise链式调用完成了整个中间件的执行

实践

已经了解到洋葱模型的设计,按照当前摸鱼的诉求,期望stack.push这部分内容由业务方自己去注入,而退出登录只需要执行compose(stack)({})即可,额外诉求是项目中期望对弹窗有优先级的处理,那就是不是谁先进入谁先执行。对此改造一下middleware定义,新增level表示优先级后续它进行排序,优先级越高设置level值越高即可。

type Middleware<T = unknown> = {
  level: number;
  middleware: (context: T | undefined, next: () => Promise<any>) => void;
};

因为我们需要提供给业务方一个接口来添加中间件,这里使用类来实现,通过暴露出add和remove方法对中间件进行添加和删除,利用add方法在添加时利用level对中间件进行排序,使用stack来保存已经排序好的中间件。dispatch通过CV大法实现

class Scheduler<T> {
  stack: Middleware<T>[] = [];
  add(middleware: Middleware<T>) {
    const index = this.stack.findIndex((it) => it.level <= middleware.level);
    this.stack.splice(index === -1 ? this.stack.length : index, 0, middleware);
    return () => {
      this.remove(middleware);
    };
  }
  remove(middleware: Middleware<T>) {
    const index = this.stack.findIndex((it) => it === middleware);
    index > -1 && this.stack.splice(index, 1);
  }
  dispatch(context?: T) {
    // eslint-disable-next-line
    const that = this;
    let index = -1;
    return mutate(0);
    function mutate(i: number): Promise<void> {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'));
      index = i;
      const fn = that.stack[i];
      if (index === that.stack.length) return Promise.resolve();
      try {
        return Promise.resolve(fn.middleware(context, mutate.bind(null, i + 1)));
      } catch (error) {
        return Promise.reject(error);
      }
    }
  }
}
export default Scheduler;

然后修改业务中的处理,之后再加类似需求就可以摸鱼了。

// 暴露一个logoutScheduler方法
export const logoutScheduler = new Scheduler();
const handleLogout = () => {
    logoutScheduler.dispatch().then(() => {
        logoutConfirm();
    })
}
// 编辑页面
logoutScheduler.add({
    level: 2,
    middleware: async (_, next) => {
        if (getEditState() === 'xxx') {
          await editConfirm()
        }
        await next();
    }
})
// vip页面
logoutScheduler.add({
    level: 2,
    middleware: async (_, next) => {
        if (getUserVipState() === 'xxx') {
            await vipConfirm()
        }
        await next();
    }
})

总结

一个好的设计能在实际开发中更好的去解耦业务,而好的设计需要我们去阅读那些优秀的源码去学习和理解才能为我们所用。

以上就是JavaScript 设计模式之洋葱模型原理及实践应用的详细内容,更多关于JavaScript 设计模式洋葱模型的资料请关注我们其它相关文章!

(0)

相关推荐

  • JS前端中的设计模式和使用场景示例详解

    目录 引言 策略模式 1.绩效考核 2.表单验证 策略模式的优缺点: 代理模式 1.图片懒加载: 2.缓存代理 总结 引言 相信大家在日常学习和工作中都多多少少听说/了解/使用过 设计模式,我们都知道,使用恰当的设计模式可以优化我们的代码,那你是否知道对于前端开发哪些 设计模式 是日常工作经常用到或者必须掌握的呢?本文我将带大家一起学习下前端常见的设计模式以及它们的 使用场景!!! 本文主讲: 策略模式 代理模式 适合人群: 前端人员 设计模式小白/想知道如何在项目中使用设计模式 策略模式 策略

  • JavaScript设计模式之命令模式和状态模式详解

    目录 命令模式 命令模式介绍 代码实现 状态模式 状态模式介绍 代码实现 小结 命令模式 命令模式介绍 命令模式(Command)的定义是:用于将一个请求封装成一个对象,从而使你可用不同的请求对客户进行参数化:对请求排队或者记录请求日志,以及执行可撤销的操作. 也就是说改模式旨在将函数的调用.请求和操作封装成一个单一的对象,然后对这个对象进行一系列的处理.此外,可以通过调用实现具体函数的对象来解耦命令对象与接收对象. 代码实现 <!DOCTYPE html> <html lang=&qu

  • JS前端设计模式之发布订阅模式详解

    目录 引言 例子1: version1: version2: 总结 引言 昨天我发布了一篇关于策略模式和代理模式的文章,收到的反响还不错,于是今天我们继续来学习前端中常用的设计模式之一:发布-订阅模式. 说到发布订阅模式大家应该都不陌生,它在我们的日常学习和工作中出现的频率简直不要太高,常见的有EventBus.框架里的组件间通信.鉴权业务等等......话不多说,让我们一起进入今天的学习把!!! 发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系 当一个对象的状态发生改变时,所有

  • JavaScript设计模式之原型模式和适配器模式示例详解

    目录 原型模式 原型模式介绍 代码实现 适配器模式 适配器模式介绍 代码实现 小结 原型模式 原型模式介绍 原型模式是指原型实例指向创建对象的种类,并通过拷贝这些原型创建新的对象,是一种用来创建对象的模式,也就是创建一个对象作为另一个对象的prototype属性 实现原型模式是在ECMAScript5中,提出的Object.create方法,使用现有的对象来提供新创建的对象的__proto__. 代码实现 var lynkCoPrototype = { model: "领克", get

  • 解析Javascript设计模式Revealing Module 揭示模式单例模式

    目录 1. Revealing Module 揭示模式 2. Singleton 单例模式 1. Revealing Module 揭示模式 该模式能够在私有范围内简单定义所有的函数和变量,并返回一个匿名对象, 它拥有指向私有函数的指针,该函数是他希望展示为公有的方法. 示例: <script> var myRevealingModule = function () { var privateVar = "Ren Cherry", publicVar = "Hey

  • JS继承与工厂构造及原型设计模式详解

    目录 序言 正文 小结 序言 我们在前一篇文章<JS精粹,原型链继承和构造函数继承的 “毛病”> ,提到了:原型链继承.构造函数继承.组合继承: 在另一篇文章<蓦然回首,“工厂.构造.原型”设计模式,正在灯火阑珊处>,提到了:我们用于创建对象的三种设计模式:工厂设计模式.构造设计模式.原型设计模式: 至此,我们可以明显的感受到:JS 要实现面向对象(继承的能力),离不开这 3 种设计模式: 原型链 + 构造函数 = 组合继承 本篇带来一个新的继承方式:寄生继承,它由工厂模式和构造函

  • JavaScript 设计模式之洋葱模型原理及实践应用

    目录 前言 洋葱模型 实践 总结 前言 先来听听一个故事吧,今天产品提了一个业务需求:用户在一个编辑页面,此时用户点击退出登录,应用需要提示用户当前有编辑内容未保存,是否保存:当用户操作完毕后再提示用户是否退出登录. 流程如下: 因为退出登录是属于公共部分由另一位同学维护,此时和他交流后“善良”的把需求仍给了他.并告知他可以通过某某方法获取我当前是否有编辑内容.然后我继续摸鱼,他开始疯狂输出 const handlerLogout = async () => { if (window.locat

  • JavaScript设计模式之模板方法模式原理与用法示例

    本文实例讲述了JavaScript设计模式之模板方法模式原理与用法.分享给大家供大家参考,具体如下: 一.模板方法模式:一种只需使用继承就可以实现的非常简单的模式. 二.模板方法模式由两部分组成,第一部分是抽象父类,第二部分是具体的实现子类. 三.以设计模式中的Coffee or Tea来说明模板方法模式: 1.模板Brverage,代码如下: var Beverage = function(){}; Beverage.prototype.boilWater = function(){ cons

  • JavaScript设计模式之门面模式原理与实现方法分析

    本文实例讲述了JavaScript设计模式之门面模式原理与实现方法.分享给大家供大家参考,具体如下: 外部与一个子系统的通信必须通过一个系统的一个门面对象进行,这就是门面模式. 门面模式具备如下两个角色: 1. 门面角色 客户端可以调用这个角色方法,此角色中有子系统的应用(知晓相关的(一个或多个)子系统的功能和责任).本角色会将所有从客户端发来的请求委派到相应的子系统去. 2. 子系统角色 可以同时有一个或多个子系统.每一个子系统都不是一个单独的类,而是一些类的集合.每一个子系统都可以被客户端直

  • javascript 设计模式之组合模式原理与应用详解

    本文实例讲述了javascript 设计模式之组合模式原理与应用.分享给大家供大家参考,具体如下: 组合模式说明 组合模式用于简单化,一致化对单组件和复合组件的使用:其实它就是一棵树: 这棵树有且只有一个根,访问入口,如果它不是一棵空树,那么由一个或几个树枝节点以及子叶节点组成,每个树枝节点还包含自己的子树枝以及子叶节点: 在面向对象编程中,叶子以及复杂对象(树枝节点)都继承一个接口或抽象类分别实现: 这个抽象定义一般三个部分组成,组件的基本信息,Add方法,Remove方法: 叶子节点只包含本

  • javascript设计模式 – 抽象工厂模式原理与应用实例分析

    本文实例讲述了javascript设计模式 – 抽象工厂模式原理与应用.分享给大家供大家参考,具体如下: 介绍:基于工厂模式,继续升级.来解决工厂模式存在多个工厂类的问题.主要的思想是将一些相关的产品组成一个产品族,由同一个工厂来统一生产. 定义:抽象工厂模式提供一个创建一系列相关或相互依赖的接口,而无须指定他们具体的类.抽象工厂模式又称kit模式,它是一种对象创建型模式. 场景:还是上面的Dialog类,如果继续向后发展,会有各种各样的弹窗,如果新增一个弹窗包含了notice和toast.这样

  • javascript设计模式 – 职责链模式原理与用法实例分析

    本文实例讲述了javascript设计模式 – 职责链模式原理与用法.分享给大家供大家参考,具体如下: 介绍:很多情况下,在一个软件系统中可以处理某个请求的对象不止一个.例如一个网络请求过来,需要有对象去解析request Body,需要有对象去解析请求头,还需要有对象去对执行对应controller.请求一层层传递,让每一个对象都基于请求完成自己的任务,然后将请求传递给下一个处理程序.是不是感觉有点中间件的感觉. 定义:职责链就是避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求.将

  • javascript设计模式 – 享元模式原理与用法实例分析

    本文实例讲述了javascript设计模式 – 享元模式原理与用法.分享给大家供大家参考,具体如下: 介绍:在我们日常开发中需要创建很多对象,虽然垃圾回收机制能帮我们进行回收,但是在一些需要重复创建对象的场景下,就需要有一种机制来进行优化,提高系统资源的利用率. 享元模式就是解决这类问题,主要目的是减少创建对象的数量.享元模式提倡重用现有同类对象,如未找到匹配的对象则创建新对象 定义:运用共享技术有效的支持大量细粒度对象的复用.系统只适用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象

  • javascript设计模式 – 中介者模式原理与用法实例分析

    本文实例讲述了javascript设计模式 – 中介者模式原理与用法.分享给大家供大家参考,具体如下: 介绍:在前端开发的过程中,组件与组件之间的通讯特别常见,一个组件的change需要引起数个组件的change,这就需要组件与组件之间存在复杂的多对多关系链.如何来减轻维护这些关系的复杂度,让组件和组件之间实现低耦合?这就是我们即将介绍的中介者模式. 定义:用一个中介对象(中介者)来封装一系列的对象交互,中介者使个对象不需要显式的相互引用,从而使其耦合松散,而且可以独立的改变他们之间的交互.中介

  • javascript设计模式 – 简单工厂模式原理与应用实例分析

    本文实例讲述了javascript设计模式 – 简单工厂模式.分享给大家供大家参考,具体如下: 介绍:简单工厂模式是最常用的一类创建型设计模式.其中简单工厂模式并不属于GoF23个经典设计模式,它通常被作为学习其他工厂模式的基础. 定义:定义一个工厂类,它可以根据参数的不同返回不同的实例,被创建的实例通常都具有相同的父类,因为在简单工厂模式中创建实例的方法是静态方法,因此简单工厂模式又被称为静态工厂方法模式,它属于类创建型模式. 场景:我们需要写一个dialog工具类,在项目初期我们只需要考虑一

  • JavaScript设计模式之观察者模式(发布订阅模式)原理与实现方法示例

    本文实例讲述了JavaScript设计模式之观察者模式(发布订阅模式)原理与实现方法.分享给大家供大家参考,具体如下: 观察者模式,又称为发布订阅模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己的状态. 在观察者模式中,并不是一个对象调用另一个对象的方法,而是一个对象订阅另一个对象的特定活动并在状态改变后获得通知.订阅者也称为观察者,而被观察的对象称为发布者或主题.当发生了一个重要的事件时,发布

随机推荐