开源图片编辑器的插件化架构 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
nihaojob
V2EX    程序员

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

  • &nsp;
  •   nihaojob 2024 年 8 月 10 日 1821 次点击
    这是一个创建于 519 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

    大纲

    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  
       2024 年 8 月 10 日
    看起来很棒!
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2744 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 15:01 PVG 23:01 LAX 07:01 JFK 10:01
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86