-
Notifications
You must be signed in to change notification settings - Fork 25
Description
重点参考 从VS Code看优秀插件系统的设计思路
实现一个插件系统需要做到这几点
定义插件接口:首先,需要定义插件与主程序之间的接口,包括插件的初始化方法、执行方法、事件监听等。这样可以确保插件与主程序之间的交互是规范的。
插件的加载方式:确定插件的加载形式,比如通过 npm 包,通过文件,通过 git 仓库等等,好的插件的组织形式使整个系统足够灵活。设计好插件的加载时机,比如惰性加载,按依赖加载等,好的加载时机把控,可以让大型系统的性能得到提升。
插件注册和管理:主程序需要提供插件注册和管理的功能,用于管理已加载的插件列表。当插件加载完成后,将其注册到主程序中,这样主程序就可以调用插件的能力。
事件通信机制:主程序和插件之间需要建立事件通信机制,以便在需要的时候进行交互。可以使用自定义事件、发布订阅模式或观察者模式等方式来实现事件的监听和触发。
插件配置:可以为插件提供一些配置选项,使得插件的行为可以根据用户需求进行定制化。安全性考虑:插件系统涉及动态加载代码,因此安全性是一个重要考虑因素。确保只加载受信任的插件,并对插件的代码进行安全性检查,以防止潜在的恶意代码注入。
业界关于插件设计模式有很多种,但是经过归纳总结,我们认为最常用的主要是以下三种插件模式:管道式、洋葱式和事件式
管道式插件
管道式插件(Pipeline Plugin)是常用的插件设计模式之一。它的主要目标是将处理流程分解为一系列独立的步骤,并允许开发者通过插件来扩展或修改这些步骤,从而实现更灵活和可维护的代码。
在管道式插件中,处理流程被表示为一条管道,数据从管道的一端输入,经过一系列步骤进行处理,最终在管道的另一端输出。每个处理步骤都由一个插件来实现,该插件负责执行特定的任务,并将处理后的数据传递给下一个插件。
管道式插件的优点包括:
- 解耦性强,管道的每个环节之间相互独立,只处理特定的问题,可单独开发、测试和维护。
- 在输入输出标准化的情况,可以灵活组合插件,根据需求动态改变管道结构,实现数据处理流程的定制化和扩展性。
管道式插件的局限性包括:
管道的设计需要考虑插件之间的数据密切性和执行顺序,可能会增加开发难度和设计复杂度。如果不合理的设计管道流程,可能会导致数据的不完整性和不准确性,对系统造成影响。
- 管道的设计需要考虑插件之间的数据密切性和执行顺序,可能会增加开发难度和设计复杂度。
- 如果不合理的设计管道流程,可能会导致数据的不完整性和不准确性,对系统造成影响。
前端工具 Gulp就是使用管道式插件
/*创建一个名为css的任务将src目录下的所有的less样式文件转成css,随后压缩并合并成一个名为app.css的文件,对这个文件加上md5版本签名,
生成到build/css路径下,并生成映射文件放到src/css*/
gulp.task('css', ['cleanWatchBuild', 'txtCopy'], function() {
return gulp.src(['src/css/**/*.less'])
.pipe(less())
.pipe(minifyCss())
.pipe(concat('app.css'))
.pipe(rev())
.pipe(gulp.dest('build/css'))
.pipe(rev.manifest({
base: 'src/**',
merge: true
}))
.pipe(gulp.dest("src/css"));
});洋葱式插件
洋葱式插件(Onion Architecture Plugin)也是常用的一类插件设计模式,它是从洋葱架构(Onion Architecture)演化而来的。
洋葱架构是一种用于构建可维护、灵活且可测试的应用程序的软件架构模式。在洋葱架构中,应用程序的核心逻辑位于内部,而外部依赖(如数据库、UI 等)则位于外部。洋葱架构通过层层包裹的方式来表示不同的关注点,类似于洋葱的结构,因此得名。
洋葱式插件将洋葱架构与插件系统相结合,以实现可插拔的、可扩展的应用程序。在这种模式下,插件可以被动态地加载和卸载,而不会影响应用程序的核心逻辑,从而使得应用程序更具灵活性和可维护性。
洋葱式插件的主要优点包括:
洋葱架构的层次分明, 洋葱式插件保留了洋葱架构的内部核心和外部依赖的层次结构。插件通常被视为外部依赖,而宿主应用程序的核心逻辑位于内部。
具备良好的重用性,洋葱架构中的各个层次和组件都可以独立地被重复利用,可以在不同的项目和场景中进行复用,提高了代码的可重用性。
举例:比如 Koa 中很多中间件具备良好的复用性(如 koa-session),多个项目均可以引入使用.
以 Koa 为例,洋葱式插件运行阶段会经过3个环节:
洋葱式插件允许插件在请求处理过程中先后执行,可以按需添加或删除插件,并且每个插件可以根据需要决定是否继续执行或终止执行,这使得洋葱式插件非常适合承当服务拦截器的角色。
与管道式插件相比,洋葱式插件对数据干涉的时机更加完备,不仅仅可以对自身的数据输入环节进行干涉和处理,在数据输出环节还能对其他插件的输出进行干涉和处理。
洋葱式插件的局限性包括:
相比管道式插件复杂性更高,洋葱式插件模式需要插件之间的协作和数据传递,即处理输入流和处理输出流,在处理复杂逻辑时可能导致代码变得复杂难以理解。
洋葱架构中的层次嵌套可能会增加函数调用的次数和层次,进而导致一定的性能损耗。
事件式插件
事件式插件(Event-based Plugin)是插件设计模式中最灵活的一种,它基于事件驱动编程。在事件式插件中,主程序(或宿主应用程序)通过触发事件来通知插件执行相应的操作。插件系统允许插件注册特定事件的监听器,并在相应事件被触发时执行相应的功能。
事件式插件的主要优点包括:
灵活度高,应用场景广。
运行方式多样,事件类型多,十分灵活,能适应于各种场景。
如 Webpack 当中,其通过 Tapable 实现了一种发布订阅者模式的插件机制,提供同步/异步钩子,串行/并行钩子,按照执行类型分为瀑布/保险/循环钩子,并且可以进行灵活组合来满足 Webpack 编译打包的所有功能扩展需求。
执行时机异步化,提升整体性能。
因为事件式插件是基于发布订阅实现的,执行的时机异步化,非阻塞式地执行代码,有利于提升整体的性能。
VS Code 在插件系统中,应对几十个插件的应用,也不会有太大的性能问题,不仅仅是因为事件触发之后才会初始化插件,也是得益于事件式插件带有的益处。
可插拔式的设计。
事件式插件还有一个重要的特点,可插拔式的设计,使插件在添加或删除的时候,都不会影响主流程的执行。
如 Chrome 浏览器支持使用事件式插件的方式来扩展其功能,但是不会影响原有的浏览器功能的执行。
事件式插件的主要问题包括:
事件式插件虽然在插件注册和执行上具备非常大的灵活性,但是相应架构设计上会比管道式和洋葱式更为复杂,从而更容易引入未知问题。
事件式插件系统完全可以覆盖管道式插件系统的职能(使用串行的事件模式达到管道的效果),但是如果明确一个管道式的需求,则更建议使用管道式插件系统,因为管道式插件系统更为简单。
webpack
// webpack 插件 https://webpack.js.org/contribute/writing-a-plugin/#root
// A JavaScript
class.class MyExampleWebpackPlugin {// Define `apply` as its prototype method which is supplied with compiler as its argument
apply(compiler) {// Specify the event hook to attach to
compiler.hooks.emit.tapAsync('MyExampleWebpackPlugin',(compilation, callback) => {
console.log('This is an example plugin!');
console.log('Here’s the `compilation` object which represents a single build of assets:',
compilation
);// Manipulate the build using the plugin API provided by webpack
compilation.addModule(/* ... */);callback();
});
}
}vscode
// vscode
{
// 定义激活的事件
"activationEvents": [
"onCommand:extension.helloWorld"
],
// 定义贡献点
"contributes": {
"commands": [
{
"command": "extension.helloWorld",
"title": "Preview Class Diagram",
"category": "TS2PLANTUML"
}
],
"menus": {
"explorer/context": [
{
"command": "extension.helloWorld1",
"when": "resourceLangId == typescript"
}
],
"commandPalette": [
{
"command": "extension.helloWorld3",
"when": "resourceLangId == typescript"
}
]
}
}
// 具体实现
import * as vscode from 'vscode';
// 一旦你的插件激活,vscode会立刻调用下述方法
export function activate(context: vscode.ExtensionContext) {
// 用console输出诊断信息(console.log)和错误(console.error)
// 下面的代码只会在你的插件激活时执行一次
console.log('Congratulations, your extension "my-first-extension" is now active!');
// 入口命令已经在package.json文件中定义好了,现在调用registerCommand方法
// registerCommand中的参数必须与package.json中的command保持一致
let disposable = vscode.commands.registerCommand('extension.helloWorld', () => {
// 把你的代码写在这里,每次命令执行时都会调用这里的代码
// ...
// 给用户显示一个消息提示
vscode.window.showInformationMessage('Hello World!');
});
context.subscriptions.push(disposable);
}事件式插件的应用
事件式插件在前端领域有着广泛的应用,比如构建工具 Webpack,以及知名代码编辑器 VS Code,这里以 VS Code 为例来讲述一下事件式插件的运行原理。
这里主要研究客户端的插件系统运行流程,web 端类似。
整体的运行流程如下:





