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

插件化架构

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

各个主流框架插件化异同

二话不说先上结初始化游戏启动器失败论。

触发方式 插件babel什么意思 API 插件功能
umi 基于 tapable 的发源码编程器布订阅初始化游戏启动器失败模式 10 种核心方法,50 种扩展方法,9 个babel电影核心属性 在路由、生成文件、构建webpack是什么打包、HTML 操作、命令等方面提供能力
babel 基于 viswebpack配置itor 的接口英文访问者模式 基于@接口英文babel/types 对于 AST 的操作等
r源码精灵永久兑换码ollup 基于 hook 的回调模式 构建钩子、输出钩子、监听钩子 定制构建和打包阶段的能力接口crc错误计数
webpack 基于 tapable 的发布订阅模式 主要为 compolie接口和抽象类的区别r 和 compilBabela源码时代tion 提供一系列的钩子 loader 不能实现的都靠它
vue-cli 基于 hook 的回调模式 生成阶段为 Generator API,运行阶段为 chainWebpack 等更改 webpacwebpack打包流程k 配置为主的 api 在生成项目、项目运行和 vue ui 阶段提供能力

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

插件内核(plugiCore):用于管理插件源码时代

插件接口(plugiwebpack环境变量配置nApi):用于提供 api 给插件使用;

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

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

umi 插件(plugin)

我们先从最简单的初始化失败是怎么解决开始,认识一个umi 插件长什么样。我们以插件集preset(@webpack面试题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 上babel原理的两个方法属性,主要实现了两个功能,一个是在 htmbabel原理l 文件头部添加脚本,另一个是在入口文件最后添加代码。其中,preset是一系列插件的合集。代码非常简单源码,就是 require 了一系列的plugin。插件集preset(packages/preset-初始化built-in/src/webpack最多支持几个入口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/s接口crc错误计数rc/plugins/features/),一些html 操作相关插件(packages/preset-built-in/src/plugwebpack热更新原理ins/features/html/)以及一些命令相关插件(packages/preset-built-in/src/plugins/commands/)。

源码编程器注册方法插件registerMethods(packages/pre源码精灵永久兑换码set-built-in/src/plugwebpack是什么ins/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-nocheckintofirstline
content=`//@ts-nocheck${EOL}${content}`;
}
if(!existsSync(absPath)||readFileSync(absPath,'utf-8')!==content){
writeFileSync(absPath,content,'utf-8');
}
},
});
}

当我们在控制台umi路径webpack是什么下键入命令npx umi dev后,就启动了 umi 命源码精灵永久兑换码令,附带 dev 参数,经过一系列的操作后实例化Service对象(路径:packages/umi/src/ServiceWithBuilt接口是什么In.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使初始化电脑用。至此我们介webpack打包流程绍了以默认插件集preset为代表的umi插件。

插件接口(pluginApi)

Service对象(packages/core/src/Service/Service.ts)中的getPluginAPI方法为插件提供了插件接口getPluginAPIWebpack接口就是整个插件系统的babel韩剧桥梁。它使用代理模式将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),并传入pr初始化电脑时出现问题eset插件集(@umijs/prbabel什么意思eset-webpack官网中文built-in)。该对象继承自CoreServeice(packages/corewebpack配置/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,插件唯一的key
path:winPath(path),//路径
apply(){
//延迟加载插件
try{
constret=require(path);
//usethedefaultmemberforesmodules
returncompatESModuleRequire(ret);
}catch(e){
thrownewError(`Register${type}${path}failed,since${e.message}`);
}
},
defaultConfig:null,//默认配置
};
2.初始化插件

umi实例化Service对象babel巴别塔后会调用Servi接口测试用例设计ce对象的run方法。插件的初始化就是在run方法中完成的。初始化presetplugin的过程babel电影大同小异源码时代,我们重点看初始化plugin的过程。

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

这里我们要重点看一下在最开始prWebpackeset集中第一个注册webpack打包流程方法插件中注册扩展方法时曾提到的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-ignore
this.register(hook);
};
}

因此当执行插件代码时,如果是核心方法则直接执行,如果是扩展方法则除了writeTmp接口英文File,其余都是初始化电脑hooksByPluginId下注册了hook。到这里Servi初始化电脑时出现问题ce完成了插件的初始化,执行了插件调用的核心方法和扩展方法。

3.初始化 hooks

通过下述代码,Service将以插件名称为维度配置的ho源码编辑器下载ok,转换为以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扩展方法为例 转换前:

'./node_modules/@@/features/devScripts':[
{key:'addBeforeMiddlewares',fn:[Function(anonymous)]},
{key:'addHTMLHeadScripts',fn:[Function(anonymous)]},
……
],
'./node_modules/@@/features/umiInfo':[
{key:'addHTMLHeadScripts',fn:[Function(anonymous)]},
{key:'addEntryCode',fn:[Function(anonymous)]}
],
'./node_modules/@@/features/html/headScripts':[{key:'addHTMLHeadScripts',fn:[Function(anonymous)]}],

转换之后:

addHTMLHeadScripts:[
{
key:'addHTMLHeadScripts',
fn:[Function(anonymous)],
pluginId:'./node_modules/@@/features/devScripts'
},
{
key:'addHTMLHeadScripts',
fn:[Function(anonymous)],
pluginId:'./node_modules/@@/features/umiInfo'
},
{
key:'addHTMLHeadScripts',
fn:[Function(anonymous)],
pluginId:'./node_modules/@@/features/html/headScripts'
}
],

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

4.触babel韩剧发 hook

在程序达到 pluginReady 状态后接口和抽象类的区别,Service 立即执行了一次触发 h源码编辑器ook 操作。

awaitthis.applyPlugins({
key:'onPluginReady',
type:ApplyPluginsType.event,
});

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

asyncapplyPlugins(opts:{
key:string;
type:ApplyPluginsType;
initialValue?:any;
args?:any;
}){
//找到对应需要触发的hook会调集,这里的hooks就是上面以插件名称为维度配置的hook转换为以hook名称为维度配置的回调集
consthooks=this.hooks[opts.key]||[];
//判断事件类型,umi将回调事件分为add、modify和event三种
switch(opts.type){
caseApplyPluginsType.add:
if('initialValue'inopts){
assert(
Array.isArray(opts.initialValue),
`applyPluginsfailed,opts.initialValuemustbeArrayifopts.typeisadd.`,
);
}
//事件管理基于webpack的Tapable库,只用到了AsyncSeriesWaterfallHook一种事件控制方式,既异步串行瀑布流回调方式:异步,所有的钩子都是异步处理;串行,依次执行;瀑布流,上一个钩子的结果是下一个钩子的参数。
consttAdd=newAsyncSeriesWaterfallHook(['memo']);
for(consthookofhooks){
if(!this.isPluginEnable(hook.pluginId!)){
continue;
}
tAdd.tapPromise(
{
name:hook.pluginId!,
stage:hook.stage||0,
//@ts-ignore
before:hook.before,
},
//与其他两种事件类型不同,add类型会返回所有钩子的结果
async(memo:any[])=>{
constitems=awaithook.fn(opts.args);
returnmemo.concat(items);
},
);
}
returnawaittAdd.promise(opts.initialValue||[]);
caseApplyPluginsType.modify:
consttModify=newAsyncSeriesWaterfallHook(['memo']);
for(consthookofhooks){
if(!this.isPluginEnable(hook.pluginId!)){
continue;
}
tModify.tapPromise(
{
name:hook.pluginId!,
stage:hook.stage||0,
//@ts-ignore
before:hook.before,
},
//与其他两种钩子不同,modify类型会返回最终的钩子结果
async(memo:any)=>{
returnawaithook.fn(memo,opts.args);
},
);
}
returnawaittModify.promise(opts.initialValue);
caseApplyPluginsType.event:
consttEvent=newAsyncSeriesWaterfallHook(['_']);
for(consthookofhooks){
if(!this.isPluginEnable(hook.pluginId!)){
continue;
}
tEvent.tapPromise(
{
name:hook.pluginId!,
stage:hook.stage||0,
//@ts-ignore
before:hook.before,
},
//event类型,只执行钩子,不返回结果
async()=>{
awaithook.fn(opts.args);
},
);
}
returnawaittEvent.promise();
default:
thrownewError(
`applyPluginfailed,typeisnotdefinedorisnotmatched,got${opts.type}.`,
);
}
}

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

babel 插件机制

babel主要的作用就是语法转换babel的整个babel-polyfill过程分为三个部分:解析,将代码转换为抽象语法树(AST);转换,遍历 AST 中的节点进行语法转换操作;生成,根据最新的 AST 生成目标代码初始化电脑时出现问题未进行更改。其中初始化是什么意思在转换的过程中接口crc错误计数就是依据babel配置的各个插件去完成的。

babel 插件
constcreatePlugin=(name)=>{
return{
name,
visitor:{
FunctionDeclaration(path,state){},
ReturnStatement(path,state){},
}
};
};

可以看到babel的插件也是返回一个函数,和umi的很相似。但是babel插件的运行却并不是基于发布订阅的事件驱动模式源码1688而是采用访问者模接口和抽象类的区别babwebpack性能优化el会通过一个访问webpack环境变量配置vwebpack是什么isitor统一遍历节点,提供方法及维护节点关系,插件只需Babel要在visitor中注册自己关心的节点类型,当visitor遍历到相关节点时就会调用插件在visitor上注册的方法并执行。

webpack 插件babel巴别塔机制

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

constpluginName='ConsoleLogOnBuildWebpackPlugin';
classConsoleLogOnBuildWebpackPlugin{
apply(compiler){
compiler.hooks.run.tap(pluginName,(compilation)=>{
console.log('webpack 构建正在启动!');
});
}
}
module.exports=ConsoleLogOnBuildWebpackPlugin;

webpack在初始化时会统一执行插件的apply方法。插件接口文档通过注册Compilercompilation的钩接口子函数,在整个编译生命周初始化是什么意思期都可以访问compiler对象,完成插件功能。同时整个事件驱动的功能都是基于 webpack 的源码交易平台核心工具TapableTapable同样也是umi的事件驱动工具。可以看到umiwebpack的整体思路是很相似的。

ro初始化llup 插件机制

rollup也是模块打包工具,与 webpack 相比rollup更适babel什么意思合打包纯 js 的类库。同样rollup也具有插件机制。一个典型的rollup插件:

exportdefaultfunctionmyExample(){
return{
name:'my-example',
resolveId(source){},
load(id){},
};
}

rollup 插件维护源码精灵永久兑换码了一套同步/异步、串行/并行、熔断/传参的事件回调机制,不过这部分并没有单独抽出类库,而是在 rollup 项目中维护的。通过插件控制器(src/utils/PluginDriver.ts)、插件上下文(src/utils/PluginCowebpack面试题ntext.ts)、插件缓存(src初始化磁盘/接口文档utils/PluginCache.ts),完成初始化磁盘了提供插件 api 和插件内源码之家核的能力。

vue-cli 插件机webpack环境变量配置

vue-cli的插件与其他相比稍有特点,就是将插件分为几种情况,一种项目生成阶段,插件未安装需要安装插件;另一种是项目运行阶段,启动插件;还有一种是UI插件,在运行vuBabele ui时会用到。源码精灵永久兑换码

vue-cli插件的包目录结构

├──generator.js#generator(可选)
├──index.js#service插件
├──package.json
└──prompts.js#prompt文件(可选)
└──ui.js#ui文件(可选)
生成阶段

其中generator.jsprompts.js在安装插件的情况下执行,index 则在运行阶段执行。generator 示例:

module.exports=(api,options)=>{
//扩展package.json字段
api.extendPackage({
dependencies:{
'vue-router-layout':'^0.1.2'
}
})
//afterAnyInvoke钩子函数会被反复执行
api.afterAnyInvoke(()=>{
//文件操作
})
//afterInvoke钩子,这个钩子将在文件被写入硬盘之后被调用
api.afterInvoke(()=>{})
}

prompts 会在安装期间与用户交互,获取插件的选项配置并在 generator.js 调用时作为参数存入。

在项目生成阶段通过 packages/@vue/cli/lib/GeneratorAPI.js 提供插件 api;在 pa源码时代ckages/@vue/cli/lib/Generator.js 中初始化插件,执行插件注册的 api,在 pacbabel原理kages/@vue/clBabeli/lib/Creatwebpack面试题or.js 中运行插件注册的钩子函数,最终完成插件功能的调用。

运行阶段

vue-cli初始化电脑时出现问题运行阶段插件:

constVueAutoRoutingPlugin=require('vue-auto-routing/lib/webpack-plugin')
module.exports=(api,options)=>{
api.chainWebpack(webpackConfig=>{
webpackConfig
.plugin('vue-auto-routing')
.use(VueAutoRoutingPlugin,[
{
pages:'src/pages',
nested:true
}
])
})
}

在项目运行阶段的插件主要用来修改webpack的配置,创建初始化电脑时出现问题未进行更改或者修改命令。由 packages/@vue/cli-service/lib/PluginAPI.js 提供pl接口英文uginapi,packages/@vue/cli-service/lib/Service.js 完成插件的初始化和运行。而vue-cli插件的运行主要是基于webpack面试题回调函数的模式来管理的。

通过以Babel上介绍,可以发现插件机制是现代前端项目工程化框架中必不可少的一部分,插件的实现形式多种多样,但总的结构是大体一致的,既由插件(plugin)插件 api(p接口是什么luginApi)插件核心(pluginCore) 三部分组成。其中通接口测试用例设计过插件核心去注册和管理插件,完成插件的初始化和运行工作,插件 api 是插件和系统之间的初始化sdk什么意思桥梁,使插件完成特定功能,再通过不同插件的组合形成了一套功能完源码中的图片整的前端框架系统。

评论

发表回复