首页 > 资讯 > 科技 > 正文
2024-02-28 12:16

基于Canvas的简历编辑器


大约一个月前,我发现掘金总是给我推荐相关的内容,比如很多小游戏、流程图编辑器、图片编辑器等各种项目。 不知道是不是因为有一天我点击了它。 相关内容触发了推荐机制,或者说是因为现在很流行,大家都在看。 本着可以不用但又不能没有的原则,我也花了近一个月的时间来实现简历编辑器。

有关编辑器的历史文章:

背景

我有一个基于 DOM 的简历编辑器项目。 因为找不到可以实现的有趣场景,所以我选择继续从事简历编辑器的工作。 我开始做简历编辑是因为很多简历网站都需要会员,或者简历定制的很差,达不到我想要的效果。 一天晚上我在学校突然有了一个想法,就自己做了一个。

因为是抱着学习的态度和对技术的好奇心来做的,所以除了一些工具包如、、Jest等包之外,还有关于数据结构/delta、插件/、核心模块/核心等方方面面的内容。手动实施。 其实这也是基于一个原则,如果你能写自己的学习项目,你就可以自己写。 如果公司/商业项目有现有的包,则使用现有的包。 这里的目标是学习而不是制造产品。 自己学习。 我当然希望能够更多地接触到相对底层的能力。 如果踩的坑多了,我就会对相关能力有更深入的了解。 如果是公司项目,一定要优先考虑成熟的产品。 成熟的产品将处理边界情况。 而且积累的问题也不容易比较。

开源地址:

在线演示:

笔记

因为我的主要目标是学习基础知识和能力,所以很多功能模块都是以简单的方式实现的。 只关注一个可以使用的。 事实上,做好图形编程是非常困难的。 如果我想做一些复杂的能力,我会更倾向于使用konva等工具包来实现。 即使是简单的实现功能,我在写代码的时候也不会使用它。 遇到了很多问题,记录了一些解决问题的想法。

数据结构

数据结构的设计与此类似。 最终的数据结构形式是扁平化的,但是在Core中,需要设计State来管理树形结构,因为Undo/Redo功能需要设计在不完全存储快照的情况下。 这意味着一定要设计 Ops,因为你要实现的功能是有组合能力的,所以最终的形式其实是一个树形结构,而我想要的结构是扁平的,因为树形结构比较难找。 ,需要实现的Ops类型也会增加。 我希望尽可能减少Ops的类型并且能够做到,所以最终的数据结构作为存储通过State来管理整个编辑器状态。

已经设计了原子Op,因此在设计模块时无需保存所有快照。 但是,如果每个操作都需要合并到Stack中,那就不太好了。 通常,有 N 个操作同时被撤消。 /Redo,所以这个模块应该有一个定时器。 如果N毫秒内没有添加新的Op,则该Op将被合并到Stack中。 但我当时就在想一个问题。 如果用户在这N毫秒内执行了Undo操作,那么应该怎么办? 后来想了想,其实很简单。 此时,只需清零定时器,立即将临时Op[]放入重做栈即可。

任何一个元素都是一个矩形,数据结构就是基于这个设计抽象出来的。 绘制时,分为两个重叠的图层。 内层用于绘制特定图形。 预计这里需要增量更新,外层用来绘制中间状态,比如选择图形、多选、调整图形的位置/大小等,这里会全面刷新,标尺可能会稍后画到这里。 在实现交互的过程中,我遇到了一个比较棘手的问题。 因为没有DOM,所以所有的操作都需要根据位置信息来计算。 例如,选择图形后要调整大小的点需要处于选中状态并单击。 位置就是那些点加上一定的偏移量,然后根据事件调整图形大小。 其实这里会有很多交互,包括多选、拖拽选择、Hover效果,都是基于,,三个事件完成的,那么如何管理状态和绘制UI交互呢一个麻烦的问题。 这里我只能想到根据不同的状态携带不同的,然后绘制交互。

绘图状态

在实现绘图的时候,我一直在思考如何实现这个能力。 因为上面说了,这里没有DOM,所以一开始我通过实现了一个非常混乱的状态管理,而且是完全基于事件的。 触发然后执行相关副作用来调用Mask方法进行重绘。 后来我觉得没有办法维护这样的代码,所以我做了一些修改,把我需要的所有状态都存储在一个Store中。 我通过我定制的事件管理来通知状态变化,最终通过了状态变化的类型。 要严格控制要绘制的内容,可以看作是对相关逻辑进行了一层抽象,但是这里相当于维护了大量的状态,而且这些状态是相互关联的,所以会出现很多if/ else 来处理不同类型的状态变化,而且由于很多方法比较复杂,经过多层,虽然状态管理比以前更好,可以清楚地知道状态在哪里发生了变化,但实际上还是不太容易维护。 最后我又想了想,决定在绘图方面实现类似DOM的能力,因为我想要实现的能力本质上似乎就是DOM和事件的关系,而DOM结构是一个非常成熟的设计,并且有一些很棒的想法,例如 DOM 事件流。 我不需要展平每个节点的事件。 相反,我只需要确保事件从ROOT节点开始并最终在ROOT上结束,并且整个树的形状和状态由用户使用DOM API来实现。 我们只需要处理ROOT即可进行管理,会非常方便。 下一阶段的状态管理计划以此方式实施。

渲染和事件

利用文本编辑器编译_文本编辑器c语言代码_c实现文本编辑器

前面我们提到我们要通过模拟DOM来完成绘制和交互,那么这里显然涉及到了DOM的两个重要内容,即DOM渲染和事件处理。 那么我们先来谈谈渲染。 用法实际上与将所有 DOM 设置为 . 所有渲染都是相对于该 DOM 元素的位置绘制的。 那么我们就需要考虑重叠的情况。 让我们想一个例子。 ,A的为10,A的子元素B为100,C和A相等且都是20,那么当这三个元素重叠时,最上面的元素是C,也就是说实际上只看横向元素,如果A的值为10,而A的子元素B的值为1,那么当这两个元素重叠时,顶部元素是B,这意味着子元素通常渲染在父元素之上。 那么我们这里也需要模拟这个行为,但是因为我们没有浏览器的渲染合成层,所以我们只能操作一层,所以这里我们需要按照一定的策略来渲染。 渲染时,我们采用与 DOM 渲染策略相同的方式,即先渲染父元素,再渲染子元素,类似于深度优先递归遍历的渲染顺序。 不同的是,在遍历每个节点之前,我们需要对子节点进行排序,以保证同级节点渲染的重叠关系。

在渲染的基础上,我们还需要考虑事件的实现。 比如我们选中状态下,调整元素八个方向大小的点必须在选择节点的上层。 那么如果我们现在需要实现事件模拟,那么由于这八个点与选择节点之间存在一定的重叠,所以如果此时鼠标移动到重叠点,因为实际渲染位置较高,只需应该触发该点的事件而不是后续选择节点事件。 事实上,由于没有DOM结构,我们只能使用坐标计算。 所以这里我们最简单的方法就是保证整个遍历的顺序。 也就是说,必须先遍历高节点,然后再遍历低节点。 当我们找到这个节点时,结束遍历,然后触发事件。 我们还需要模拟事件捕获和冒泡机制。 事实上,这个顺序与渲染相反。 我们想要的是位于优势顶部的元素,优先级给予更像树的右子树。 后序遍历就是改变前序遍历的输出、左子树、右子树的位置。 但问题来了。 当这样的高频事件触发时,我们每次都要计算节点。 位置并使用深度优先遍历非常消耗性能,因此这里实现了典型的空间换时间,并且将当前节点的所有子节点按顺序存储。 如果节点发生变化,则直接通知该节点的所有节点。 每层的父节点都会重新计算。 这里可以按需计算,这样在其他子树不变的情况下,可以节省下次计算的时间,并且存储了该节点的引用,不会太大。 消耗,从而将递归变为迭代。 另外,由于找到了当前节点,所以模拟捕获和冒泡时不需要触发递归。 可以通过两个栈来模拟。

重点

我平时会做很多和富文本相关的功能,所以在实现画板的时候,我总是想按照富文本的设计思想来实现。 因为我之前提到过需要在编辑面板中实现富文本,所以重点非常重要。 如果焦点不在画板上,如果按Undo/Redo键,画板应该不会响应,所以现在需要有一个状态来控制当前焦点是否在它上面。 经过研究,我们找到了两种解决方案。 第一种解决方案是使用.,但是不会有焦点,所以需要给元素赋值==“-1”属性,这样才能获取焦点状态。 第二种方案是在上面再覆盖一层div,并使用:none来防止鼠标指针事件。 event,但此时可以通过 获取焦点元素。 此时只需判断焦点元素是否为集合元素即可。

无限画布

之前我并没有打算实现平移和拖动,这就是无限画布的能力,但是后来当我真正开始使用这个主框架来实现我想做的业务功能时,我发现这是不可能的,所以我想在后期添加这个能力。 虽然这个能力本身并不复杂,但是因为这个能力一开始就没有设计,所以后来做的时候就有点不适应了。 比如mask批量刷新频率没有对齐,ctx应该是反转的偏移值,画布之外的很多地方之前都没有绘制。 计算错误等等,没有设计就突然增加功能感觉有点不舒服,但好处是不需要大规模重构,只需对个别点进行修正即可。

另外,说点别的,除了一些辅助工具如——以及arco等组件库——这个项目是我自己写的,相当于一个实现的引擎,尤其是在现在的core-delta--utils结构下设计上,它可以完全拆卸并作为工具包使用。 当然,易用性和性能肯定不如那些著名的开源框架。 但今天偶然看到一条评论说得很好:如果是为了提高个人能力,那么最好先了解开源库,然后模仿开源库的功能。 主要目标是学习; 而如果商业化使用的话,就成为知名开源库的优先选择,可以很大程度上降低成本。

性能优化

实施过程中,主要绘图性能优化包括:

绘制可见区域,完全超出画布的元素不绘制。

按需绘制,只绘制当前操作范围内的元素。

分层绘制,高频操作绘制在上层画布上,基本元素绘制在下层画布上。

节流批量绘制,高频操作节流绘制,上层画布采集依赖批量绘制。

超级链接

众所周知,绘图是纯粹的图片,但实际导出PDF的超链接是可以点击的。 然而,目前我们无法仅用图片来做到这一点,所以这个问题需要解决。 我想到的一种解决方案是导出,此时通过DOM生成一个透明的a标签,覆盖原来的超链接位置,这样就可以达到点击跳转的效果。 PDF本身也是一种文件格式,因此可以借助/PDFjs等PDF布局生成工具导出。 这样导出时也可以直接写入固定位置,不受浏览器打印的分页限制。 。

去做

前面说过,我还是采用比较简单的实现方式,所以很多功能还不够完善,还有很多能力我想做:

终于

这次的体验我感觉很好。 后面我也会写一些文章,讲一下实施过程中遇到的问题以及如何解决。 不过我目前的主要工作还是编写富文本编辑器和富文本编辑器。 小编也是的一员,以后可能会先写和小编相关的文章。

开源地址: