当我们说插件系统的时候,我们在说什么
时间:2023-03-28 14:03:32
HTML
当我们谈论外挂系统时,我们在谈论什么?其实在我们的日常生活中有很多直观的表现。最近打算买个吸尘器。我发现现在的吸尘器越来越高端了。吸尘器可以实现拖地、除螨等多种功能。完成。从电脑的角度来看,这个吸尘器其实就是一个功能齐全的外挂系统,这些吸头就是他的外挂生态。这样做有什么好处?对于用户:使用更方便,原来需要同时购买多款产品的功能,只需购买这款吸尘器即可实现。对于厂商来说,好处更多:一方面降低了实施的复杂度,更有利于分工协作。核心部门可以专注于吸尘器的基本功能,做到吸力更大,噪音更小,增加自己的产品。竞争力,至于吸头,可以交给其他部门。另一方面也可以利用生态让其他厂商参与生产各种能力的吸头(戴森在这方面做得特别好,有很多第三方吸头与戴森相关)在互联网上),进一步扩大了自身的品牌影响力。也正是因为这么多的好处,现在汽车、无人机、吸尘器等都或多或少出现了功能性的可选配件。这些都是外挂系统在生活中的体现。回到我们的计算机世界,插件系统广泛应用于各种工具,例如:Umi、Egg、JQuery、WordPress、Babel、Webpack……当我们打开Umi的官网,我们可以看到显着位置下面这段话:umi是基于路由的,同时支持配置路由和约定路由,保证路由功能完备,扩展功能。然后配备了完整生命周期的插件体系,覆盖了从源代码到构建产品的每一个生命周期,支持各种功能扩展和业务需求。从上面这段话,我们可以看出两点:基于路由的插件系统,所以Umi其实就是一个基于路由的插件系统。它的核心功能是路由,其他功能以插件的形式补充。比如你需要使用antd相关的内容,可以导入plugin-antd。如果要使用dva,可以importplugin-dva。如果你想使用package的request方式,可以引入plugin-request...通过上面的介绍,相信你心中已经对插件系统有了一定的了解。现在让我们定义插件系统。什么是插件系统说到插件系统,我们先来解释一下插件的定义。在网上找了很多资料,每个人的看法都不一样。大部分都是在应用的维度上进行说明。据维基百科(wikipedia))解释:在计算机技术中,插件是一种软件组件,它为现有的计算机程序添加特定的功能。当一个程序支持插件时,它就支持定制。插件必须依赖应用程序自身才能发挥作用,单独靠插件是无法正常运行的。相反,应用程序不需要依赖插件来运行,这样插件就可以加载到应用程序上并动态更新,而不会对应用程序造成任何改变。但是我理解的插件更多的是一种设计形式,它可以有很多种展示形式。最贴近我心意的插件定义是handling-plugins-in-php一文中写的这句话:所谓插件就是一种允许非核心代码在运行时修改应用程序的处理方式.根据以上对插件的介绍,我们可以给插件系统下一个定义:插件系数是由一个可插件化的核心模块及其配套的插件模块组成的应用组织形式,其中核心模块可以独立运行并实现特定功能,插件模块需要运行在核心模块之上,可以在应用程序运行时修改程序的处理方式,从而增强或改变处理结果的程序。插件的大部分实现都是从设计模式演化而来的。大概可以参考:观察者模式、策略模式、装饰者模式、中介模式、责任链模式等等。插件系统一般由核心系统和插件模块两部分组成注:有时,我们也称插件为:add-on、module、extension。说是插件。核心模块核心模块,顾名思义,泛指系统的核心功能,定义了系统的运行模式和基本业务逻辑。核心系统一般不依赖任何插件。比如上面提到的Umi的核心就是路由。babel的核心能力是语法分析(将js文件转换为AST)。Webpack的核心系统是打包构建能力。插件模块插件模块是根据相应的协议或标准开发的外围配套设施。插件模块可能是一个js文件,一个配置文件,或者更复杂的应用系统,这完全取决于对应的“核心系统”是如何承包和加载插件的。为什么要插件?插件最重要的意义在于提高了整个系统的可扩展性。一句话:插件可以将扩展的功能分散在插件中,内部保持核心不变。它具有以下显着优势:维护成本低:只需关注核心系统的稳定性。轻松协同开发:由于核心系统与插件系统完全是单向依赖,插件之间基本相互独立,降低了“沟通协作”的成本,易于团队和第三方开发者共同扩展应用,很好的利用了社区生态。减少应用程序(核心包)大小:通过不加载未使用的功能来减少应用程序的大小,从而大大增加核心包的范围。轻松添加新功能:在工具开发之初,开发者很难考虑到整个应用的所有功能。如果将所有功能都写入核心包,可能会带来巨大的升级和维护成本。但通过插件系统,可以在不影响核心功能的情况下,快速添加新功能。一般来说,主要有以下几种插件形式(个人整理)常规插件注入插件事件插件插槽插件常规插件这个最简单,只要我们约定好,它可以轻松完成易于实现,常规插件一般依赖核心系统自行加载。如果约定比较简单,只是一些配置约定,可以用简单的JSON配置来实现。比如cms脚手架中的每一个模板都可以理解为一个插件。通过不同的配置,我们约定了模板的展示形式、模板的位置、交互问题……剩下的可以由用户根据自己的需求来创建。一个新的模板。然而,纯JSON所能表达的信息量仍然有限。因此,为了实现更复杂的插件功能,我们通常需要使用函数。比如我们约定一个插件结构是{name,action},action可以指定一个js函数module.exports={"name":"increase","action":(data)=>data.value+1}更进一步,通过约定好的目录结构来区分功能,比较有代表性的是Egg,通过目录结构来区分controller、middleware、schedule...,不同的目录结构自然对应不同的生命周期。例如,schedule目录下定义的文件将作为定时任务自动执行,并且约定了schedule的结构和任务方法。module.exports={schedule:{interval:'1m',//1分钟间隔type:'all',//指定所有worker都需要执行},asynctask(ctx){constres=awaitctx.curl('http://www.api.com/cache',{dataType:'json',});ctx.app.cache=res.data;},};比如:Egg注入插件,通常需要使用核心系统提供的API或者生命周期,这类插件通常是一个函数,接收一个API集合,比如Umi,就是一个很标准的注入插件-in,它的插件形式是一个接收API集合的函数:exportdefault(api)=>{//yourplugincodehere};与常规插件不同的是,这类插件通常会主动调用相关的API方法来注入自己的功能或能力。导出默认函数(api:IApi){api.logger.info('useplugin');api.modifyHTML(($)=>{$('body').prepend(`
helloUmiplugin
`);return$;});}示例:webpack,egg,babel事件插件in,顾名思义,提供通过事件开发插件的能力,最常见的比如dom事件:document.on("focus",callback);虽然只是普通的业务代码,但本质上是一种插件机制:可扩展:可以重复定义N个相互独立的焦点事件。事件相互独立:每个回调不会相互影响。也可以解释为事件机制是在某些阶段释放钩子,让用户代码延长整个框架的生命周期。ServiceWorker就更明显了。业务代码几乎完全由一堆时间监视器组成,比如安装时序。您可以随时添加监视器以延迟安装时间,而不会干扰其他代码。示例:服务工作者、dom事件。Socket插件这种插件通常是UI元素的扩展。最经典的代表就是React和Vue。它们的组件化其实就是插件化的另一种表现形式。虽然React本身在某种程度上是一个插件系统,但它专注于UI的抽象。带槽的组件可以理解为核心系统,槽是插件的入口。这样做的好处是实现了UI解耦,父元素不需要知道子元素的具体实例,只需要提供合适的槽位即可。functionMenu({plugins}){return
{plugins.map(p=>{p.name} )}
}该方法最常见的应用领域是CMS系统、静态页面生成器……当然,有些情况似乎是例外,比如Tree的查询功能,它依赖于子元素TreeNode的协作。但它依赖于基于某种约定的子元素,而不是具体子元素的实例。父元素只需要同意与子元素的接口。真正需要关心物理结构的是子元素。例如,插入Tree的子元素节点的TreeNode必须实现一定的方法。如果不满足这个功能,就不要把组件放到Tree下;不需要考虑Tree的实现。您只需要知道默认子元素有哪些约定即可。示例:React、gaea-editor。如何实现插件化?一般来说,要实现一个插件能力,核心系统需要提供以下能力:“必须”决定插件的注册和加载方式“必须”决定核心系统的生命周期和相关暴露的API“不需要”暴露插件合适范围的Context,隔离不同场景的上下文(通常是比较复杂的插件系统,比如vscode,chrome插件)“不需要”来判断插件dependencies"notnecessary"确定插件与核心系统的通信机制系统的大致流程如下:首先会经过解析插件的过程,主要是找到所有的插件-需要加载的插件。然后将这些插件绑定到特定的生命周期或事件。最后在合适的时候处理调用相应的插件就可以了。插件解析(导入)方式下面列举一些常用的插件导入方式:通过npm名称:比如只要npm包匹配了某个前缀,就会自动注册为插件,对于示例:umi约定,只要npm包名使用@umijs或者umi-plugin,开头都会自动加载为插件。按文件名:比如项目中存在xx.plugin.ts,会自动引用该插件,一般用作辅助解决方案。通过代码:这个很基础,就是通过代码来require,比如babel-polyfill,但是这个要求插件执行逻辑恰好在浏览器中运行,场景比较有限。通过描述文件:这是一种比较常见的方式。几乎所有的插件系统都会提供一个入口描述文件,比如描述package.json或者对应的配置文件中的一个属性,表示要加载的插件,比如.babelrc:{"plugins":["babel-plugin-myPlugin","@babel/plugin-transform-runtime"]}Umi的插件机制,比如Umi的插件,大致有以下几个方法:resolvePlugins:即解析插件,是获取对应插件的具体代码。主要的处理逻辑在getPlugins中。大致流程是从配置文件和约定的位置(包括内置的和自定义的)获取对应的插件地址,然后通过require动态加载,形成[id,apply,opts]结构,方便后续统一注册加载。initPlugins:是注册插件的进程。依次调用initPlugin注册插件。它通过Proxy将PluginApi、Service上的方法、环境变量注入到api对象中,然后为插件调用。这里其实用的是观察者模式。插件在调用具体方法(api.xxx)时,实际上是将相应的函数注册到方法的钩子上。applyPlugins:调用插件。在一个特定的生命周期内,所有订阅该生命周期的函数都可以通过调用该方法得到通知。从上面的步骤可以看出,Umi的流程也是按照我们之前说的:解析插件-->注册插件-->调用插件。超级简单的插件系统如何使用话不多说,给我看代码这里以一个计算器的例子来聊聊插件系统(点此进入codesandbox看实际例子)。比如下面这个例子,这个计算器的核心功能就是:有基本设置值的能力(应该是最简单的能力),然后我们在此基础上提供了两种方法,自增和自减.importReact,{useState}from"react";import"antd/dist/antd.css";import"./index.css";import{Button}from"antd";exportdefaultfunctionCalculator(props){const{初始值}=道具;const[value,setValue]=useState(initalValue||0);consthandleInc=()=>setValue(value+1);consthandleDec=()=>setValue(value-1);return(
);}在这个届时,如果我们想继续扩展它的能力,不用插件的想法,我们可能会直接扩展它上面的功能:exportdefaultfunctionCalculator(props){const[value,setValue]=useState(initalValue||0);consthandleInc=()=>setValue(value+1);consthandleDec=()=>setValue(value-1);//新功能consthandleSquared=()=>setValue(value*value);返回(
);}如果我们使用plugin首先提取一些通用的结构,约定一个插件的结构:{name,//buttonnameexec,//pressthebutton的执行方法}然后编写插件要注册的通用方法,比如我们这里的每一个插件都是一个按钮constbuttons=plugins.map((v)=>(
v.exec(value,setValue)}>{v.name}按钮>));返回();这里,我们将插件逻辑和渲染逻辑封装在一个函数中,将其拆分,然后添加核心插件(按钮),格式如下:exportdefaultfunctionshowCalculator({initalValue,plugins}){constcorePlugins=[{name:"inc",exec:(val,setVal)=>setVal(val+1)},{name:"dec",exec:(val,setVal)=>setVal(val-1)}];constnewPlugins=[...corePlugins,...plugins];返回<计算器initalValue={initalValue}plugins={newPlugins}/>;}现在我们有了最简单的插件计算器版本,我们可以扩展一个方形插件:showCalculator({initalValue:1,plugins:[{name:"square",exec:(val,setVal)=>setVal(val*val)}]}),而且很多插件系统都有生命周期钩子。我们也在这里模拟生命周期。一般来说,生命周期可以通过观察者模式。这里是最简单的一个Event机制(日常开发可以考虑Tapable)constevent={eventList:{},listen:function(key,fn){if(!this.eventList[key]){this.eventList[key]=[];}this.eventList[key].push(fn);},触发:函数(...args){constkey=args.splice(0,1);constfns=this.eventList[key];如果(!fns||fns.length===0){返回假;}for(leti=0,len=fns.length;i{//注册所有以on开头的Object.keys(p).filter(key=>key.indexOf('on')===0&&typeofp[key]==='函数').forEach(key=>event.listen(key,p[key]))});然后我们这里开启两个生命周期:onMount和onUnMount//这里简单定义两个生命周期consthandleMount=()=>event.trigger('onMount');consthandleUnMount=()=>event.trigger('onUnMount');它们的触发条件也很简单,就是写一个useEffectuseEffect(()=>{onMount();return()=>{onUnMount()}},[]);这时候我们在插件中添加对应的onMount方法,输出一句看看:OK,这么简单的插件系统就算是完成了。最后说了这么多,主要想给大家传达一个外挂的概念。在设计时,可以多考虑??应用的核心能力,专注于编写核心代码,通过插件扩展其他能力。这样,您只需要关注核心功能的实现是否可靠,其他功能的扩展性和可靠性由插件开发者负责。这样可以保证你的应用在功能稳定的前提下,具有更强的扩展性。同时这样也可以尽量避免写出特别复杂和难维护的代码。著名的Javascript工程师NicholasZakas(JavaScript高级编程、高性能JavaScript作者、Eslint作者)曾说过:一个好的框架或者一个好的架构是很难出错的,你的工作就是确保最简单的事情是正确的。一旦你理解了这一点,整个系统就会变得更容易维护。NicholasZakas,JavascriptJabber075-可维护的Javascript参考https://en.wikipedia.org/wiki/Plug-in_(computing)webpack插件Babel插件手册umi插件开发精讲《插件化思维》designing-a-javascript-plugin-systemhow-i-created-my-first-plugin-system在JavaScript和Node.js的PHPPlugin架构中处理插件,即插即用https://www.bryanbraun.com/2015/02/16/on-designing-great-syst...本文由网易云音乐技术团队发布。文章未经授权禁止任何形式的转载。我们常年招聘各种技术岗位。如果你要跳槽,又恰好喜欢云音乐,那就加入我们吧grp.music-fe(at)corp.netease.com!