如何去设计一个可插拔的平台

4,417 阅读3分钟

简介

随着互联网的不断发展,应用越来越复杂,面对庞大复杂的代码模块,一不小心就牵一发而动全身,给开发者们带来了巨大的痛苦。然后有一种设计模式-插件系统 应运而生

插件模式的特点

  • 可插拔,对平台的运行不造成任何影响
  • 平台暴露出特定的api在规定的消息通知hook钩子中运行
  • 平台核心足够的稳定安全可靠

一个好的插件平台,插件的多少和插件自身的稳定性应该是和平台隔离的同时平台自身核心要运行稳定,才能不影响插件的运行。简单的说就是平台实现hook钩子消息通信模式去通知插件的执行。插件只能在平台规定的hook钩子去执行对应的暴露的api,如下图(blog.csdn.net/qiwoo_weekl…

image.png

代码实现

首先实现一个平台,平台的功能的每秒执行一次注册的任务

class Pingtai {
  constructor({ plugins }) {
    this.taskList = []
    this.timer = null
  }

  init() {
    this.run()
  }

  addTask(task) {
    this.taskList.push(task)
  }

  run() {
    this.stop()
    this.timer = setTimeout(() => {
      this.taskList.forEach((task) => {
        task.log(`${moment().format('YYYY-MM-DD HH:mm:ss')}`)
      })
      this.run()
    }, 1000)
  }

  stop() {
    clearTimeout(this.timer)
  }
}

主程序

const sys = new Pingtai()
function Main(props) {
  const [strMap, setStrMap] = useState({})
  const handleAddTask = useCallback(() => {
    const name = 'taskName' + Date.now()
    sys.addTask({
      name,
      log(str) {
        setStrMap((old) => {
          return {
            ...old,
            [name]: str,
          }
        })
      },
    })
    sys.run()
  }, [])

  return (
    <div>
      <Button type="primary" onClick={handleAddTask}>
        添加任务
      </Button>
      <p>这是打印的内容:</p>
      {Object.keys(strMap).map((key) => (
        <div key={key}>
          <span style={{ color: 'orange' }}>{`${key} => `}</span>
          {strMap[key]}
        </div>
      ))}
    </div>
  )
}

嗯,很简单,就是主程序页面有一个按钮,点击添加任务,然后平台开始每秒打印一个时间显示在页面上,平台的功能很简单,也没啥问题,那么怎么让它支持插件呢?

对应上面我们的图和说明,那么我们来实现插件的注册和消息hooks通信,改造一下Pintai这个class

class Pingtai {
  constructor({ plugins }) {
    this.plugins = []
    this.taskList = []
    this.timer = null
    plugins.forEach((plugin) => plugin.install(this))
  }

  init() {
    this.runHook('init')
    this.run()
  }

  runHook(name, ...args) {
    this.plugins.forEach((plugin) => {
      if (plugin.name === name) {
        plugin.run(this, ...args)
      }
    })
  }

  addTask(task) {
    this.taskList.push(task)
    this.runHook('addTask', task)
  }

  run() {
    this.stop()
    this.timer = setTimeout(() => {
      this.taskList.forEach((task) => {
        task.log(`${moment().format('YYYY-MM-DD HH:mm:ss')}`)
      })
      this.run()
    }, 1000)
  }

  stop() {
    clearTimeout(this.timer)
  }
}

这里的核心是runHook这个方法,这是一个消息通信,告诉我们要去执行哪一个plugin了,下面我们来实现一个简单的添加任务后提示的Plugin类

class AddPlugin {
  constructor() {
    this.name = '添加提示插件'
  }

  install(sys) {
    sys.plugins.push({
      name: 'addTask',
      run: this.run,
    })
  }

  run(sys, task) {
    task.log(this.name + ' <=> 添加了一个东西')
    message.success('添加任务成功')
  }
}

这个类的核心是install注册插件和run方法,install是告诉我们,插件应该运行到哪一个hook钩子上,而run里面的第一个参数sys就是平台暴露给插件的api,当然你可以让它的权限变得更小一些,比如只有taskList编辑和访问权限

那么主程序的第一行也要稍微改一下,让插件注册进来,这样我们才能看到效果

// old
const sys = new Pingtai()
// new
const sys = new Pingtai({
  plugins: [new AddPlugin()],
})
sys.init()

然后我们按照上面的说法来理清一下这里插件实现的逻辑

  1. 插件掉用install方法注册到平台上
  2. 当平台执行addTask方法时候会执行‘addTask’的hook钩子,然后这个钩子回去找到对应注册的插件去执行插件
  3. 插件执行成功,而平台原来的addTask依旧不受影响正常执行

结语

到这里我相信你应该对可插拔的平台设计有了一定的概念;我建议试试按照这个思路去自己实现一下浏览器插件,webpack插件会让你对可插拔的系统设计有更深的体会