乐趣区

关于前端:纯前端图像编辑器毕业班蹭饭地图制作工具的一点开发心得

毕业班蹭饭地图制作工具 是一个所见即所得的图像编辑器,用来帮忙高中毕业班的孩子们把班级同学的去向进行可视化。
本文次要记录我在开发这个利用时的一些,不波及具体实现细节,利用是闭源的,思考当前把框架开源。

一、编辑器利用设计的两个根本要求

1. 编辑器板块之间应防止耦合

例如:在 PS 等图像编辑器利用中,拾色器都有“从画板上取色”的性能,这意味着拾色器不仅承当色彩输出的性能,还要实现读取画板数据的性能。

不过实际上我的利用里并没有取色性能,因为局部用了 svg,取色板做起来多少有点麻烦。

开发者面对这种需要的第一想法,必定是让画板给拾色器提供一个专门的取色接口,俺也一样。然而这样做,拾色器就必须与画板耦合:

graph LR
用户事件 --> 拾色器 -- 申请取色 --> 画板
 画板 -- 点取的色彩 --> 拾色器

拾色器与画板的耦合只是比拟直观的一个例子,编辑器中板块之间的合作场景极多,如果让它们两两耦合的话,前面扩大编辑器性能的时候会极其艰难。
实际上这个利用并不是第一个版本,我在 2021 年上线了其首个版本,其板块之间耦合极其重大,尝试扩大利用的时候往往牵一发而动全身。

2. 编辑器必须有良好的状态治理,并严格遵循

对于任何一个有教训的前端开发,对着文档拼凑出一个能够编辑图像的利用绝非难事,然而图像编辑器的目标就是为了升高非专业人士解决图像的门槛,因而在交互设计上要进行容错,最广泛的容错设计就是撤销 (undo) 和重做 (redo)。
在简单的视图上进行撤销和重做是极其艰难的,将视图齐全形象成状态则会容易许多。因而,一般前端利用的 事件→视图 架构不再实用,而是要严格遵循 事件→状态→视图 的架构,这样一来程序能力把编辑成绩的工夫“切面”保留在一个历史栈中,产生撤销 / 重做操作的时候从保留的历史“切面”复原为状态。

graph LR
历史栈 -- 历史切面 --> 状态治理
状态治理 -- 状态切面 --> 历史栈
用户事件 -- 撤销 or 重做 --> 历史栈
用户事件 -- 扭转 --> 状态治理 --> 视图渲染

当然很多现有利用也是采纳 事件→状态→视图 的架构来设计的——设计上是这样,然而开发过程中难免会有一部分状态被偷懒的开发者“截留”在组件里,导致“切面”数据没有记录视图的全副成果。

二、“毕业班蹭饭地图”制作工具的架构设计

1. 协定——负责板块之间通信,以避免板块耦合

仍以拾色器和画板的联动为例,实现步骤为:

  1. 画板向协定提供取色接口(即在画板上笼罩一张截图,用户点截图,就会取得对应地位的色彩参数);
  2. 拾色器订阅前述一个“取色协定”,用户点击取色按钮的时候,经协定向画板收回取色申请,画板开启取色性能;
  3. 用户在画板上选取色彩,色彩值经协定传送给拾色器并输出;
  4. 拾色器经协定向画板发送敞开取色的信号,画板即敞开取色性能。
sequenceDiagram
拾色器 ->> 取色协定: 发动取色申请
取色协定 ->> 画板: 调起取色性能
画板 -->> 画板: 用户在取色界面抉择色彩
画板 ->> 取色协定: 用户选取的色彩
取色协定 ->> 拾色器: 色彩值
拾色器 ->> 取色协定: 敞开取色
取色协定 ->> 画板: 敞开取色

这样一来,取色器与画板的解耦合就实现了,单方只负责本人的角色,不关怀协定另一侧是什么对象。除了画板之外,任意板块都能够提供取色的接口,甚至能够在电脑上运行一个截图客户端,间接获取用户屏幕上任意一点的色调信息,或者通过网络拾取他人屏幕上的色调信息。
在整个“毕业班蹭饭地图制作工具”中,协定利用非常宽泛,连最重要的状态管理机制都是通过相应的协定接入利用的,这意味着如果前面要改为 Pinia 或者 Redux 之类的状态治理计划的时候,能够间接做一个胶水层替换目前的实现。
当然,板块与协定之间存在肯定的耦合,这是无奈防止的。

2. 模式——负责组合某一类性能

在一个画板利用里,往往会有多种不同类型的图元须要治理,如文字、图片、多边形,这些图元领有不同的个性,把它们全副放在一起开发是不理智的。所以在这个利用里,我将不同的性能放到不同的模式开发,例如图片模式、文字模式等。
有人可能要问:把图形别离开发了,那图形之间的协同怎么办?——答案是用协定连通彼此。
因为模式将一整个性能都集成在一块了,一个模式相当于一整类性能的入口,如果要增删画板的某一项性能,在代码外面增减对应的模式即可:

  editor.registerMode(new GlobalMode());
  editor.registerMode(new RegionMode());
  editor.registerMode(new ImageMode());
  editor.registerMode(new TextMode());
  // AuthorMode 仅作者可用,所以线上版将其正文掉
  // editor.registerMode(new AuthorMode());

3. 部件——负责渲染视图

作为一个以前端技术为撑持的利用,最终还是要渲染成 HTML 的,承当这一工作的货色,我把它叫做部件,在代码里则应用 Widget 示意。
部件应用 React 组件输入视图,然而为了把视图整合到利用里,我应用 HOCReact.Component 组件做了一层包装,导致视图的写法有些“非主流”,相似这样:

class TestWidget extends Widget {private state = new WidgetState({ count: 0});

  // 这个 renderer 不是 React.FC,而是一个 React.FC 工厂
  renderer: ({initWidgetState}) => {const { state} = this;
    // initWidgetState 专门用来绑定 React 与 Widget 的状态
    const [getState, setState] = initWidgetState(state);

    // 这里的返回值才是真正的 React.FC
    // 然而请留神外面无奈应用任何 React Hooks
    return () => {const { count} = getState();
      return <>
        以后数值:{count}
        <button
          onClick={() => { setState({ count: count + 1}) }}
        > 加一 </button>
      </>
    }
  }
}

写法比拟繁琐,横看像 React.Component,纵看像React.FC,好在 TypeScript 能够及时揭示我怎么去写,然而不反对热更新。

4. 区域——作为渲染部件的容器

应用“模式”来组合一系列性能,从逻辑上来讲,的确是十分正当的。
然而从视图上讲,如果你移除了文字渲染模式,就等于同时移除:增加文字的按钮、输出文字的弹窗、编辑文字的表单……这些货色都是部件 (Widget),而每个部件该当被渲染到不同的容器里,如果采纳组件树的思维去组织部件容器与部件的关系,那么增删“模式”的时候就须要去操纵组件树挨个批改部件的容器(或者组件树状态)。
这个问题的解决办法是——让部件决定本人渲染到哪一个区域(Zone),区域会订阅部件的生命周期,从而更新视图。

部件 (Widget) 和区域 (Zone) 之间采纳申明渲染类型的形式确定彼此。例如一个 Zone 申明渲染 ["draw-board-view","view"],而一个 Widget 申明承受 ["draw-board-view","main-view","view"]渲染——那么这个 Widget 就会被交给此 Zone 渲染。

在这个例子中,如果没有任何 Zone 申明渲染 "draw-board-view",则 Widget 可能会被交给申明渲染 "main-view"Zone
如果没有对应的 Zone 呢?不存在的—— WidgetZone 类的外部实现确保至多会有一个 "view"类型,只不过到这境地的状况下,所有的 zone 都会被渲染到一起,整个利用就没法应用了。

这样的办法还有一个益处:如果部件的性能同时适配了挪动端和 PC 端,那么咱们只须要针对两端各开发一套 Zone 体系,就能够跨端适配。

不过目前画板部件并不能适配挪动端,所以并没有跨端反对。

5. 框架——负责整合、调度以上各板块

以上这些板块之间的性能须要整合方能协调,这个工作就交给了框架,以最简略的撤销 / 重做性能的实现为例,利用的局部组织形式大略就是这样的:

graph TB 

编辑器框架 -- 引入 --> 全局模式

全局模式 -- 初始化 --> 状态治理协定
全局模式 -- 引入 --> 撤销 / 重做按钮
撤销 / 重做按钮 -- 操纵 --> 状态治理协定
撤销 / 重做按钮 -. 渲染到.-> 按钮区域

编辑器框架 -- 引入 --> 按钮区域

subgraph 区域 Zones
  按钮区域
end

状态治理协定

整个利用波及到的各种区域、部件、协定、模式有数十种之多,全副画进去应该是十分 壮观 芜杂的,但在开发过程中我只须要关注眼下的板块就好。

三、应用到的次要技术

1. 视图框架和 UI 库

  1. 视图库:React
  2. UI 库:AntD
  3. 脚手架:Create-React-App

2. 图像编辑局部

  1. 渲染层:ZRender
  2. 拾色器:@hello-pangea/color-picker
  3. 截图:html-to-image

2. 其余

  1. 表格解析 xlsx
  2. 压缩包读取 @zip.js
  3. 本地存储 localforage

四、心得

尽管我学习 TypeScript 很久了,然而工作里从未用到,这是我的第一个 TypeScript 利用,裸露了本人之前学习的许多盲点,所以说学习编程还是要重视实际。
整个利用其实是没有什么成体系的“设计”之说的,很多想法都是在编写代码的时候忽然冒出来的,所以本文其实是一篇“总结”,实际上利用中落地的代码并不全是按这些思路来的,例如“协定”有时候也被命名为 Channel、模式有时候被命名为 Layer……多少有些芜杂,可见程序员的确须要学习一些架构、标准方面的常识。
状态治理模块是本人写的,应用 JSON 做序列化 / 反序列化,数据量大的时候性能问题显著,所以说技术不够最好不要贸然造轮子,应该多学习一些优良的我的项目。

退出移动版