我的编程空间,编程开发者的网络收藏夹
学习永远不晚

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

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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!')
  }
  
  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 设计模式洋葱模型的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

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

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

JavaScript设计模式之原型模式怎么实现

本篇内容主要讲解“JavaScript设计模式之原型模式怎么实现”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“JavaScript设计模式之原型模式怎么实现”吧!前言设计模式呢最多的可能是用到类
2023-07-02

Java设计模式之代理模式原理及实现代码分享

简介Java编程的目标是实现现实不能完成的,优化现实能够完成的,是一种虚拟技术。生活中的方方面面都可以虚拟到代码中。代理模式所讲的就是现实生活中的这么一个概念:中介。代理模式的定义:给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
2023-05-30

Android 适配器模式应用及设计原理

适配器模式是一种重要的设计模式,在 Android 中得到了广泛的应用。适配器类似于现实世界里面的插头,通过适配器,我们可以将分属于不同类的两种不同类型的数据整合起来,而不必去根据某一需要增加或者修改类里面的方法。 适配器又分为单向适配器和
2022-06-06

PHP单元素设计模式的原理及应用是怎样的

本篇文章给大家分享的是有关PHP单元素设计模式的原理及应用是怎样的,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。PHP语言的功能非常强大,英语覆盖面也很广泛,许多程序员都开始使
2023-06-17

“PHP 面向对象编程设计模式:理解 SOLID 原则及其应用”

SOLID 原则是面向对象编程设计模式中的一组指导原则,旨在提高软件设计的质量和可维护性。这些原则包括单一职责原则、开放-封闭原则、里氏替换原则、接口隔离原则和依赖倒置原则。
“PHP 面向对象编程设计模式:理解 SOLID 原则及其应用”
2024-02-25

编程热搜

目录