🧐不会设计插件系统?来看看Pictode的插件系统吧!✨

  • 🧑‍ 技术一般,我搞前端
  • 🎨 Pictode最方便集成的绘图编辑器
  • 🤗 目前Pictode正在寻求落地业务和开源帮助,欢迎大佬提交IssuesPR

体验地址

你用过的插件

你知道Vue是一个负责页面生成和数据交互JS框架,通过vue.use()的方式可以轻松给vue添加各种功能,例如:你可以通过加入vue-router来增强路由能力、或者添加pinia来支持数据共享等。

Vuemain.js 文件中,你会发现像 vue-routervue-i18n 等第三方库是如何被集成的。

Vitevite.config.js中,你会发现像下面这样的配置。

Webpackwebpack.config.js中,你会发现像下面这样的配置。

当然如果你了解Pictode那么,也会在Pictode中发现类似的用法。

什么是插件?

想象你在写一个软件,比如一个文本编辑器。你的编辑器很基础,只有最基本的文本输入和保存功能。但是你知道,有很多用户想要更高级的功能,比如拼写检查、代码高亮显示或者版本控制。为了让你的编辑器不至于变得臃肿,你可以使用插件系统。

插件就像是一组小工具箱,每个工具箱都包含一个特定的功能。当用户需要某个功能时,他们可以选择安装相应的插件,就像你在工具箱里挑选需要的工具一样。这样,用户可以根据自己的需求定制编辑器,而不必把所有可能用到的功能都塞到一个庞大的软件里。

在程序员的角度,插件就是一种模块化的设计方式,让你的软件更容易扩展和维护。你可以定义一些接口(API),让插件开发者知道如何与你的软件进行交互。这样一来,其他程序员就可以编写插件,扩展你的软件的功能,而不必深入了解你软件的全部实现细节。

看到这里你肯定也想知道如何给自己的系统增加插件机制吧!

插件约定

插件的核心在于提供给开发者的一组规范化接口,为软件与插件之间提供有效交互。确立这组接口的内容是实现插件的首要步骤。

需要明确的是,这组插件接口并非供外部直接调用,而是为系统与插件之间的内部交互而设计。

Pictode 的插件接口为例,我们进行如下规范制定:

ts 复制代码
export interface Plugin {
  name: string; // 插件名称,也是插件的唯一标识
  install(app: App, ...options: any[]): any; // 插件安装时调用
  destroy(): void; // 插件销毁时调用
  enable?(): void; // 插件启用时调用
  disable?(): void; // 插件禁用时调用
  isEnabled?(): boolean; // 检查插件是否启用
}

主应用插件管理

插件安装

我们使用Map来管理注册的插件,因为所有的插件都有一个唯一的标识:name

ts 复制代码
import { Plugin } from './types';

export class App {
  private installedPlugins: Map<string, Plugin> = new Map();
}

在接口约定时提到过,这些方法并不是供外部调用的,而是为主系统调用的。通过 app.use(plugin) 安装插件,插件的安装逻辑包括将插件记录到 Map 中,并调用我们约定的插件方法 install()。这一设计使得插件的调用和管理更加清晰,同时确保了内部接口的安全性。

ts 复制代码
import { Plugin } from './types';

export class App {
  private installedPlugins: Map<string, Plugin> = new Map();

  public use(plugin: Plugin, ...options: any[]): this {
    if (!this.installedPlugins.has(plugin.name)) {
      this.installedPlugins.set(plugin.name, plugin);
      plugin.install(this, ...options);
    }
    return this;
  }
}

主应用获取插件

前面我们说过通过Map管理插件,所以获取插件的也会极其简单。

ts 复制代码
export class App {
  private installedPlugins: Map<string, Plugin> = new Map();
  // ...

  public getPlugin<T extends Plugin>(pluginName: string): T | undefined {
    return this.installedPlugins.get(pluginName) as T;
  }

  public getPlugins<T extends Plugin[]>(pluginNames: string[]): T | undefined {
    return pluginNames.map((pluginName) => this.getPlugin(pluginName)) as T;
  }
}

控制应用的启用与禁用

应用的启用和禁用核心逻辑仍然通过调用插件的 enable()disable() 方法来实现。这种设计使得应用状态的管理更为直观和可控。

ts 复制代码
export class App {
  public enablePlugin(plugins: string | string[]): this {
    if (!Array.isArray(plugins)) {
      plugins = [plugins];
    }
    const aboutToChangePlugins = this.getPlugins(plugins);
    aboutToChangePlugins?.forEach((plugin) => plugin?.enable?.());
    return this;
  }

  public disablePlugin(plugins: string | string[]): this {
    if (!Array.isArray(plugins)) {
      plugins = [plugins];
    }
    const aboutToChangePlugins = this.getPlugins(plugins);
    aboutToChangePlugins?.forEach((plugin) => plugin?.disable?.());
    return this;
  }
}

插件卸载

插件卸载的核心逻辑包括从 Map 中移除插件,并在移除之前调用插件的 destroy() 方法。这一流程确保了插件的清理和资源释放。

ts 复制代码
import { Plugin } from './types';

export class App {
  private installedPlugins: Map<string, Plugin> = new Map();

  public destroyPlugins(plugins: string | string[]): this {
    if (!Array.isArray(plugins)) {
      plugins = [plugins];
    }
    const aboutToChangePlugins = this.getPlugins(plugins);
    aboutToChangePlugins?.forEach((plugin) => plugin?.destroy());
    return this;
  }
}

插件实现

这里给出一个示例插件,然后再将该插件注册到主应用。

ts 复制代码
class TestPlugin implements Plugin {
  public name: string = "TestPlugin";

  private _enable: boolean = false;

  public install(...options: any[]) {
    console.log(`${this.name}安装了`);
    this._enable = true;
  }

  public disable(): void {
    console.log(`${this.name}被禁用了`);
    this._enable = false;
  }

  public enable(): void {
    console.log(`${this.name}启用了`);
    this._enable = true;
  }

  public destroy(): void {
    console.log(`${this.name}要被卸载了`);
  }

  public isEnabled(): boolean {
    console.log(`${this.name}目前时是${this._enable}`);
    return this._enable;
  }

  // 插件的功能方法
  public testFunc(): void {
    console.log(`${this.name}的功能方法`);
  }
}

你写完上面的代码基本上实现了插件的核心逻辑,插件方法的调用方式如下:

ts 复制代码
const app = new App();

const testPlugin = new TestPlugin();

app.use(testPlugin);

插件的功能方法调用:

ts 复制代码
app.getPlugin("TestPlugin")?.testFunc();

🎉🎉🎉恭喜你喜提一个插件系统,你的应用可以轻松接收其他开发者的贡献了。

更便捷的使用方式

上述插件功能的调用方式是首先获取插件实例,然后通过实例调用插件方法。虽然这是一种合理的调用方式,但并不是最便捷的使用方法。

为提升使用体验,理想的调用方式应该是在插件安装后,可以直接通过 app 调用插件提供的功能方法。这样,开发者可以更轻松、直观地使用插件功能,提高了整体的可用性。

好了,现在是时候规划一下项目的目录情况了,为了提高代码的可读性我们规划代码目录如下:

我们将插件专门分配到一个名为 plugin-test 的目录,这个目录将包含插件的所有内容。

  • index.ts:包含插件的主要实现代码。
  • types.ts:用于定义插件中使用的类型数据。
  • methods.ts:是一个文件,将插件的功能方法挂载到主应用。

types.ts中给App声明插件的功能函数

ts 复制代码
import { App } from "../app";

declare module "../app" {
  export interface App {
    testFunc(): void;
  }
}

methods.ts中就可以自然而然的挂载testFunc方法到App的原型了

ts 复制代码
import { App } from "../app";
import { TestPlugin } from "./index";

App.prototype.testFunc = function () {
  const testPlugin = this.getPlugin("TestPlugin");
  if (!testPlugin) {
    return;
  }
  (testPlugin as TestPlugin).testFunc();
};

这时再从主应用使用插件功能方法时就会得到提示:

最后

🎉🎉🎉 恭喜,你成功地搭建了一个可用的插件系统。通过上述方法,你轻松实现了系统的扩展,而且无需修改主要逻辑即可实现这一扩展。这个插件系统为你的应用带来了更大的灵活性和可维护性。希望这个系统为你的项目带来更多的可能性和便利。

体验地址

如果你觉得插件机制对你在开发中有所帮助,麻烦多点赞评论收藏😊

如果插件机制对你实现某些业务有所启发,麻烦多点赞评论收藏😊

如果...,麻烦多点赞评论收藏😊

如果大家有其他弹窗方案,欢迎留言交流哦!

相关推荐
ok!ko1 小时前
设计模式之原型模式(通俗易懂--代码辅助理解【Java版】)
java·设计模式·原型模式
正小安1 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
拉里小猪的迷弟2 小时前
设计模式-创建型-常用:单例模式、工厂模式、建造者模式
单例模式·设计模式·建造者模式·工厂模式
_.Switch3 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光3 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   3 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   3 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web3 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常3 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇4 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器