V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
nihaojob
V2EX  ›  程序员

开源图片编辑器的插件化架构

  •  
  •   nihaojob · 39 天前 · 983 次点击
    这是一个创建于 39 天前的主题,其中的信息可能已经有所发展或是发生改变。

    大家好,我是开源图片编辑器的作者,在开发图片编辑器的过程中,因为一些功能无法扩展,出现过一次较大的重构,将整个编辑器改为了插件化的架构,经历过这次重构,规范了编辑器功能的扩展方式,解决了项目里很多重要的问题。

    如果你也在做类似的项目,或者对图片编辑器架构比较感兴趣,希望我的经验能给你一点点参考。

    大纲

    1. 项目介绍
    2. 为什么要做插件化
    3. 如何实现插件化
    4. 如何开发一个插件

    项目介绍

    项目中文名称是快图设计,英文名称为技术栈拼接的:vue-fabric-editor ,使用 Vue3 + Fabric.js 开发的开源图片编辑器。

    Fabric.js 是业界知名的 Canvas 工具库,它已经 12 岁了,在业界得到了广泛的应用和认可,可用于开发图片编辑、物联网组态、平面设计工具等场景。

    本文以介绍插件架构开发经验为主题,不对 Fabric.js 做过多介绍。

    项目特点

    1. 插件化架构:可通过插件的形式进行扩展,定义快捷键、右键菜单、复用的 API/Event 。
    2. 简洁易用:拖拽式设计,让普通人轻松上手操作。
    3. 功能完善:PSD 解析、辅助线、历史记录、渐变、自定义字体、裁剪等功能,满足轻量图片编辑需求。

    可应用于自媒体/海报设计、平面设计、PPT 、价签设计等场景,以下为项目的实际应用截图。

    为什么要做插件化

    项目最初使用 vue2 + Fabric.js 开发,通过使用 Vue2 的 Provide/Inject 的 API 方法,将 Fabric.js 的 canvas 对象注入到各个子组件当中。

    看起来好像组件各司其职,也符合满足单一职责原则,实际上隐藏着很大的问题。

    问题说明

    如果要实现以下 2 个功能:

    1. 画布功能需要在导入源文件时储存画布尺寸变量,需要尺寸修改的 API 方法,其他组件可订阅尺寸修改事件。
    2. 自定义字体功能需要初始化时生成字体引用的 CSS 代码,需要在导入源文件前获取到所有使用的字体名称,并进行加载,加载完成后再进行源文件的的渲染。

    在没有扩展规范的情况下,很有可能出现的情况是:

    1. 导入逻辑出现过多业务处理代码。
    2. 多个组件的订阅事件出现相互依赖。

    一个功能的代码散落在各个组件中,扩展功能无规范,很多逻辑相互缠绕。

    如果按照硬编码的方式实现,那么组件将会错综复杂,相互紧密耦合,修改一个微小功能都要梳理众多组件的影响范围。

    根本原因

    图片编辑器不同于 Canvas 库,需要有自己的生命周期 Hook 方法、API 复用方法、事件订阅、快捷键/右键菜单绑定等功能,编辑器需要有明确的扩展规范

    Fabric.js 是一个典型的 Canvas 库,不满足也不应该处理这些需求,需要有一个原则来明确定义 Canvas 库与图片编辑器的关系

    如何实现插件化

    要实现插件化,需要先明确关系与扩展规范,最后再按照设计去实现功能即可。

    抽象分层

    要解决扩展规范的问题,首先要明确编辑器与 Canvas 库的关系,用汽车来类比。

    • Canvas 库为引擎:有很强的驱动能力,是汽车的核心,就像 Fabric.js 有很完善的绘制图像能力,是编辑器的核心。
    • Editor 编辑器为底盘:围绕引擎在为汽车行驶提供更多功能和接口,比如方向盘驱动、轮子、减震、钥匙启动等,就像图片编辑器必要的辅助线、历史记录、右键菜单、快捷键等。
    • UI 框架为车壳:React/Vue + UI 组件就像车壳,只需要调用底盘提供的功能接口即可完成驱动。

    这样的分层关系,能够让我们在开发时,更清晰的拆分功能应该放在哪里更合适,绘制能力给 Fabric.js ,集成复用能力给底盘,而不是将所有逻辑一股脑的塞在组件里。

    扩展规范

    通过对历史逻辑的梳理,对编辑器做扩展需要生命周期 Hook 、API 挂载、事件订阅、右键菜单扩展、快捷键绑定,一个编辑器的扩展功能应该内聚在一个文件中,而插件化架构更为合适,扩展规范也就明晰了。

    1. 生命周期
      • 导入前
      • 导入后
      • 保存前
      • 保存后
    2. 挂载 API
    3. 事件订阅
    4. 右键菜单扩展
    5. 快捷键绑定

    引用插件:

      const canvas = new fabric.Canvas('canvas');
      // 编辑器初始化
      canvasEditor.init(canvas);
      // 引用插件
      canvasEditor.use(DringPlugin, { repoSrc: 'https://api.kuaitu.cc' });
    

    插件化实现

    接下来终于进入编码的环节,已经有很多成熟的库可以满足我们的功能需求,以下是我们用到的依赖库。

    1. Editor 对象

    负责编辑器、插件、Hook 的初始化,监听右键菜单并渲染所有插件内的 menu 方法,监听快捷键方法并所有插件内的绑定事件,主要逻辑见截图,详细实现逻辑见代码: https://github.com/ikuaitu/vue-fabric-editor/blob/main/packages/core/Editor.ts

    2. 主流程插件

    ServersPlugin 与其他插件不同,是作为主流程插件存在,与 Editor 对象同等重要,提供了 Hook 的顺序控制和基础 API 。

    Hook 控制: https://github.com/ikuaitu/vue-fabric-editor/blob/main/packages/core/ServersPlugin.ts

    3. 其他插件

    编辑器其他插件可按需单独引入,统一放置在 plugin 目录下: https://github.com/ikuaitu/vue-fabric-editor/tree/main/packages/core/plugin

    以上就是插件化的主要实现思路,具体细节逻辑可参见源码。

    如何开发一个插件

    插件以类的形式初始化,采用声明式编程的方式对外部暴露 API 、事件、快捷键,所有需要使用的 API 、事件、快捷键都需要在 apis 、events 、hotkeys 里声明。

    Hook 方法直接在插件内部按规范命名即可:

    • hookImportBefore
    • hookImportAfter
    • hookSaveBefore
    • hookSaveAfter

    右键菜单可根据选中的元素类型进行判断,可增加分割线、二级菜单嵌套。

    快捷键的扩展可根据点击事件的 keyCode 、up/down 绑定对应的方法即可。

    插件代码实例:

    import Editor from './Editor';
    type IEditor = Editor;
    
    class FontPlugin {
      public canvas: fabric.Canvas;
      public editor: IEditor;
      // 插件名称
      static pluginName = 'FontPlugin';
      // 挂载 API 名称
      static apis = ['downFontByJSON'];
      // 发布事件
      static events = ['textEvent1', 'textEvent2'];
      // 快捷键 keyCode hotkeys-js
      public hotkeys: string[] = ['backspace', 'space'];
      // 私有属性
      repoSrc: string;
    
      constructor(canvas: fabric.Canvas, editor: IEditor, config: { repoSrc: string }) {
        // 初始化
        this.canvas = canvas;
        this.editor = editor;
        // 可插入外部配置
        this.repoSrc = config.repoSrc;
      }
    
      // 钩子函数 hookImportAfter/hookSaveBefore/hookSaveAfter Promise
      hookImportBefore(json: string) {
        return this.downFontByJSON(json);
      }
    
      // 挂载 API 方法
      downFontByJSON() {
        //
      }
    
      // 私有方法 + 发布事件
      _createFontCSS() {
        const params = [];
        this.editor.emit('textEvent1', params);
      }
    
      // 右键菜单
      contextMenu() {
        const selectedMode = this.editor.getSelectMode();
        if (selectedMode === SelectMode.ONE) {
          return [
            null, // 分割线
            {
              text: '翻转',
              hotkey: '❯',
              subitems: [
                {
                  text: t('flip.x'),
                  hotkey: '|',
                  onclick: () => this.flip('X'),
                },
                {
                  text: t('flip.y'),
                  hotkey: '-',
                  onclick: () => this.flip('Y'),
                },
              ],
            },
          ];
        }
      }
    
      // 快捷键
      hotkeyEvent(eventName: string, { type }: KeyboardEvent) {
        // eventName:hotkeys 中的属性 backspace 、space
        // type:keyUp keyDown
        // code:hotkeys-js Code
        if (eventName === 'backspace' && type === 'keydown') {
          this.del();
        }
      }
    
      // 注销
      destroy() {
        console.log('pluginDestroy');
      }
    }
    
    export default FontPlugin;
    
    

    总结

    开头简单介绍了项目及应用场景,重点说明图片编辑器不同于 Canvas 库,需要有自己的生命周期 Hook 方法、API 复用方法、事件订阅、快捷键/右键菜单绑定等功能,硬编码会导致代码复杂度增高,编辑器需要有明确的扩展规范

    之后用汽车的引擎、底盘、车壳来类比图片编辑的 Canvas 库、Editor 、UI 框架之间的关系,并梳理出一个插件需要的 Hook/API/Event/右键菜单/快捷键扩展能力,并将插件化实现思路代码做了展示,最后展示了插件 API 的用法,并提供了示例。

    以上就是开源图片编辑器 vue-fabric-editor 项目的插件化架构实现,笔记多有疏漏,还望见谅,分享出来希望能够给大家一些参考,如果有收获希望,大家帮忙点赞收藏,评论区一起交流。

    如果你对这个开源项目感兴趣,欢迎你能加入我们一起维护。

    1 条回复    2024-08-10 22:49:26 +08:00
    webeasymail
        1
    webeasymail  
       39 天前
    看起来很棒!
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5481 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 01:34 · PVG 09:34 · LAX 18:34 · JFK 21:34
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.