当前位置: 首页 > 科技观察

从12.9K的前端开源项目中,我学到了什么?

时间:2023-03-13 13:02:50 科技观察

本文的重点将放在插件的架构设计上,但在分析BetterScroll2.0的插件架构之前,我们先简单了解一下BetterScroll。一、BetterScroll简介BetterScroll是一款专注于解决移动端(PC端已经支持)各种滚动场景需求的插件。其核心是iscroll的实现,供参考。其API设计基本兼容iscroll。在iscroll的基础上,扩展了一些特性,做了一些性能优化。BetterScroll1.0已经发布了30多个版本,npm月下载量50,000,累计star12,600+。那么为什么要升级到2.0呢?制作v2版本的初衷源于社区的一个诉求:BetterScroll能否支持按需加载?来源:BetterScroll2.0发布:精益求精,与你同行为了支持插件的按需加载,BetterScroll2.0采用了插件式架构设计。CoreScroll作为最小的滚动单元,对外暴露了丰富的事件和hooks,其余的功能则通过不同的插件进行扩展,这会让BetterScroll使用起来更加灵活,适应不同的场景。下面是BetterScroll2.0的整体架构图:项目采用monorepos的组织方式,使用lerna进行多包管理,每个组件都是一个独立的npm包:BetterScroll2.0和西瓜播放器一样,也是插件式设计idea,CoreScroll是最小的滚动单元,其余功能通过插件扩展。比如长列表中常见的上拉加载和下拉刷新功能,在BetterScroll2.0中,这些功能分别通过pull-up和pull-down这两个插件来实现。插件的好处之一就是可以支持按需加载。此外,将独立的功能拆分成独立的插件,将使核心系统更加稳定和健壮。好了,下面简单介绍一下BetterScroll。下面言归正传,分析一下这个项目中一些值得学习的东西。2.开发体验2.1更好的智能提示BetterScroll2.0使用TypeScript进行开发。为了让开发者在使用BetterScroll时有更好的智能提示,BetterScroll团队充分利用了TypeScript接口的自动合并功能,让开发者在使用插件时,可以有相应的Options提示和bs(BetterScrollinstance)可以有相应的方法提示。2.1.1智能插件选项提示2.1.2智能BetterScroll实例方法提示接下来为了后面更好的理解BetterScroll的设计思路,简单介绍一下插件架构。3.插件架构简介3.1插件架构的概念插件架构是一种功能拆分的可扩展架构,通常用于实现基于产品的应用程序。可插入架构模式允许您将其他应用程序功能作为插件添加到核心应用程序,提供可伸缩性以及功能分离和隔离。插件架构模式包括两类架构组件:核心系统(CoreSystem)和插件模块(Plug-inmodules)。应用逻辑拆分为独立的插件模块和核心系统,具有可扩展性、灵活性、功能隔离和自定义处理逻辑等特点。图中核心系统的功能比较稳定,不会因为业务功能的扩展而不断修改,而插件模块可以根据实际业务功能的需要不断调整或扩展。插件架构的本质是将可能需要改动的部分封装在插件中,从而在不影响整体系统稳定性的情况下,达到快速灵活扩展的目的。可插拔架构的核心系统通常提供系统运行所需的最少功能集。插件模块是包含特定处理、附加功能和自定义代码的独立模块,用于增强或扩展核心系统的附加业务功能。通常插件模块也是独立的,有些插件依赖于其他几个插件。尽量减少插件之间的通信以避免依赖性问题很重要。3.2插件式架构的优点高灵活性:整体的灵活性是对环境变化的快速响应能力。由于插件之间的耦合度较低,更改通常会被隔离并快速实施。可测试性:插件可以独立测试并轻松模拟以演示或原型化新功能,而无需修改核心系统。高性能:虽然插件化架构本身不会让应用程序变得高性能,但是使用插件化架构构建的应用程序的性能通常不会差,因为可以定制或删减不需要的功能。介绍完插件架构相关的基础知识,下面我们来分析一下BetterScroll2.0是如何设计插件架构的。4.BetterScroll插件架构实现插件核心系统设计涉及三个关键点:插件管理、插件连接和插件通信。下面我们将围绕这三个关键点逐步分析BetterScroll2.0是如何实现插件架构的。4.1插件管理为了对内置插件进行统一管理,也方便开发者根据业务需要开发符合规范的自定义插件。BetterScroll2.0规定了统一的插件开发规范。BetterScroll2.0的插件需要是一个具有以下特点的类:1.静态的pluginName属性;2.实现PluginAPI接口(当且仅当插件方法需要代理到bs);3.构造函数的第一个参数是BetterScrollInstancebs,可以通过bs事件或者hooks注入自己的逻辑。为了直观的理解上述开发规范,我们以内置的PullUp插件为例,看看它是如何实现上述规范的。PullUp插件扩展了BetterScroll的上拉加载能力。顾名思义,静态pluginName属性代表插件的名称,PluginAPI接口代表插件实例提供的API接口。通过PluginAPI接口可以看出它支持4种方法:finishPullUp():void:结束上拉加载行为;openPullUp(config?:PullUpLoadOptions):void:动态开启上拉功能;closePullUp():void:关闭上拉加载功能;autoPullUpLoad():void:自动执行上拉加载。插件通过构造函数注入BetterScroll实例bs,然后我们可以通过bs事件或者hooks注入自己的逻辑。那么为什么要注入bs实例呢?如何使用bs实例?这里先把这些问题放在心上,后面再分析。4.2插件连接核心系统需要知道当前有哪些插件可用,如何加载这些插件,何时加载这些插件。一个常见的实现是插件注册机制。核心系统提供插件注册(可以是配置文件、代码或数据库)。插件注册中心包含了每个插件模块的信息,包括名称、位置、加载时机(启动时加载,或者按需加载)等。这里我们以前面提到的PullUp插件为例,看看如何注册并使用插件。首先,您需要使用以下命令安装PullUp插件:$npminstall@better-scroll/pull-up--save成功安装pullup插件后,您需要使用BScroll.use方法注册插件:importBScrollfrom'@better-scroll/core'importPullupfrom'@better-scroll/pull-up'BScroll.use(Pullup)然后在实例化BetterScroll时需要传入PullUp插件的配置项。newBScroll('.bs-wrapper',{pullUpLoad:true})现在我们知道可以通过BScroll.use方法注册插件,那么这个方法的内部处理是怎样的呢?要回答这个问题,我们先看一下对应的源码://better-scroll/packages/core/src/BScroll.tsexportconstBScroll=(createBScrollasunknown)asBScrollFactorycreateBScroll.use=BScrollConstructor.use在BScroll.ts文件中,BScroll.use方法指向BScrollConstructor.use静态方法,实现如下:=ctor.pluginNameconstinstalled=BScrollConstructor.plugins.some((plugin)=>ctor===plugin.ctor)//省略部分代码if(installed)returnBScrollConstructorBScrollConstructor.pluginsMap[name]=trueBScrollConstructor.plugins.push({name,applyOrder:ctor.applyOrder,ctor,})returnBScrollConstructor}}观察上面的代码,我们可以看到use方法接收一个参数,类型为PluginCtor,用于描述插件的特性构造函数。PluginCtor类型的具体声明如下:interfacePluginCtor{pluginName:stringapplyOrder?:ApplyOrdernew(scroll:BScroll):any}当我们调用BScroll.use(Pullup)方法时,我们会先获取当前插件的名称,然后判断当前插件是否已经安装。如果已经安装,则直接返回BScrollConstructor对象,否则注册插件。即分别将当前插件的信息保存到pluginsMap({})和plugins([])对象中:调用use静态方法后,会返回BScrollConstructor对象,支持链式调用:BScroll.use(鼠标滚轮)。use(ObserveDom).use(PullDownRefresh).use(PullUpLoad)现在我们知道BScroll.use方法是如何注册插件的了。注册插件只是第一步。要使用注册的插件,我们还需要在实例化BetterScroll的时候传入插件的配置项,初始化插件。对于PullUp插件,我们通过以下方式初始化插件。newBScroll('.bs-wrapper',{pullUpLoad:true})所以要了解插件是如何连接到核心系统并初始化插件的,我们需要分析BScroll的构造函数://packages/core/源代码/BScroll。tsexportconstBScroll=(createBScrollasunknown)asBScrollFactoryexportfunctioncreateBScroll(el:ElementParam,options?:Options&O):BScrollConstructor&UnionToIntersection>{constbs=newBScrollConstructor(el,选项)返回(bsasunknown)asBScrollConstructor&UnionToIntersection>}在createBScroll工厂方法中,会通过new关键字调用BScrollConstructor构造函数,创建BetterScroll实例。所以接下来重点分析BScrollConstructor构造函数://packages/core/src/BScroll.tsexportclassBScrollConstructorextendsEventEmitter{constructor(el:ElementParam,options?:Options&O){constwrapper=getElement(el)//省略部分代码this.plugins={}this.hooks=newEventEmitter([...])this.init(wrapper)}privateinit(wrapper:MountedBScrollHTMLElement){this.wrapper=wrapper//省略部分代码this.applyPlugins()}}通过阅读BScrollConstructor的源码,我们发现在BScrollConstructor的构造函数内部会调用init方法进行初始化,在init方法内部还会进一步调用applyPlugins方法来应用注册的插件://packages/核心/src/BScroll.tsexportclassBScrollConstructorextendsEventEmitter{privateapplyPlugins(){constoptions=this.optionsBScrollConstructor.plugins.sort((a,b)=>{constapplyOrderMap={[ApplyOrder.Pre]:-1,[ApplyOrder.Post]:1,}constaOrder=a.applyOrder?applyOrderMap[a.applyOrder]:0constbOrder=b.applyOrder?applyOrderMap[b.applyOrder]:0returnaOrder-bOrder}).forEach((item:PluginItem)=>{constctor=item.ctor//当指定插件启用且插件构造函数的类型为函数时,然后创建相应的插件if(options[item.name]&&typeofctor==='function'){this.plugins[item.name]=newctor(this)}})}}会在applyPlugins方法里面按照插件设置的顺序排序,然后使用bs实例作为参数调用插件的构造函数创建插件,并将插件实例保存到bs实例内部的plugins({})属性中。我们在这里介绍了插件管理和插件连接。下面介绍最后一个重点——插件通信。4.3插件通信插件通信是指插件之间的通信。虽然插件在设计时是完全解耦的,但是在实际的业务运行过程中,难免会有某个业务流程需要多个插件进行协同,这就需要两个插件之间进行通信;由于插件之间没有直接联系,通信必须通过核心系统,所以核心系统需要提供插件通信机制。这种情况类似于计算机。电脑的CPU、硬盘、内存、网卡都是独立设计的配置。但是,计算机在运行过程中,CPU与内存、内存与硬盘必须进行通信。计算机通过主板上的总线提供这些。组件之间的通信功能。同样,对于插件架构的系统,核心系统通常会以事件总线的形式提供插件通信机制。说到事件总线,可能有些小伙伴有点陌生。但是如果采用发布-订阅的模式应该很容易理解。阿宝哥这里不打算介绍发布-订阅模型,只是用一张图来回顾一下模型。对于BetterScroll来说,它的核心是BScrollConstructor类,它继承了EventEmitter事件派发器://packages/core/src/BScroll.tsexportclassBScrollConstructorextendsEventEmitter{constructor(el:ElementParam,options?:Options&O){this.hooks=newEventEmitter(['refresh','enable','disable','destroy','beforeInitialScrollTo','contentChanged',])this.init(wrapper)}}EventEmitter类是BetterScroll内部提供的,其instance会对外提供eventbus的功能,这个类对应的UML类图如下:说到这里,我们可以回答之前留下的第一个问题:“那为什么要注入bs实例?”。因为bs(BScrollConstructor)实例的本质也是一个事件派发器,在创建插件时注入bs实例,让插件通过统一的事件派发器进行通信。我们已经知道第一个问题的答案了,那么让我们看看第二个问题:“如何使用bs实例?”。为了回答这个问题,我们继续以PullUp插件为例,看看插件是如何使用bs实例进行消息通信的。exportdefaultclassPullUpimplementsPluginAPI{staticpluginName='pullUpLoad'constructor(publicscroll:BScroll){this.init()}}在PullUp构造函数中,bs实例会保存在PullUp实例内部的scroll属性中,然后在PullUp内部注入即可用于事件通信的插件bs实例。例如派发插件内部事件,在PullUp插件中,当滚动到底部的距离小于阈值时,触发pullingUp事件:privatecheckPul??lUp(pos:{x:number;y:number}){const{threshold}=this.optionsif(...){this.pulling=true//省略部分代码this.scroll.trigger(PULL_UP_HOOKS_NAME)//'pullingUp'}}知道后如何使用bs实例来派发事件,我们来看看如何在插件内部使用来监听插件感兴趣的事件//packages/pull-up/src/index.tsexportdefaultclassPullUpimplementsPluginAPI{staticpluginName='pullUpLoad'构造函数(publicscroll:BScroll){this.init()}privateinit(){this.handleBScroll()this.handleOptions(this.scroll.options.pullUpLoad)this.handleHooks()this.watch()}}会调用PullUp构造函数中的init方法对插件进行初始化,init方法内部会调用不同的方法进行不同的初始化操作,这里和事件相关的是handleHooks方法,实现如下:privatehandleHooks(){this.hooksFn=[]//省略部分代码eventTypes.contentChanged,()=>{this.finishPullUp()})}显然在handleHooks方法内部,会进一步调用registerHooks方法注册hook:privateregisterHooks(hooks:EventEmitter,name:string,handler:Function){hooks.on(名称、处理程序、this)this.hooksFn.push([hooks,name,handler])}通过观察registerHooks方法的签名可以看出它支持3个参数,第一个参数是EventEmitter对象,另外2个参数代表事件名称和事件handler位于registerHooks方法内部,它只是通过hooks对象监听指定的事件。那么this.scroll.hooks对象是什么时候创建的呢?在BScrollConstructor构造函数中我们找到了答案。//packages/core/src/BScroll.tsexportclassBScrollConstructorextendsEventEmitter{constructor(el:ElementParam,options?:Options&O){//省略部分代码this.hooks=newEventEmitter(['refresh','enable','disable','destroy','beforeInitialScrollTo','contentChanged',])}}显然this.hooks也是一个EventEmitter对象,所以可以用来做事件处理。好了,插件通讯的内容就先介绍到这里。下面用一张图来总结一下这部分的内容:介绍完BetterScroll插件架构的实现,我们简单说一下BetterScroll项目的工程方面。五、工程化在工程化方面,BetterScroll使用了一些业界通用的解决方案:lerna:Lerna是一个管理包含多个包的JavaScript项目的管理工具。prettier:Prettier中文意思是美丽、漂亮,是一种流行的代码格式化工具。tslint:TSLint是一种可扩展的静态分析工具,用于检查TypeScript代码的可读性、可维护性和功能性错误。commitizen&cz-conventional-changelog:用于帮助我们生成符合规范的提交信息。husky:husky可以防止不规范的代码被commit、push、merge等。jest:Jest是Facebook维护的JavaScript测试框架。工作服:获取Coveralls.io的覆盖率报告并在README文件中添加一个漂亮的覆盖率按钮。vuepress:一个由Vue驱动的静态站点生成器,用于为BetterScroll2.0生成文档。因为本文的重点不在工程上,所以上面的宝哥简单罗列了BetterScroll在工程上使用的开源库。如果你也对BetterScroll项目感兴趣,可以看看项目中的package.json文件,重点关注项目中npm脚本的配置。