ikuaitu / vue-fabric-editor

快图设计-基于fabric.js和Vue的开源图片编辑器,可自定义字体、素材、设计模板。fabric.js and Vue based image editor, can customize fonts, materials, design templates.
https://ikuaitu.github.io/doc/#/
MIT License
5.34k stars 966 forks source link

Editor 插件化架构讨论 #205

Closed nihaojob closed 6 months ago

nihaojob commented 1 year ago

插件化架构思考

为什么要插件,fabric.js已经是一个支持发布订阅,可扩展的canvas library。再基于fabric.js封装一个层插件化架构,是否过度设计的一些辩证思考。

  1. 我们希望构建一个可便捷扩展的编辑器,而非底层library或一个零散Vue应用。
  2. 编辑器属于更上层的应用层,有自己独特的生命周期与扩展需求,编辑器应用与libray不在一个分层中,编辑器依赖library。
  3. 生命周期:导入文件前后、插入素材、保存文件前后等操作,我们很多功能模块需要根据这些周期相应的处理。
  4. 扩展需求:编辑器需要天然支持可快速扩展右键菜单和快捷键的功能,以及模块内的API和事件通知。
  5. 基于fabric.js 和 编辑器的插件机制,我对比了一下现有功能模块,可以更好的将功能内聚在插件内,而非组件和功能耦合在一起。例如:
    1. 导入文件前进行字体的加载、画布大小的设置。
    2. 导出文件前隐藏标尺、画布大小调整操作。
    3. 保存文件后恢复标尺、画布大小。
    4. 快捷键、右键菜单可通过配置化的方式导入到个个模块,将功能内聚在功能模块中。

插件功能

插件化应尽可能的简化,便于开发人员快速理解与扩展,但要满足基础的需求:

  1. 插件可收到重要生命周期事件。
  2. 插件可快速扩展快捷键。
  3. 插件可快速扩展右键菜单。
  4. 插件可保留出功能API,与发布订阅自己的插件事件。
  5. 插件可获取原生fabric.js 的canvas 对象。
  6. 插件之间可相互通讯(尽量避免,但需支持)。
  7. 防止插件名称、API、订阅事件冲突。

插件API构想

插件的静态属性:

插件生命周期:

编辑器生命周期:

快捷键扩展事件:

右键菜单扩展事件:

插件伪代码实例

import ExtednPlugin from '@/core/ExtednPlugin';
// 注册插件
class rulerPlugin extends ExtednPlugin {
    // 插件名称
    name = 'ruler'
    // 插件默认配置
    defaultOption = {
        color: 'red',
        size: 0.2
    }
    // 内置事件,可通知其他插件
    events = {
        'createRulerStart': 'CREATE_RULER_START'
    }
    // API 暴露给外部使用
    apis = ['enableRuler', 'disableRuler']

    // 快捷键
    hotkeys = ['crtl+h']

    // 默认实现 ExtednPlugin
    // constructor(canvas, event, option = {}){
        // 自动将外部传入的属性合并
        // this.defaultOption = {defaultOption..., option}
        // 调用初始化方法
        // this.init()
        // 挂载上下文
        // this.ctx = { canvas, event, editor }
    //}

    // 初始化逻辑
    init(){
        // 获取属性
        const { color } = this.defaultOption
        // 通过上下文获取 canvas和event对象
        const { canvas, editor } = this
        canvas.on('', () => {
            // 发布事件
            this.editor.emit(this.events.createRulerStart)
            // 调用其他插件方法
            editor.getPlugin('otherPluginName')?.otherApiName()
        })
    }

    // 销毁逻辑
    destroy(){
      canvas.off('')
    }

   // 右键菜单的扩展 支持子菜单
   contextMenu() {
     // 判断条件 返回菜单与事件,可
        if(true){
         return [{
            text: '菜单',
            command: this._commandHandler
          },
          {
            text: '父菜单',
            child:[{
                text: '子菜单',
                command: this._commandHandler
              }]
          }]     
      }
    }

    // 快捷键功能函数
    _commandHandler(){
      console.log('快捷键事件')
    }

    // 生命周期-保存前 隐藏标尺
    hookSaveBefore(){
        this._hideGuideline()
    }

    // 生命周期-保存后 展示标尺
    hookSaveAfter(){
        this._showGuideline()
    }

    // 快捷键回调
    hotkeyEvent(eventName, event){
        // 快捷键 显示隐藏标尺
        if(event = 'crtl+h'){
            this.status = !this.status
            this.status ? this._showGuideline() : this._hideGuideline
        }
    }

    // 暴漏给外部的API  激活标尺
    enable(){

    }   

    // 暴漏给外部的API  关闭标尺
    disable(){

    }

    // 私有方法  隐藏标尺
    _hideGuideline(){}

    // 私有方法  显示标尺
    _showGuideline(){}

}

// 初始化 fabric画布
const canvas = new fabric.Canvas('canvas')

// 引入插件 可选传入自定义配置
Editor.use(EditorWorkspace).use(CanvasRuler, { color: 'red', size: 0.1 })

// 初始化编辑器
const editor = new Editor(canvas)

// 调用插件方法 1
editor.enableRuler()
// 调用插件方法 2
const ruler = editor.getPlugin('ruler')
rule.enableRuler()

// 事件订阅
const ruler = editor.getPlugin('ruler')
editor.on(ruler.event.createRulerStart, () => {
  // do something
})

编辑器整体架构

按照插件化重构后,将分层更加清晰,将分为如下几个部分:

  1. Edirot:提供插件调度、安装机制。
  2. plugin:约束插件规则,提供功能插件。
  3. Components:业务组件,调用Edirot对象提供的API方法,可订阅编辑器内的事件。
  4. commonUse:组件需高频使用的功能,如当前是单选、多选、选中ID等功能,属于业务功能,单独将组件需要的功能封装在通用hook中,供components使用。

模块关系如下图:

Vue-Fabric-Editor
nihaojob commented 1 year ago

我遇到的麻烦

我目前可以根据这套规则来实现插件化架构,但是不确定要不要引入 tapable,如果用从基础的发布订阅来看,events可以满足我们大部分场景的需求。

但是只有1个功能无法满足,例如 我们希望通过插件机制来实现自动加载字体, 插入JSON前,字体插件会JSON遍历出字体并进行加载,并阻塞钩子的进程,等字体文件加载完成后,再执行其他的钩子事件

events并没有提供这样的功能,而且 tapable 的AsyncHook可以更容易的实现,但是引入 tapable 可能会增加大家的理解成本。

Qiu-Jun commented 1 year ago

我遇到的麻烦

我目前可以根据这套规则来实现插件化架构,但是不确定要不要引入 tapable,如果用从基础的发布订阅来看,events可以满足我们大部分场景的需求。

但是只有1个功能无法满足,例如 我们希望通过插件机制来实现自动加载字体, 插入JSON前,字体插件会JSON遍历出字体并进行加载,并阻塞钩子的进程,等字体文件加载完成后,再执行其他的钩子事件

events并没有提供这样的功能,而且 tapable 的AsyncHook可以更容易的实现,但是引入 tapable 可能会增加大家的理解成本。

我遇到的麻烦

我目前可以根据这套规则来实现插件化架构,但是不确定要不要引入 tapable,如果用从基础的发布订阅来看,events可以满足我们大部分场景的需求。

但是只有1个功能无法满足,例如 我们希望通过插件机制来实现自动加载字体, 插入JSON前,字体插件会JSON遍历出字体并进行加载,并阻塞钩子的进程,等字体文件加载完成后,再执行其他的钩子事件

events并没有提供这样的功能,而且 tapable 的AsyncHook可以更容易的实现,但是引入 tapable 可能会增加大家的理解成本。

还好吧,如果非开发的话,并不关注理解第三方库。比如很多人用vuedraggable,但很多人都不知道他是用了sortable.js。一般这种情况,我考虑的是第三方包的稳定性和大小

nihaojob commented 1 year ago

我遇到的麻烦

我目前可以根据这套规则来实现插件化架构,但是不确定要不要引入 tapable,如果用从基础的发布订阅来看,events可以满足我们大部分场景的需求。 但是只有1个功能无法满足,例如 我们希望通过插件机制来实现自动加载字体, 插入JSON前,字体插件会JSON遍历出字体并进行加载,并阻塞钩子的进程,等字体文件加载完成后,再执行其他的钩子事件events并没有提供这样的功能,而且 tapable 的AsyncHook可以更容易的实现,但是引入 tapable 可能会增加大家的理解成本。

我遇到的麻烦

我目前可以根据这套规则来实现插件化架构,但是不确定要不要引入 tapable,如果用从基础的发布订阅来看,events可以满足我们大部分场景的需求。 但是只有1个功能无法满足,例如 我们希望通过插件机制来实现自动加载字体, 插入JSON前,字体插件会JSON遍历出字体并进行加载,并阻塞钩子的进程,等字体文件加载完成后,再执行其他的钩子事件events并没有提供这样的功能,而且 tapable 的AsyncHook可以更容易的实现,但是引入 tapable 可能会增加大家的理解成本。

还好吧,如果非开发的话,并不关注理解第三方库。比如很多人用vuedraggable,但很多人都不知道他是用了sortable.js。一般这种情况,我考虑的是第三方包的稳定性和大小

Get,谢谢June的讨论。

nihaojob commented 1 year ago

https://github.com/nihaojob/vue-fabric-editor/commits/refactor-plugin 插件化进展同步 已支持快捷、API、事件相应,已改造对齐线、拖拽功能。

nihaojob commented 1 year ago

插件机制

liaojunhao commented 1 year ago

你的插件话,会像 polotno 那样的配置嘛?

1123612483 commented 1 year ago

能否给Editor插件单独打包?这样方便其他框架或者应用引用