问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

UMI3源码解析系列之插件化架构核心

发布网友 发布时间:2024-10-02 10:04

我来回答

1个回答

热心网友 时间:2024-12-12 08:30

插件化架构

插件化架构(Plug-inArchitecture),也被称为微内核架构(MicrokernelArchitecture),是一种面向功能进行拆分的可扩展性架构,在如今的许多前端主流框架中都能看到它的身影。今天我们以umi框架为主,来看看插件化架构的实现思路,同时对比一下不同框架中插件化实现思路的异同。

各个主流框架插件化异同

二话不说先上结论。

触发方式插件API插件功能umi基于tapable的发布订阅模式10种核心方法,50种扩展方法,9个核心属性在路由、生成文件、构建打包、HTML操作、命令等方面提供能力babel基于visitor的访问者模式基于@babel/types对于AST的操作等rollup基于hook的回调模式构建钩子、输出钩子、监听钩子定制构建和打包阶段的能力webpack基于tapable的发布订阅模式主要为compolier和compilation提供一系列的钩子loader不能实现的都靠它vue-cli基于hook的回调模式生成阶段为GeneratorAPI,运行阶段为chainWebpack等更改webpack配置为主的api在生成项目、项目运行和vueui阶段提供能力

一个完整的插件系统应该包括三个部分:

插件内核(plugiCore):用于管理插件;

插件接口(pluginApi):用于提供api给插件使用;

插件(plugin):功能模块,不同的插件实现不同的功能。

因此我们也从这三部分入手去分析umi的插件化。

umi插件(plugin)

我们先从最简单的开始,认识一个umi插件长什么样。我们以插件集preset(@umijs/preset-built-in)中的一个内置插件umiInfo(packages/preset-built-in/src/plugins/features/umiInfo.ts)为例,来认识一下umi插件。

import{IApi}from'@umijs/types';exportdefault(api:IApi)=>{//调用扩展方法addHTMLHeadScripts在HTML头部添加脚本api.addHTMLHeadScripts(()=>[{content:`//!umiversion:${process.env.UMI_VERSION}`,},]);//调用扩展方法addEntryCode在入口文件最后添加代码api.addEntryCode(()=>`window.g_umi={version:'${process.env.UMI_VERSION}',};`,);};

可以看到umi插件导出了一个函数,函数内部为调用传参api上的两个方法属性,主要实现了两个功能,一个是在html文件头部添加脚本,另一个是在入口文件最后添加代码。其中,preset是一系列插件的合集。代码非常简单,就是require了一系列的plugin。插件集preset(packages/preset-built-in/src/index.ts)如下:

exportdefaultfunction(){return{plugins:[//注册方法插件require.resolve('./plugins/registerMethods'),//路由插件require.resolve('./plugins/routes'),//生成文件相关插件require.resolve('./plugins/generateFiles/core/history'),……//打包配置相关插件require.resolve('./plugins/features/404'),……//html操作相关插件require.resolve('./plugins/features/html/favicon'),……//命令相关插件require.resolve('./plugins/commands/build/build'),……}

这些plugin主要包括一个注册方法插件(packages/preset-built-in/src/plugins/registerMethods.ts),一个路由插件(packages/preset-built-in/src/plugins/routes.ts),一些生成文件相关插件(packages/preset-built-in/src/plugins/generateFiles/),一些打包配置相关插件(packages/preset-built-in/src/plugins/features/),一些html操作相关插件(packages/preset-built-in/src/plugins/features/html/)以及一些命令相关插件(packages/preset-built-in/src/plugins/commands/)。

在注册方法插件registerMethods(packages/preset-built-in/src/plugins/registerMethods.ts)中,umi集中注册了几十个方法,这些方法就是umi文档中插件api的扩展方法。

exportdefaultfunction(api:IApi){//集中注册扩展方法['onGenerateFiles','onBuildComplete','onExit',……].forEach((name)=>{api.registerMethod({name});});//单独注册writeTmpFile方法,并传参fn,方便其他扩展方法使用api.registerMethod({name:'writeTmpFile',fn({path,content,skipTSCheck=true,}:{path:string;content:string;skipTSCheck?:boolean;}){assert(api.stage>=api.ServiceStage.pluginReady,`api.writeTmpFile()shouldnotexecuteinregisterstage.`,);constabsPath=join(api.paths.absTmpPath!,path);api.utils.mkdirp.sync(dirname(absPath));if(isTSFile(path)&&skipTSCheck){//write@ts-nocheckintofirstlinecontent=`//@ts-nocheck${EOL}${content}`;}if(!existsSync(absPath)||readFileSync(absPath,'utf-8')!==content){writeFileSync(absPath,content,'utf-8');}},});}

当我们在控制台umi路径下键入命令npxumidev后,就启动了umi命令,附带dev参数,经过一系列的操作后实例化Service对象(路径:packages/umi/src/ServiceWithBuiltIn.ts),

import{IServiceOpts,ServiceasCoreService}from'@umijs/core';import{dirname}from'path';classServiceextendsCoreService{constructor(opts:IServiceOpts){process.env.UMI_VERSION=require('../package').version;process.env.UMI_DIR=dirname(require.resolve('../package'));super({...opts,presets:[//配置内置默认插件集require.resolve('@umijs/preset-built-in'),...(opts.presets||[]),],plugins:[require.resolve('./plugins/umiAlias'),...(opts.plugins||[])],});}}export{Service};

在Service的构造函数中就传入了上面提到的默认插件集preset(@umijs/preset-built-in),供umi使用。至此我们介绍了以默认插件集preset为代表的umi插件。

插件接口(pluginApi)

Service对象(packages/core/src/Service/Service.ts)中的getPluginAPI方法为插件提供了插件接口。getPluginAPI接口就是整个插件系统的桥梁。它使用代理模式将umi插件核心方法、初始化过程hook节点api、Service对象方法属性和通过@umijs/preset-built-in注册到service对象上的扩展方法组织在了一起,供插件调用。

getPluginAPI(opts:any){//实例化PluginAPI对象,PluginAPI对象包含describe,register,registerCommand,registerPresets,registerPlugins,registerMethod,skipPlugins七个核心插件方法constpluginAPI=newPluginAPI(opts);//注册umi服务初始化过程中的hook节点['onPluginReady',//插件初始化完毕'modifyPaths',//修改路径'onStart',//启动umi'modifyDefaultConfig',//修改默认配置'modifyConfig',//修改配置].forEach((name)=>{pluginAPI.registerMethod({name,exitsError:false});});returnnewProxy(pluginAPI,{get:(target,prop:string)=>{//由于pluginMethods需要在register阶段可用//必须通过proxy的方式动态获取最新,以实现边注册边使用的效果if(this.pluginMethods[prop])returnthis.pluginMethods[prop];//注册umiservice对象上的属性和核心方法if(['applyPlugins','ApplyPluginsType','EnableBy','ConfigChangeType','babelRegister','stage',……].includes(prop)){returntypeofthis[prop]==='function'?this[prop].bind(this):this[prop];}returntarget[prop];},});}插件内核(pluginore)1.初始化配置

上面讲到启动umi后会实例化Service对象(路径:packages/umi/src/ServiceWithBuiltIn.ts),并传入preset插件集(@umijs/preset-built-in)。该对象继承自CoreServeice(packages/core/src/Service/Service.ts)。CoreServeice在实例化的过程中会在构造函数中初始化插件集和插件:

//初始化Presets和plugins,来源于四处//1.构造Service传参//2.process.env中指定//3.package.json中devDependencies指定//4.用户在.umirc.ts文件中配置this.initialPresets=resolvePresets({...baseOpts,presets:opts.presets||[],userConfigPresets:this.userConfig.presets||[],});this.initialPlugins=resolvePlugins({...baseOpts,plugins:opts.plugins||[],userConfigPlugins:this.userConfig.plugins||[],});

经过转换处理,一个插件在umi系统中最终会表示为如下格式的一个对象:

{id,//@umijs/plugin-xxx,插件名称key,//xxx,插件唯一的keypath:winPath(path),//路径apply(){//延迟加载插件try{constret=require(path);//usethedefaultmemberforesmolesreturncompatESMoleRequire(ret);}catch(e){thrownewError(`Register${type}${path}failed,since${e.message}`);}},defaultConfig:null,//默认配置};2.初始化插件

umi实例化Service对象后会调用Service对象的run方法。插件的初始化就是在run方法中完成的。初始化preset和plugin的过程大同小异,我们重点看初始化plugin的过程。

//初始化插件asyncinitPlugin(plugin:IPlugin){//在第一步初始化插件配置后,插件在umi系统中就变成了一个个的对象,这里导出了id,key和延迟加载函数applyconst{id,key,apply}=plugin;//获取插件系统的桥梁插件接口PluginApiconstapi=this.getPluginAPI({id,key,service:this});//注册插件this.registerPlugin(plugin);//执行插件代码awaitthis.applyAPI({api,apply});}

这里我们要重点看一下在最开始preset集中第一个注册方法插件中注册扩展方法时曾提到的registerMethod方法。

registerMethod({name,fn,exitsError=true,}:{name:string;fn?:Function;exitsError?:boolean;}){//注册的方法已经存在的情况的处理if(this.service.pluginMethods[name]){if(exitsError){thrownewError(`api.registerMethod()failed,method${name}isalreadyexist.`,);}else{return;}}//这里分为两种情况:第一种注册方法时传入了fn参数,则注册的方法就是fn方法;第二种情况未传入fn,则返回一个函数,函数会将传入的fn参数转换为hook钩子并注册,挂载到service的hooksByPluginId属性下this.service.pluginMethods[name]=fn||function(fn:Function|Object){consthook={key:name,...(utils.lodash.isPlainObject(fn)?fn:{fn}),};//@ts-ignorethis.register(hook);};}

因此当执行插件代码时,如果是核心方法则直接执行,如果是扩展方法则除了writeTmpFile,其余都是在hooksByPluginId下注册了hook。到这里Service完成了插件的初始化,执行了插件调用的核心方法和扩展方法。

3.初始化hooks

通过下述代码,Service将以插件名称为维度配置的hook,转换为以hook名称为维度配置的回调集。

Object.keys(this.hooksByPluginId).forEach((id)=>{consthooks=this.hooksByPluginId[id];hooks.forEach((hook)=>{const{key}=hook;hook.pluginId=id;this.hooks[key]=(this.hooks[key]||[]).concat(hook);});});

以addHTMLHeadScripts扩展方法为例转换前:

exportdefaultfunction(){return{plugins:[//注册方法插件require.resolve('./plugins/registerMethods'),//路由插件require.resolve('./plugins/routes'),//生成文件相关插件require.resolve('./plugins/generateFiles/core/history'),……//打包配置相关插件require.resolve('./plugins/features/404'),……//html操作相关插件require.resolve('./plugins/features/html/favicon'),……//命令相关插件require.resolve('./plugins/commands/build/build'),……}0

转换之后:

exportdefaultfunction(){return{plugins:[//注册方法插件require.resolve('./plugins/registerMethods'),//路由插件require.resolve('./plugins/routes'),//生成文件相关插件require.resolve('./plugins/generateFiles/core/history'),……//打包配置相关插件require.resolve('./plugins/features/404'),……//html操作相关插件require.resolve('./plugins/features/html/favicon'),……//命令相关插件require.resolve('./plugins/commands/build/build'),……}1

至此插件系统就绪达到pluginReady状态。

4.触发hook

在程序达到pluginReady状态后,Service立即执行了一次触发hook操作。

exportdefaultfunction(){return{plugins:[//注册方法插件require.resolve('./plugins/registerMethods'),//路由插件require.resolve('./plugins/routes'),//生成文件相关插件require.resolve('./plugins/generateFiles/core/history'),……//打包配置相关插件require.resolve('./plugins/features/404'),……//html操作相关插件require.resolve('./plugins/features/html/favicon'),……//命令相关插件require.resolve('./plugins/commands/build/build'),……}2

那么是如何触发的呢?我们来详细看一下applyPlugins的代码实现:

exportdefaultfunction(){return{plugins:[//注册方法插件require.resolve('./plugins/registerMethods'),//路由插件require.resolve('./plugins/routes'),//生成文件相关插件require.resolve('./plugins/generateFiles/core/history'),……//打包配置相关插件require.resolve('./plugins/features/404'),……//html操作相关插件require.resolve('./plugins/features/html/favicon'),……//命令相关插件require.resolve('./plugins/commands/build/build'),……}3

至此,umi的整体插件工作流程介绍完毕,后续代码就是umi根据流程需要不断触发各类的hook从而完成整个umi的各项功能。除了umi,其他的一些框架也都应用了插件模式,下面做简单介绍对比。

babel插件机制

babel主要的作用就是语法转换,babel的整个过程分为三个部分:解析,将代码转换为抽象语法树(AST);转换,遍历AST中的节点进行语法转换操作;生成,根据最新的AST生成目标代码。其中在转换的过程中就是依据babel配置的各个插件去完成的。

babel插件exportdefaultfunction(){return{plugins:[//注册方法插件require.resolve('./plugins/registerMethods'),//路由插件require.resolve('./plugins/routes'),//生成文件相关插件require.resolve('./plugins/generateFiles/core/history'),……//打包配置相关插件require.resolve('./plugins/features/404'),……//html操作相关插件require.resolve('./plugins/features/html/favicon'),……//命令相关插件require.resolve('./plugins/commands/build/build'),……}4

可以看到babel的插件也是返回一个函数,和umi的很相似。但是babel插件的运行却并不是基于发布订阅的事件驱动模式,而是采用访问者模式。babel会通过一个访问者visitor统一遍历节点,提供方法及维护节点关系,插件只需要在visitor中注册自己关心的节点类型,当visitor遍历到相关节点时就会调用插件在visitor上注册的方法并执行。

webpack插件机制

webpack整体基于两大支柱功能:一个是loader,用于对模块的源码进行转换,基于管道模式;另一个就是plugin,用于解决loader无法解决的问题,顾名思义,plugin就是基于插件机制的。来看一个典型的webpack插件:

exportdefaultfunction(){return{plugins:[//注册方法插件require.resolve('./plugins/registerMethods'),//路由插件require.resolve('./plugins/routes'),//生成文件相关插件require.resolve('./plugins/generateFiles/core/history'),……//打包配置相关插件require.resolve('./plugins/features/404'),……//html操作相关插件require.resolve('./plugins/
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
邯郸自驾游到青岛马壕运河遗址推荐线路 株洲自驾到青岛马壕运河遗址途径地方 梧州回青岛马壕运河遗址要几个小时 石嘴山到青岛马壕运河遗址要多少油钱 可不可以用开水敷脸 开水能不能敷脸 发动机和发电机区别?? 电音之王朴智妍MV的图片? 电音之王mv里跳舞的是谁 自己怎样开网站 怎样把一个网页设置为主页 Vue3中组件使用ref时获取组件类型推导 Vue 3 Composition API与Hooks模式 ...ReturnType的多hook间返回值和参数类型实现“类型收敛” 福建金桥学校如何体现其独特的教育理念和价值观? 临沂市金雀山职工中等专业学校办学宗旨 雒城二中教育宗旨 重庆安全技术职业学院办学宗旨 最值得收藏的25条美容妙招 这个月要去韩国旅行,给家人带点什么纪念品好呢,韩国景区推荐平价_百度... 太吾绘卷怎么偷精纯介绍_太吾绘卷怎么偷精纯是什么 惊天破完整版?惊天破电影高清百度云下载 梦幻西游的字在PS里是什么选项 梦幻西游怎样打出颜色的字 有谁知道鬼吹灯有声版第一部精绝古城到第十二话后面的哪里能够找到呢... 各位朋友谁能给我一个鬼吹灯MP3格式的打包下载能下的来的地址,我有找... 电话费实缴金额是289.67元,可是我缴了300元,其中10.63元转为预存金... EBC5丰帆的作品都有哪些?鬼吹灯就不用说了。如果有丰帆其他作品的下载... EBC5丰凡版鬼吹灯mp3第三四季哪有?下载 下列( )账户的余额一般在贷方 A固定资产 B预收账款 C预付账款 D实... WPS表格中双击单元格后,单元格里面的内容就消失了是怎么... TradingView--前端(Vue)最专业的K线图表工具(只支持历史数据K线展示... 梦见马蜂蛰老婆 ...半年教师资格课程考试通知举办教育学、教育心理学考前培训 关于汾酒杏花村 山西杏花村酒厂出产的89年60度的瓷瓶汾酒的价格大约是多少?瓶子印了一... 2002牧童杏花村 38度 糖度:65-85 500ML 标准号:QBT1981 许可证:XK01... 南瓜花卷的做法及花样窍门 阴道口两侧瘙痒,抓了会有红点出现,越抓越明显,有时侯连阴唇都会痒,这是... ...突然发现龟头上长了几颗小红点,有时感觉有点痒,不是是性病吧?我现在... 身上有小红点伴随着痒是怎么回事 ...就有红点出现,而且越挠越多,请问一下这是怎么回事?应该怎么办?_百度... ...我想快速瘦腿、减肥,还想快速长高有什么好的运动... ...月快速有效的减肥方法!最好是最有效的瘦腰瘦腿的方法!悬赏50分... 死神大战火影中的鸣人怎么爆九尾 平顶山中考录取分数线 求HYDE<虹>&<雪之足迹>歌词 日本彩虹乐团有那些歌好听?不过我感觉和x-japan的没法比 但也有人气... 安徽2007年510分到515分之间多少人? 2021永州市三中录取分数线是多少? 每天晚上都可以走在他后面 有时会忍不住上去找他 这样好不好