插件模式的实现
什么是插件
插件一般是可独立完成某个或一系列功能的模块。一个插件是否引入一定不会影响到系统本身的正常运行(除非它和另一个插件存在依赖关系)。插件在何时被引入,何时被调用都是由系统来调度的。一个系统可以存在多个插件,这些插件可以通过系统预定的方式进行组合。
为什么需要插件
通常,一个系统是需要持续迭代的,在开发之初几乎做不到面面俱到。这就需要我们的系统具备一定的可扩展性,而插件形式的设计模式是我们常常选用的方法。
需要插件模式,我们可以做到:
- 为现有的系统提供全新的能力;
- 对现在的能力进行增强。
同时还可以:
- 插件代码与系统代码解耦,可以独立开发;
- 可动态引入并配置;
- 通过多个单一职责的插件进行组合,可实现多种复杂的逻辑,实现逻辑在复杂的场景中的复用。
实现一个简单的系统
在实现之前,我们需要明确插件用来帮助你解决的问题是什么。在这里,我们要做一个文本解析器,将以下形式的文本:
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
然后打开指定的地址,在浏览器的控制台中可以看到以下输出:
如此一来,我们的字符串解析器系统就算是搭建起来了。当然,它的功能还是很简陋,甚至可以说是没有任何功能。接下来我们给 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)
在浏览器控制台中我们可以看到以下内容,说明我们的解析插件生效了:
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)
在浏览器控制台中可以看到以下内容:
一个简单的带有插件功能形式的文本解析系统就完成了。
增强系统能力
我们知道,一个插件他除了可以被注册,在某些特定的情况下,我们应该也可以卸载该插件。并且插件应该具备配置项来实现功能定制的需求。这也就是接下来我们要做的事。
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))
在浏览器的控制台里可以看到以下信息:
上面就是这篇文章的所有内容了。当然,一个插件系统的实现是与业务需求息息相关的,不同的需求,可能会选择不同的插件实现方式。