插件模式的实现

前端开发
2022年12月29日
1275

什么是插件

插件一般是可独立完成某个或一系列功能的模块。一个插件是否引入一定不会影响到系统本身的正常运行(除非它和另一个插件存在依赖关系)。插件在何时被引入,何时被调用都是由系统来调度的。一个系统可以存在多个插件,这些插件可以通过系统预定的方式进行组合。

为什么需要插件

通常,一个系统是需要持续迭代的,在开发之初几乎做不到面面俱到。这就需要我们的系统具备一定的可扩展性,而插件形式的设计模式是我们常常选用的方法。

需要插件模式,我们可以做到:

  1. 为现有的系统提供全新的能力;
  2. 对现在的能力进行增强。

同时还可以:

  1. 插件代码与系统代码解耦,可以独立开发;
  2. 可动态引入并配置;
  3. 通过多个单一职责的插件进行组合,可实现多种复杂的逻辑,实现逻辑在复杂的场景中的复用。

实现一个简单的系统

在实现之前,我们需要明确插件用来帮助你解决的问题是什么。在这里,我们要做一个文本解析器,将以下形式的文本:

markdown
**Hello** \n ~~world~~.

解析成:

markdown
<strong>Hello</strong> <del>world</del>.

系统本身不进行任何解析操作,通过插件的形式来增加系统的功能。明确了问题之后,我们开始着手实现一个简单的具有插件形式的文本解析系统。

创建一个项目

这里我们通过 vite 直接创建一个原生 JavaScript 的项目:

bash
pnpm create vite plugin-design-pattern --template vanilla-ts cd plugin-design-pattern pnpm install # 清理文件 rm -rf public rm -f src/counter.ts src/style.css src/typescript.svg # 新增 Parser.ts 文件 touch src/Parser.ts

srcParser.ts 中增加以下内容:

typescript
export default class Parser { parse (input: string): string { const output = input return output } }

修改 src/main.ts 里面的内容:

typescript
import Parser from './Parser' const input = '**Hello**\n~~world~~.' const parser = new Parser() const output = parser.parse(input) console.log(output)

在终端中运行开发命令:

bash
pnpm run dev

然后打开指定的地址,在浏览器的控制台中可以看到以下输出:

image-20221228155113046.png

如此一来,我们的字符串解析器系统就算是搭建起来了。当然,它的功能还是很简陋,甚至可以说是没有任何功能。接下来我们给 Parser 增加插件功能。

增加插件功能

typescript
// src/Parser.ts export interface ParserPlugin { (input: string): string } export default class Parser { #plugins: ParserPlugin[] = [] get plugins (): ParserPlugin[] { return this.#plugins } use (plugin: ParserPlugin): Parser { this.plugins.push(plugin) return this } parse (input: string): string { let output = input this.plugins.forEach(plugin => { output = plugin(output) }) return output } }

如上面的代码所示,我们增加了一个 #plugins 属性,用来存储被注册的插件,同时增加了 use() 方法来注册插件,最后在 parse() 函数中依次调用 plugin 来完成解析操作。

strongParser

接下来,增加一个 strongParser 插件来完成对 **xxxx** 的解析:

typescript
// src/libs/strongParser.ts import type { ParserPlugin } from '../Parser' const strongParser: ParserPlugin = input => { return input.replace(/\*\*(.*?)\*\*/g, (_, $1) => `<strong>${$1}</strong>`) } export default strongParser

然后在 src/main.ts 中引入并使用:

typescript
import Parser from './Parser' import strongParser from './libs/strongParser' const input = '**Hello**\n~~world~~.' const parser = new Parser() parser.use(strongParser) const output = parser.parse(input) console.log(output)

在浏览器控制台中我们可以看到以下内容,说明我们的解析插件生效了:

image-20221228163108831.png

delParser

strongParser 一样,继续加上 delParser 插件:

typescript
// src/libs/delParser.ts import type { ParserPlugin } from '../Parser' const delParser: ParserPlugin = input => { return input.replace(/~~(.*?)~~/g, (_, $1) => `<del>${$1}</del>`) } export default delParser

然后在 src/main.ts 中引入并使用:

diff
import Parser from './Parser' import strongParser from './libs/strongParser' import delParser from './libs/delParser' const input = '**Hello**\n~~world~~.' const parser = new Parser() parser.use(strongParser) + .use(delParser) const output = parser.parse(input) console.log(output)

在浏览器控制台中可以看到以下内容:

image-20221228163601081.png

一个简单的带有插件功能形式的文本解析系统就完成了。

增强系统能力

我们知道,一个插件他除了可以被注册,在某些特定的情况下,我们应该也可以卸载该插件。并且插件应该具备配置项来实现功能定制的需求。这也就是接下来我们要做的事。

typescript
export interface ParserPluginExecutor { (input: string, options?: ParserPluginOptions): string } export interface ParserPlugin { name: string enable?: boolean executor: ParserPluginExecutor } export type ParserPluginOptions = Record<string, any> interface PluginType extends Required<ParserPlugin> { options?: ParserPluginOptions } export default class Parser { #plugins = new Map<string, PluginType>() get plugins (): Map<string, PluginType> { return this.#plugins } use <T extends ParserPluginOptions>(plugin: ParserPlugin | ParserPluginExecutor, options?: T): Parser { let p: ParserPlugin if (typeof plugin === 'function') { p = { name: plugin.name, enable: true, executor: plugin } } else { p = { ...plugin } } if (typeof p.enable === undefined) { p.enable = true } this.plugins.set(p.name, { ...p, options } as PluginType) return this } /** * 激活插件 */ activate (pluginName: string, options?: ParserPluginOptions): Parser { const plugin = this.plugins.get(pluginName) if (plugin) { plugin.enable = true if (options) { plugin.options = options } } return this } /** * 失活插件 */ deactivate (pluginName: string): Parser { const plugin = this.plugins.get(pluginName) if (plugin) { plugin.enable = false } return this } parse (input: string): string { let output = input this.plugins.forEach(plugin => { if (plugin.enable) { output = plugin.executor(output, plugin.options) } }) return output } }

可以看到,代码发生了比较大的变动。

首先 #plugins 的数据结构改成了 Map,并且对 plugin 的类型也作了一下调整(ParserPlugin | ParserPluginExecutor),同时也调整了 use() 方法注册插件的逻辑。

接着,增加了 activate()deactivate() 方法用于激活和失活插件,同时在 activate() 中也允许用户再次修改插件中的配置项,实现更灵活的功能定制。

最后,在 parse() 方法中对 plugin 调用也变成了先判断 plugin.enable 是否为真,为真则调用 plugin.executor() 进行解析工作,同时注入最新的 plugin.options

如此一来,我们的插件系统也变得更加完善了,接下来需要对两个解析插件作一定的调整。

typescript
// src/libs/strongParser.ts import type { ParserPluginExecutor, ParserPluginOptions } from '../Parser' export interface StrongParserOptions extends ParserPluginOptions { tagName?: string } const strongParser: ParserPluginExecutor = (input, options: StrongParserOptions = {}) => { const tagName = options.tagName ?? 'strong' return input.replace(/\*\*(.*?)\*\*/g, (_, $1) => `<${tagName}>${$1}</${tagName}>`) } export default strongParser

strongParser 里面,我们新增加了配置项,让用户可以指定解析的 tagName

typescript
import type { ParserPlugin, ParserPluginExecutor } from '../Parser' const delParser: ParserPluginExecutor = input => { return input.replace(/~~(.*?)~~/g, (_, $1) => `<del>${$1}</del>`) } export default { name: 'delParser', executor: delParser, enable: false } as ParserPlugin

而在 delParser 里面我们改变了默认导出的内容为 ParserPlugin

接着是对 main.ts 的调整:

typescript
// src/main.ts import Parser from './Parser' import strongParser, { StrongParserOptions } from './libs/strongParser' import delParser from './libs/delParser' const input = '**Hello**\n~~world~~.' const parser = new Parser() parser.use<StrongParserOptions>(strongParser, { tagName: 'b' }) .use(delParser) const output = parser.parse(input) console.log(output) console.log(parser.deactivate(strongParser.name).activate(delParser.name).parse(input))

在浏览器的控制台里可以看到以下信息:

image-20221229110923360.png

上面就是这篇文章的所有内容了。当然,一个插件系统的实现是与业务需求息息相关的,不同的需求,可能会选择不同的插件实现方式。