Babel插件、webpack插件、vue-cli插件,为什么这么多优秀的框架都使用插件系统呢? 插件架构有哪些? 有什么好处? 可以应用在哪些场景?
1.插件架构定义
插件架构也称为微内核架构,是指软件的核心比较小,主要功能和业务逻辑都通过插件来实现。 插件架构通常有两个核心概念:内核和插件。
内核(pluginCore)通常只包含系统运行的最少功能;
插件是通常提供单一功能的独立模块。 内核通常将要完成的所有业务可视化,并抽象出最小的细粒度基础套接字供插件调用。 这样的话,插件开发的效率将会大大提升。 例如浏览器就是典型的插件架构。 浏览器是核心,页面是插件。 这样通过不同的URL地址加载不同的页面,提供特别丰富的功能。 而且,当我们开发网页时,浏览器会提供很多API和能力,而这些socket都是通过窗口挂载的,比如DOM、BOM、Event、Location等。
设计一个已建立的插件架构系统,它包含三个要素:
PlugCore:插件核心,提供插件运行时,管理插件加载、运行、卸载的生命周期(类比浏览器);
pluginAPI:插件运行所需的基本socket(类比浏览器例子,相当于window);
插件:一系列具有特定功能的独立模块(类比浏览器的情况,相当于不同的网页)。
2.插件架构实践
我们从plugCore、pluginAPI、plugin三个要素出发,剖析jQuery、Babel、Vue CLI这三个优秀开源库的插件架构实践。
2.1 jQuery的插件框架
jQuery 是一个 JavaScript 库,它极大地简化了 JavaScript 编程,用更少的代码做更多的事情。 早期浏览器的标准并不统一,开发出兼容不同浏览器用户的网页非常令人沮丧。 jQuery在适应不同浏览器差异的基础上,为后端开发者提供了更易用的API来完成网页编程。 使用jQuery编译的网页在一套代码下也可以在不同厂家的浏览器上正常运行。 在 MV* 框架流行之前,jQuery 是绝对的霸主。 jQuery是可扩展的,并且有完善的插件系统。 Web开发所需的各类插件都可以在其生态中找到。 我们来分析一下jQuery插件系统。
插件定义:
特别说明:$.fn = jQuery.protype(插件精华)。 jQuery的插件机制是通过原型链挂载的。
插件机制执行流程
演示示例
$app可以在原型链上找到myPlugin:
总结起来就是三个要素:
pluginCore:通过原型链参数扩展不同的插件,获取jQuery实例后即可调用。
pluginAPI:jQuery包的核心socket,(jQuery以其优秀的Api取胜)
plugin:不受限制typescript 调用jquery,可以是JavaScript类型,一般是实现特定功能的模块,比如日期选择器等。 2.2 Babel的插件架构
Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本代码转换为向后兼容的 JavaScript 语法,以便可以在当前和旧版本的浏览器或其他环境中运行。 在代码转换的过程中,涉及到很多特性和语法转换,ECMAScript的提案总是在不断更新。 如何组织大量(且不断减少)的转换规则? 让我们看看 Babel 是如何工作的。
Babel 转换源代码分三步:
Parse:进行词法分析(Lexical Analysis)和句型分析(Syntropic Analysis),生成抽象语法树(AST);
Transform(变换):遍历AST中的每个节点,并执行相应的变换操作。 这个过程使用不同的插件来实现各种特征和句型的转换;
生成(generate):根据AST生成目标代码。
Babel 在 AST 转换过程中(即上图中的步骤 2)采用了插件架构。 下面将详细解释这个转换过程中使用插件架构的情况。
插件定义:
插件是一个返回包含访问者的对象的函数。 插件定义的部分概念描述:
名称:插件名称
pluginAPI:插件运行时传入的API
Visitor:是一个对象,对象的key是AST各个节点的类型,对象的值是一个函数,AST转换的过程就发生在这里。
nodePath:它是 AST 节点的实例对象。 具体可以参考:@babel/parser/src/parser/node.js[1],其中,type array:节点的类型,常见类型:VariableDeclaration(变量声明)、VariableDeclarator(变量声明表达式)、 ArrowFunctionExpression(箭头函数表达式)等,详细请参考@babel/types[2]。 (作者认为pluginAPI还应该包含nodePath,因为每个节点实例不仅描述了句型和词汇描述,还包含了所需句型之间的转换方法)
插件示例
一个将箭头函数转换为普通函数的插件:@babel/plugin-transform-arrow-functions [3] 源码:
插件的执行思路:
第一步,执行插件并获取访问者对象;
第二步,ATS遍历节点,检测nodePath === 'ArrowFunctionExpression'的类型,找到vistor对象中key为ArrowFunctionExpression的函数;
第三步,将nodePath传入这个函数中调用(这一步改变了AST); 单个插件的执行思路已经很清晰了,那么在ATS遍历过程中如何让多个插件协同工作呢?
Babel在转换源代码的过程中,插件架构的工作流程如下:
第一步:通过解析babel配置文件(或者命令行的--plugins参数)获取Babel配置的所有插件的描述;
第二步,将插件的require放入显存,获取插件函数,执行插件函数,获取多个包含vistor数组的对象; (详细逻辑:@babel/core/src/config/full.js [4])
第三步,将多个包含vistor数组的对象整合成一个大的visor源码展示(详细逻辑:@babel/core/src/transformation/index.js [5]):
合并后的访问者对象:
访问者对象中的值变成Array“function(nodePath)”
第四步,遍历AST时,各个节点根据NodeType获取visitor[NodeType],并依次执行。 总结起来就是三个要素:
pluginCore:插件被加载并集成(即访客合并)。 AST遍历时,调用vistor[NodeType]查找并依次调用;
pluginAPI:nodePath,提供不同类型节点的socket,用于转换AST节点;
插件:访问者[NodeType]=函数(节点路径)。 2.3 vue-cli的插件架构
Vue CLI 是一个基于 Vue.js 的完整快速开发系统。 CLI 插件是 npm 包,为您的 Vue 项目提供可选功能,例如 Babel/TypeScript 转译、ESLint 集成、单元测试和端到端测试等。Vue CLI 插件的名称以 @vue/cli-plugin 开头-(内置插件)或vue-cli-plugin-(社区插件)typescript 调用jquery,非常易于使用。 接下来我们来分析cli插件的定义、执行、安装等流程。
插件定义
该插件必须是一个以 vue-cli-plugin 命名的 npm 包,并且目录结构也必须严格按照文件命名定义。
注:@vue/cli-service[6]将使用项目根目录下package.json中的dependency和devDependencies中定义的符合插件命名规范的npm包作为项目插件。
文件命名及内容说明:
Generator.js:添加插件时会执行,可以安装npm包、修改项目源码等功能;
Prompts.js:所有对话逻辑都存储在prompts.js文件中。 对话框内部通过询问器[7]实现,调用该询问器获取安装选项结果,并在调用generator.js时作为参数存储;
index.js:Service插件的入口,@vue/cli-service[8]启动时执行
详细内容请参考Vue Cli插件开发手册[9]
我们将Vue CLI的插件执行分为两种情况:
第一种:未安装插件,添加插件时调用(prompts.js + Generator.js)
第二种:插件已经安装,插件系统启动时(index.js)会执行。 第一次安装过程
与 Babel 的自动安装和添加插件相比,Vue CLI 的插件系统提供了命令行安装方式,变得非常方便。 我们看一下Vue Cli插件系统,看看如何实现一行命令添加插件的功能。
安装过程的执行思路如下:
第一步:从命令行参数解析插件名称,使用 npm (yarn) install vue-cli-plugin-xxx 安装插件,源码位置:@vue/cli/lib/add.js [10]
第二步:require('vue-cli-plugin-xxx/prompts'),并获取用户安装选项结果pluginOptions,源码位置:@vue/cli/lib/invoke.js [11]
步骤3:使用pluginName和pluginOptions作为参数形成Generator[12]对象的实例
第4步:执行generator.generate技术。 此步骤包括三个关键步骤:
1)require(vue-cli-plugin-xxx/generator),获取插件的执行函数;
2)构建GeneratorAPI(即pluginAPI);
3)调用generator.js导入函数。
详细代码:@vue/cli/lib/Generator.js [113]
第五步:将插件的参数添加到vue.config.js文件中。第二次操作流程
插件运行流程由插件系统@vue/cli-service定义[14]。 这里调用插件有两种:
第一类内置插件@vue/cli-service与命令和配置相关,系统功能拆分为多个外部插件,插件系统中默认调用);
第二种项目插件,package.json中定义的npm包名符合插件命名规范)。
插件操作逻辑非常简单:
这两个进程的插件API是不同的。
安装过程:@vue/cli/lib/GeneratorAPI [15]
运行流程:@vue/cli-service/lib/PluginAPI [16] 从三个要素总结出来:
1)安装过程
pluginCore:@vue/cli[17]通过命令行参数获取插件包名,然后安装插件的npm包,并执行promps.js获取用户安装选项结果,然后使用选项结果和生成器.js作为参数构造一个generator,并在调用generator.generate时执行generator.js函数;
pluginAPI:GeneratorAPI[18],提供源码修改、npm包管理、模板文件生成等功能;
插件:由prompts.js和generator.js组成,解决项目中植入某种能力时需要处理的依赖关系。 2)运行流程
pluginCore:@vue/cli-service[19],从package.json中获取项目插件后,与系统的外部插件合并,最后顺序执行;
pluginApI:PluginAPI [20],提供更改 webpack 配置和命令管理的能力;
插件:index.js 文件,在不同的命令下工作。 一个插件系统可以有多种插件类型,插件系统可以通过命令安装插件。 用户在使用插件系统时添加插件也非常方便。
3、插件架构的应用
3.1 应用场景
通过上面的例子,总结一下插件架构的应用场景。
第一种:丰富的pluginAPI场景:代码运行在多个场景中,需要平滑场景间的差异。 (jQuery);
第二种:丰富的插件场景,插件系统,可以预见需求会增加,适合通过更多的插件来简化系统的代码量(Babel)
第三种:丰富的pluginCore和pluginAPI场景。 插件系统本身非常复杂,对开发者的要求极高。 此时复杂的工作都在内核和pluginApi中实现,而大部分简单的编码工作则交给插件方来实现,插件方也可以使用pluginApi快速完成业务开发(Vue CLI ) 3.2 发展方向
通过构建插件标准以及开发过程中积累的插件编程能力,整个公司使用一套插件系统(中台),这意味着我们不必重新发明业务轮子,而团队和企业才能不断积累自己。 插件生态让软件开发可以像汽车等工业制造一样创建标准化的装配线。
发表评论