最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Chimee中的设计模式之美

    正文概述 掘金(Sheeeeep)   2021-06-01   476

    涌现

    在《失控——机器社会与经济的新生物学》一书中,作者用蜂群的例子向读者们阐述了“涌现”(emergence)的概念,即一个复杂系统中由次级组成单元间简单的互动所造成的复杂现象[1]。

    在业务开发中,“涌现”的现象非常常见。产品的功能在持续的迭代不断追加,这些功能可能是不同的开发者增加的、可能是在不同的时间阶段增加的。众多看似简单或复杂的功能,纠缠在一起,逐渐进化成一个可怕的怪兽。

    播放视频是一个在很多业务领域都非常复杂的业务功能。比如视频网站:有弹幕、有广告;比如前段时间很火的、回形针出品的、基本操作:用canvas模拟视频交互等等。在线教育也是其中之一。

    一个在线教育产品通常会有多种多样课程,每种课程都会有不同的上课形式:

    • 不能暂停的、可以暂停的;
    • 不能拖动进度的、可以拖动进度的、按给定预设点跳转进度的、用快进快退的跳转进度的;
    • 支持各种互动形式:红包、摄像头、作业、调查问卷等;
    • 穿插各种各样的动画、声音效果;

    面对这种业务需求,通常开发者会使用一个基础的视频播放器组件,支持常用的播放、停止操作,抛出进度事件等。然后,将业务功能做成一个个业务组件(vue/react)。最后,在不同的视频播放场景,用不同的参数初始化播放器组件,按业务需求逐一插入对应的业务组件。这种做法会引起以下几个问题:

    1. 重复工作

    对每一种播放场景,都要一一将组件插入,然后处理播放器和业务组件、业务组件和业务组件之间的交互、处理组件层级关系、增加动画效果等等。

    2. 系统易腐化

    因为业务需求是随着产品迭代渐渐产生的,如果开发者的经验不够,很容易出现将业务功能强耦合在当前播放场景中、下一次在其它播放场景用到这个业务功能的时候,又要把功能剥离出来或者直接把代码复制过来。

    3. 强行用代码的树形结构去描述业务场景的网状结构

    组件的嵌套关系、事件传递路线都是树形的。但是在复杂的业务场景中,程序的处理流程错综复杂,并非只有一条主线。如果把整个程序的处理流程画出来的话,会是一个网状结构。用树形的组件结构、事件路线去翻译、处理网状结构,显然就会比较吃力。

    面对这些问题,Chimee提出了一套解决方案。

    Chimee是什么

    Chimee是奇舞团开源的一个视频播放器框架

    在使用方法上,它由若干个包组成。使用的时候要根据需求进行功能配置组合、二次开发,形成一个符合业务需求的播放器。

    在简单的视频播放场景,官方提供了web端和wap端、已经封装好的播放器,可以直接拿来使用。在复杂的视频播放场景,则可以根据需求的复杂程度来自定义播放器。

    对于较为简单的自定义需求,如修改播放图标,定制播放弹窗插件等,可以在官方提供的封装好的播放器上进行二次开发。

    对于复杂的自定义需求,可以在引入Chimee的本体包后,进行全方位的自定义:设定所需要的编解码器、开发并配置业务功能插件。

    这是使用Chimee的一个简单示例:

    import Chimee from 'Chimee';
    import forwardRewindPlugin from './forwardRewindPlugin'
    
    Chimee.install(forwardRewindPlugin);
    
    const player = new Chimee({
        wrapper: '#player',
       src: 'demo.mp4',
        plugins: [forwardRewindPlugin.name]
    });
    

    首先,我们引入的是Chimee的原生裸包:import Chimee from 'Chimee'; 它不包含任何基础UI:进度条、开始暂停等等。但是和常见的视频播放器一样,可以监听事件:播放、暂停等,调用相关的API:seek,load等。

    为了实现快退快进功能,我们安装了一个快进快退插件Chimee.install(forwardRewindPlugin);

    forwardRewindPlugin插件的定义如下:

    import { PluginConfig } from 'Chimee';
    import ForwardRewind from './ForwardRewind.vue';
    
    export const name = 'forward-rewind';
    
    export const plugin: PluginConfig = {
      name,
      el: `<div id="${name}"></div>`,
      data: {
        forwardStep: 20,
        rewindStep: 20,
      },
      methods: {
        forward() {
          this.$emit('seek',
            this.currentTime + this.forwardStep
          )
        },
        rewind() {
          this.$emit('seek',
            this.currentTime - this.rewind
          )
        },
      },
      create() {
        const component = new ForwardRewind();
        component.$on('forward', () => {
          this.forward();
        });
    
        component.$on('rewind', () => {
          this.rewind();
        });
    
        component.$mount(`#${name}`);
      },
    };
    
    

    从代码里可以看到,这里定义的“插件”其实只是一个对象PluginConfig,包含了如下键值:

    1. name,插件名称,在Chimee内部的标识参数;
    2. el,插件申请的dom节点,插件初始化的时候,会创建并挂载这个根元素到页面结构上;
    3. data,语法糖,会动态绑定到插件实例上;
    4. methods,语法糖,会动态绑定到插件实例上;可以看到,在forwardrewind两个方法里会抛出一个seek事件。播放器能够监听到这个事件,执行进度跳转;
    5. create,生命周期钩子

    除了这些参数,还有其它生命周期钩子、events:监听事件的语法糖等等。

    将插件的名字在初始化播放器的时候作为参数传递,Chimee就会将插件中申请的dom节点挂载在页面上,监听插件中指定的事件,并执行插件相应的生命周期。从而实现快进快退功能。

    从以上示例可以知道,Chimee中的插件是一个拥有生命周期、支持事件机制、可以直接调用播放器API的对象。正是Chimee的插件化架构,让它可以解决我们上一节中提出的、业务中的诸多问题。

    Chimee的插件化架构

    “插件”是一个并不新鲜,但也并不简单的概念。Chimee中的插件、Chrome浏览器的插件、webpack的插件等等,看似功能各有不同,其实都是插件化架构的具体应用。

    插件化架构,也叫做微内核架构,是一种面向功能进行拆分的可扩展性架构,允许开发者将其他功能作为插件添加到核心应用程序,从而提供可扩展性以及分离和隔离功能。变化部分封装在插件里,达到快速灵活扩展的目的,而又不影响整体系统的稳定[2]。

    插件化架构的基本组成和核心功能应当包括:

    • 核心系统组件:负责和具体业务功能无关的通用功能

    插件管理:当前有哪些插件可用,如何加载这些插件,什么时候加载插件插件连接:插件如何连接到核心系统、插件实现规范插件通信:插件间的通信

    • 插件模块组件:负责实现具体的业务逻辑

    本节从插件化架构的角度解读Chimee源码。

    Chimee本体包的目录结构如下:

    .
    ├── config
    ├── const
    ├── dispatcher
    ├── helper
    ├── plugin
    ├── kernels
    └── index.ts
    

    在第一级目录从后往前看(跳过不需要关注的部分):

    • index.ts:Chimee类,也是整个Chimee本体包的导出对象;
    • dispatcher:定义了调度器类和一些其它的核心类;

    disptcher文件夹中定义了Chimee的核心功能,index.ts将dispacher中对外提供的功能组织为Chimee类并导出。

    .
    ├── binder.ts
    ├── bus.ts
    ├── dom.ts
    ├── video-wrapper.ts
    ├── kernel.ts
    ├── plugin.ts
    └── index.ts
    

    在dispatcher目录从后往前看(跳过不需要关注的部分):

    • index.ts:Dispatcher类
    • plugin.ts:插件类
    • video-wrapper.ts:实现VideoWrapper类,封装浏览器中原生的video标签,屏蔽处理video的兼容问题
    • dom.ts:实现DOM类,负责进行框架中涉及到的DOM操作;
    • bus.ts:实现(Event)Bus类,也就是俗称的事件总线,实现了一套事件的触发和监听机制
    • binder.ts:实现Binder类,封装了框架中涉及到的事件监听、事件触发操作;

    这些类实现了上文中提到的插件化系统应有的核心功能:

    1. 在Dispatcher类中
    • 模块内变量pluginConfigSet保存开发者定义的插件配置
    • 实例的成员变量plugins中,以name为key保存插件的实例
    • 在Chimee的构造函数中初始化Dispatcher,在Dispatcher的构造函数中初始化各个插件、
    • 在初始化时,各个时机中执行插件的生命周期
    1. 在Plugin类中
    • 利用Object.defineProperty,代理一些参数、API的访问到dispatcher上,实现插件和核心系统的连接
    • 读取methods、events等值,执行事件监听、this的绑定等等
    1. 在Binder和Bus类中,定义了事件总线机制,用来实现插件间的通信

    事件总线模式

    上节中提到对于“插件通信“这个问题,Chimee采用了事件总线模式实现。

    常见的设计模式可以分为三类[3]:

    • 创建型:主要解决“对象的创建”问题;
    • 结构型:主要解决“类或对象的组合或组装”问题;
    • 行为型:主要解决的就是“类或对象之间的交互”问题;

    Chimee采用的事件总线EventBus模式,是行为型设计模式:观察者模式中的一种。

    观察者模式是一个比较抽象的模式,根据不同的应用场景和需求,有完全不同的实现方式。但是大体的设计思路通常是,维护一个监听者列表,实现添加监听者、移除监听者、通知监听者功能。

    Chimee的事件总线机制,是一种支持同步和异步,阻塞和非阻塞、带生命周期的事件总线机制。在Bus类中实现。相关API包括:

    1. on/once:绑定事件
    2. off:解绑事件
    3. emit:触发异步事件
    4. emitSync:触发同步事件

    同步和异步

    emit会触发异步事件,当事件是异步的时候,监听者可以返回一个pending状态的Promise,将事件挂起。

    emitSync会触发同步事件,当时事件是同步的时候,监听者只能返回布尔值,将事件阻截或通过。

    事件的生命周期

    一个事件的触发,会分为五个阶段,也就是事件的生命周期:before、process、main、after、_。

    before阶段:触发beforeXXX事件,该事件的监听者可以通过返回Promise或者布尔值将事件挂起或取消;

    process阶段:判断事件是否需要对核心系统,也就是video进行操作,如果需要则执行。比如play事件,是需要操作video的,video.play()会在这个阶段执行;

    main阶段:执行各个监听者对XXX事件的监听回调;

    after阶段:触发afterXXX事件,该事件的监听者可以通过返回Promise或者布尔值将事件挂起或取消;

    _阶段:副作用阶段,触发_XXX事件,该事件在main阶段后必然会执行,可以用于检测事件是否被成功触发;

    Chimee事件总线机制的实现

    为了实现以上功能,Bus首先定义了核心的数据结构events,保存事件名-事件阶段-插件id-回调函数数组的树形结构。

    private events: {
        [eventName: string]: {
          [eventStage: string]: {
            [pluginId: string]: Array<(...args: any[]) =>  any>,
          },
        },
      };
    
    

    基于以上数据结构,Bus定义了方法如下:

    Chimee中的设计模式之美

    绑定事件

    绑定事件的逻辑比较简单,从on/once入口传入事件名、事件阶段和事件监听函数,通过addEvent方法传入数据结构events中。

    有两点值得注意的地方:

    1. 拆分on方法和addEvent方法。因为once中也需要调用addEvent,所以进行了拆分。
    2. 函数劫持:在once方法中,为了实现“一次触发后自动解绑”的功能,需要增加一个函数劫持的步骤:将传入的回调函数包裹在一个新的函数中,新的函数里会执行解绑操作。最后将这个新的函数作为真正的回调函数保存。
    newHandler = () => {
     handler();
     unbind();
    }
    
    

    “一次性绑定”的功能在很多框架和库的事件机制中都有提供,比如Vue中的$once 也是用函数劫持的思路实现的。

    解绑事件

    解绑事件的逻辑也比较简单,从off入口传入事件名,事件阶段,调用removeEvents把它从数据结构中删除

    触发异步事件

    这是整个事件总线机制中最复杂的部分。

    第一步:使用事件名,从events中获取事件event。

    注意events的数据结构是:事件名-事件阶段-插件id-回调函数数组的树形结构,所以这里获取到的event的数据结构是:事件阶段-插件id-回调函数数组

    第二步:执行getEventQueue筛选出event中所有before阶段的监听函数,形成数组beforeQueue

    第三步:执行beforeQueue中的监听函数。

    Chimee提供了一个runRejectableQueue方法来执行beforeQueue,会按照数组索引,从0开始,递归执行所有函数。前一个函数resolve/返回true后才会执行下一个。如果其中一个reject或者返回false,则停止执行。

    runRejectableQueue方法和Promise.all似乎有一些相似之处。但是:

    1. Promise.all中的所有Promise全部都会执行,无论中间是否有rejectPromise。只是最终返回的Promise状态为reject
    2. Promise.all中的Promise不会等到上一个resolve后才执行,而是立即执行;

    第四步:当beforeQueue执行完毕(均resolve/返回true),会执行eventProcessor,在eventProcessor中,会执行事件的process阶段。

    第五步:执行trigger

    trigger中会执行事件的main阶段、after阶段、副作用阶段。其中main阶段和after阶段的执行和before阶段一致,从event中取出mainQueue、afterQueue,用runRejectableQueue执行。

    副作用阶段会特殊一些,依次同步调用。

    触发同步事件

    同步事件的触发和异步事件的触发过程大体一致。主要区别在于所有的事件阶段队列xxxQueue,在异步触发中使用runRejectableQueue执行,但是在同步触发中,使用runStoppableQueue执行。runStoppableQueue会按照数组索引,从0开始,递归执行所有函数。前一个函数返回true后才会执行下一个。如果其中一个返回false,则停止执行。

    可以看到,两者的区别在于是否接受Promise。

    生命周期

    在上一节中,Chimee在经典的事件总线模型上增加了生命周期。Chimee的插件同样也有生命周期。

    生命周期是钩子函数的一种体现。在一个有序的步骤中的特殊位置(挂载点),插入自定义的内容。这就叫"钩子",它给了开发者在某个阶段做某些处理的机会。

    和钩子函数类似的概念的还有模板方法模式回调函数

    • 模板方法模式。在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤[3]。
    • 回调函数。回调是一种双向调用关系。A用F作为参数调用B,B反过来调用F,这种调用机制就叫作“回调”。

    钩子函数的执行,常常要用到回调、钩子函数的注入也常常在模版代码中执行。它们在框架设计、实际应用中都有着广泛的使用。比如:

    • Vue/React类组件中的生命周期,是钩子函数的一种;
    • 子类对父类的继承,就是最典型的模板方法模式;
    • JavaScript作为一门函数式编程语言,将函数作为参数传递并执行的方式是家常便饭。

    总的来说,生命周期的机制,常常在各种框架的设计中出现,可以很好的起到扩展框架功能点的作用。

    装饰器

    在Chimee的代码中,还大量的用到了装饰器。装饰器是一种经典的设计模式,属于解决“类或对象的组合或组装”问题的结构型设计模式,其出现的意图是对类的原始方法进行增强。

    以Chimee中的一段具体代码为例:

    class Bus {
     // ...
     @runnable(secondaryChecker)
     public emit(key: string): Promise<any> {
       //...
     }
     // ...
    }
    
    function secondaryChecker(key: string) {
      // 检查key是否合法,并在生产环境输出非法提示
    }
    
    

    @runnable(secondaryChecker)就是对emit方法的增强:它在emit方法执行的时候,检查传入的key值是否合法。

    JavaScript中的装饰器,目前处于建议征集的第二阶段,但是在TypeScript中已做为一项实验性特性予以支持。其具体用法不再展开介绍。

    Chimee实现了若干个装饰器,并将它们包含在了一个单独的包[toxic-decorators](https://github.com/toxic-johann/toxic-decorators "toxic-decorators")里,如:

    • runnable:传入一个可执行的函数包裹装饰的函数
    • readonly:定义一个属性为只读

    在class-style的Vue组件中,装饰器也被广泛使用,如:

    import { Vue, Component, Watch } from 'vue-property-decorator';
    
    @Component
    export default class YourComponent extends Vue {
      @Watch('child')
      onChildChanged(val: string, oldVal: string) {}
    }
    
    

    @Component装饰类,将这个类增强为组件。

    @Watch('child')装饰类方法,将这个方法增强为对child属性的监听。

    总结

    Chimee的设计中,运用了插件化架构、事件总线模型、生命周期、装饰器等多种架构方法和设计模式。

    借助合理的架构和设计模式,实际业务中许多问题都能得到解决。

    1. 重复工作

    插件化架构可以大大的减少重复工作,面对新的播放场景,将插件以数组的方式组合即可形成满足业务新需求的播放器。

    1. 系统易腐化

    在插件化的架构下,业务功能天然被隔离在一个个插件中,在顶层的框架中形成了强制的开发规范,整个视频播放系统呈现出良性增长的趋势。

    1. 强行用代码的树形结构去描述业务场景的网状结构

    将业务功能抽象成插件后,依靠事件总线模型,每个插件只和事件总线打交道。最后按照业务流程把插件组装起来。这种思考路径更适合复杂程序的开发。

    很多开发者会把业务功能的开发简单粗暴的定义为CRUD,认为其缺乏复杂度和技术挑战。事实上,在日常的业务开发中,可以去抽象可复用的功能,乃至总结领域内的业务模式,积极的将它们提炼为通用的工具、库、框架,最终就能得到系统稳定性的提升、人效的提高、技术的成长。

    参考文献

    [1] 陆睿.《CRUD工程师晋级之路》.zhihu.com

    [2] 李运华.《从0开始学架构》.极客时间

    [3] 王争.《设计模式之美》.极客时间


    起源地下载网 » Chimee中的设计模式之美

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元