Skip to content

前端领域-常见插件设计模式 #115

@pfan123

Description

@pfan123

重点参考 从VS Code看优秀插件系统的设计思路

实现一个插件系统需要做到这几点
定义插件接口:首先,需要定义插件与主程序之间的接口,包括插件的初始化方法、执行方法、事件监听等。这样可以确保插件与主程序之间的交互是规范的。
插件的加载方式:确定插件的加载形式,比如通过 npm 包,通过文件,通过 git 仓库等等,好的插件的组织形式使整个系统足够灵活。设计好插件的加载时机,比如惰性加载,按依赖加载等,好的加载时机把控,可以让大型系统的性能得到提升。
插件注册和管理:主程序需要提供插件注册和管理的功能,用于管理已加载的插件列表。当插件加载完成后,将其注册到主程序中,这样主程序就可以调用插件的能力。
事件通信机制:主程序和插件之间需要建立事件通信机制,以便在需要的时候进行交互。可以使用自定义事件、发布订阅模式或观察者模式等方式来实现事件的监听和触发。
插件配置:可以为插件提供一些配置选项,使得插件的行为可以根据用户需求进行定制化。安全性考虑:插件系统涉及动态加载代码,因此安全性是一个重要考虑因素。确保只加载受信任的插件,并对插件的代码进行安全性检查,以防止潜在的恶意代码注入。

业界关于插件设计模式有很多种,但是经过归纳总结,我们认为最常用的主要是以下三种插件模式:管道式、洋葱式事件式

管道式插件

管道式插件(Pipeline Plugin)是常用的插件设计模式之一。它的主要目标是将处理流程分解为一系列独立的步骤,并允许开发者通过插件来扩展或修改这些步骤,从而实现更灵活和可维护的代码。

image.png

在管道式插件中,处理流程被表示为一条管道,数据从管道的一端输入,经过一系列步骤进行处理,最终在管道的另一端输出。每个处理步骤都由一个插件来实现,该插件负责执行特定的任务,并将处理后的数据传递给下一个插件。

管道式插件的优点包括:

  • 解耦性强,管道的每个环节之间相互独立,只处理特定的问题,可单独开发、测试和维护。
  • 在输入输出标准化的情况,可以灵活组合插件,根据需求动态改变管道结构,实现数据处理流程的定制化和扩展性。

管道式插件的局限性包括:

管道的设计需要考虑插件之间的数据密切性和执行顺序,可能会增加开发难度和设计复杂度。如果不合理的设计管道流程,可能会导致数据的不完整性和不准确性,对系统造成影响。

  • 管道的设计需要考虑插件之间的数据密切性和执行顺序,可能会增加开发难度和设计复杂度。
  • 如果不合理的设计管道流程,可能会导致数据的不完整性和不准确性,对系统造成影响。

前端工具 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 等)则位于外部。洋葱架构通过层层包裹的方式来表示不同的关注点,类似于洋葱的结构,因此得名。

img

洋葱式插件将洋葱架构与插件系统相结合,以实现可插拔的、可扩展的应用程序。在这种模式下,插件可以被动态地加载和卸载,而不会影响应用程序的核心逻辑,从而使得应用程序更具灵活性和可维护性。

洋葱式插件的主要优点包括:

洋葱架构的层次分明, 洋葱式插件保留了洋葱架构的内部核心和外部依赖的层次结构。插件通常被视为外部依赖,而宿主应用程序的核心逻辑位于内部。

具备良好的重用性,洋葱架构中的各个层次和组件都可以独立地被重复利用,可以在不同的项目和场景中进行复用,提高了代码的可重用性。

举例:比如 Koa 中很多中间件具备良好的复用性(如 koa-session),多个项目均可以引入使用.

以 Koa 为例,洋葱式插件运行阶段会经过3个环节:

img

洋葱式插件允许插件在请求处理过程中先后执行,可以按需添加或删除插件,并且每个插件可以根据需要决定是否继续执行或终止执行,这使得洋葱式插件非常适合承当服务拦截器的角色。

与管道式插件相比,洋葱式插件对数据干涉的时机更加完备,不仅仅可以对自身的数据输入环节进行干涉和处理,在数据输出环节还能对其他插件的输出进行干涉和处理。

洋葱式插件的局限性包括:

相比管道式插件复杂性更高,洋葱式插件模式需要插件之间的协作和数据传递,即处理输入流和处理输出流,在处理复杂逻辑时可能导致代码变得复杂难以理解。

洋葱架构中的层次嵌套可能会增加函数调用的次数和层次,进而导致一定的性能损耗。

事件式插件

img

事件式插件(Event-based Plugin)是插件设计模式中最灵活的一种,它基于事件驱动编程。在事件式插件中,主程序(或宿主应用程序)通过触发事件来通知插件执行相应的操作。插件系统允许插件注册特定事件的监听器,并在相应事件被触发时执行相应的功能。

事件式插件的主要优点包括:

灵活度高,应用场景广。

运行方式多样,事件类型多,十分灵活,能适应于各种场景。

如 Webpack 当中,其通过 Tapable 实现了一种发布订阅者模式的插件机制,提供同步/异步钩子,串行/并行钩子,按照执行类型分为瀑布/保险/循环钩子,并且可以进行灵活组合来满足 Webpack 编译打包的所有功能扩展需求。

image.png

执行时机异步化,提升整体性能。

因为事件式插件是基于发布订阅实现的,执行的时机异步化,非阻塞式地执行代码,有利于提升整体的性能。

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 端类似。

整体的运行流程如下:

image.png

参考

vscode 插件开发——开发第一个vscode插件

插件化设计模式在前端领域的应用

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions