做牛津纺衬衫的网站,个人网站包含哪些内容,平面设计岗位职责,做一个公司网站一般多少钱基于Canvas实现的简历编辑器
大概一个月前#xff0c;我发现社区老是给我推荐Canvas相关的内容#xff0c;比如很多 小游戏、流程图编辑器、图片编辑器 等等各种各样的项目#xff0c;不知道是不是因为我某一天点击了相关内容触发了推荐机制#xff0c;还是因为现在Canvas…基于Canvas实现的简历编辑器
大概一个月前我发现社区老是给我推荐Canvas相关的内容比如很多 小游戏、流程图编辑器、图片编辑器 等等各种各样的项目不知道是不是因为我某一天点击了相关内容触发了推荐机制还是因为现在Canvas比较火大家都在卷本着我可以用不上但是不能不会的原则我也花了将近一个月的时间通过Canvas实现了简历编辑器。
关于Canvas简历编辑器项目的相关文章:
社区老给我推Canvas我也学习Canvas做了个简历编辑器Canvas图形编辑器-数据结构与History(undo/redo)Canvas图形编辑器-我的剪贴板里究竟有什么数据Canvas简历编辑器-图形绘制与状态管理(轻量级DOM)Canvas简历编辑器-MonorepoRspack工程实践
为什么要自行实现一个简历编辑器
固定模版不好用各种模版用起来细节上并不是很满意要么是模块的位置固定要么是页面边距不满意而通过Canvas实现的简历编辑器都是图形完全依靠画布绘制图形在给定的基础图形上可以任意绘制不会有排版问题。数据安全不能保证因为简历上通常会存在很多个人信息例如电话、邮箱等等这些简历网站通常都需要登录才能用数据都存在服务端虽然泄漏的可能性不大但是保护隐私还是很重要的此编辑器是纯前端项目数据全部存储在本地没有任何服务器上传行为可以完全保证数据安全。维持一页简历不易之前使用某简历模版网站时某一项写的字较多时导出就会出现多页的情况而我们大家大概都听说过简历最好是一页所以在实现此编辑器时是直接通过排版的方式生成PDF 所以在设置页面大小后导出的PDF总会是保持一页看起来会更美观。
背景
我是有个基于DOM实现的简历编辑器项目的因为暂时找不到可以用Canvas实现的比较有意思的场景所以才选择了继续做简历编辑器最开始做简历编辑器就是因为很多简历网站都是要开会员的要不就是简历的自定义程度比较差达不到我想要的效果在学校的某一个晚上突发奇想于是自己做了一个出来。
因为是本着学习的态度以及对技术的好奇心来做的所以除了一些工具类的包例如 ArcoDesign、ResizeObserve、Jest 等包之外关于 数据结构packages/delta、插件化packages/plugin、核心模块packages/core 等都是手动实现的。实际上这也是本着 自己学习的项目能自己写就自己写公司/商业化项目能有已有包就用已有包 的原则来的在这里的目标是学习而不是做产品自己学习肯定是希望能够更多地接触相对底层一些的能力自己可以多踩一些坑会对相关能力有更深的理解如果是公司的项目那肯定是成熟的产品优先成熟的产品对于边界case的处理以及积攒的issue也不是轻易能够比拟的。
开源地址: https://github.com/WindrunnerMax/CanvasEditor 。 在线DEMO: https://windrunnermax.github.io/CanvasEditor/ 。 笔记
因为我的主要目标是学习基本的Canvas知识和能力所以很多功能模块都是采用简单的方式实现的主打一个能用就行。而实际上做好图形编程是一件非常困难的事如果要做一些复杂的能力我会更倾向于用konva等工具包来实现而即使是简单地实现功能在写代码的时候我也遇到了很多问题也记录一些思考来解决问题。
数据结构
数据结构的设计类似于DeltaSet最终呈现的数据结构形式是扁平化的但是在Core中需要设计State来管理树形结构因为要设计Undo/Redo的功能在不全量存储快照的情况下就意味着必须设计原子化的Op因为想实现的功能有组合这个能力所以最终实现的形式实际上是树形的结构而我希望的结构是扁平化的因为树形结构查找起来比较费劲需要实现的Op类型也会变多我希望能尽量减少Op的类型并且能够做到History所以最终定下的数据结构是DeltaSet作为存储通过State来管理整个编辑器状态。
History
原子化的Op已经设计好了所以在设计History模块时就不需要全量保存快照了但是如果每个操作都需要并入History Stack的话可能并不是很好通常都是有N个Op的一并Undo/Redo所以这个模块应该有一个定时器如果在N毫秒秒内没有新的Op加入的话就将Op并入History Stack但是当时我在思考一个问题如果这N毫秒内用户进行了Undo操作应该怎么办后来想想实际上很简单此时只需要清除定时器将暂存的Op[]立即放置于Redo Stack即可。
绘制
任何元素都是矩形数据结构也是据此设计抽象出来的在绘制的时候分为两层Canvas重叠的方式内层的Canvas是用来绘制具体图形的这里预计需要实现增量更新而外层的Canvas是用来绘制中间状态的例如选中图形、多选、调整图形位置/大小等在这里是会全量刷新的并且后边可能会在这里绘制标尺。在实现交互的过程中我遇到了一个比较棘手的问题因为不存在DOM所有的操作都是需要根据位置信息来计算的比如选中图形后调整大小的点就需要在选中状态下并且点击的位置恰好是那几个点外加一定的偏移量然后再根据MouseMove事件来调整图形大小而实际上在这里的交互会非常多包括多选、拖拽框选、Hover效果都是根据MouseDown、MouseMove、MouseUp三个事件完成的所以如何管理状态以及绘制UI交互就是个比较麻烦的问题在这里我只能想到根据不同的状态来携带不同的Payload进而绘制交互。
绘制状态
在实现绘制的时候我一直在考虑应该如何实现这个能力因为上边也说了这里是没有DOM的所以最开始的时候我通过MouseDown、MouseMove、MouseUp实现了一个非常混乱的状态管理完全是基于事件的触发然后执行相关副作用从而调用Mask的方法进行重新绘制。再后来我觉得这样的代码根本没有办法维护所以改动了一下将我所需要的状态全部都存储到一个Store中通过我自定义的事件管理来通知状态的改变最终通过状态改变的类型来严格控制将要绘制的内容也算是将相关的逻辑抽象了一层只不过在这里相当于是我维护了大量的状态而且这些状态是相互关联的所以会有很多的if/else去处理不同类型的状态改变而且因为很多方法会比较复杂传递了多层导致状态管理虽然比之前好了一些可以明确知道状态是因为哪里导致变化的但是实际上依旧不容易维护。最终我又思考了一下决定在绘图这里实现类似于DOM的能力因为我想实现的能力似乎本质上就是DOM与事件的关联而DOM结构是一种非常成熟的设计了这其中有一些很棒的点子例如DOM的事件流我不需要扁平化地调整每个Node的事件而是只需要保证事件是从ROOT节点起始最终又在ROOT上结束即可并且整个树形结构以及状态是靠用户利用DOM的API来实现的我们管理之需要处理ROOT就好了这样就会很方便下个阶段的状态管理是准备用这种方式来实现的。
渲染与事件
在前边我们提到了我们想通过模拟DOM来完成Canvas的绘制与交互那么在这里就很明显涉及到DOM的两个重要内容即DOM渲染与事件处理。那么就先聊下渲染方面的内容使用Canvas实际上就很像将所有DOM的position设置为absolute所有的渲染都是相对于Canvas这个DOM元素的位置绘制那么我们就需要考虑重叠的情况那么想一个例子A的zIndex是10A的子元素B的zIndex是100C与A是平级的且zIndex为20那么当这三个元素重叠的时候在最顶部的元素是C也就是说zIndex实际上只看平级元素再假如A的zIndex是10A的子元素B的zIndex是1那么在这两个元素重叠的时候在最顶部的元素是B也就是说子元素通常都是渲染在父元素之上的。那么我们在这里也需要模拟这个行为但是因为我们没有浏览器的渲染合成层我们能够操作的只有一层所以在这里我们需要根据一定的策略进行渲染在渲染时我们与DOM的渲染策略相同即先渲染父元素再渲染子元素类似于深度优先递归遍历的渲染顺序不同的是我们需要在每个节点遍历之前将子节点根据zIndex排序来保证同层级的节点渲染重叠关系。
在渲染的基础上我们还需要考虑事件的实现例如我们的选中状态八向调整元素大小的点一定是在选区节点的上层的那么假如现在我们需要实现onMouseEnter事件的模拟那么因为Resize这八个点位与选区节点是有一定重叠的所以如果此时鼠标移动到重叠的点因为Resize的实际渲染位置更高所以只应该触发这个点的事件而不应该触发后边的选区节点事件而实际上由于没有DOM结构的存在我们就只能使用坐标计算那么在这里我们最简单的方法就是保证整个遍历的顺序也就是说高节点的遍历一定是要先于低节点的当我们找到这个节点就结束遍历然后触发事件事件的捕获与冒泡机制我们也需要模拟实际上这个顺序跟渲染是反过来的我们想要的是优点顶部的元素优先更像树的右子树优先后序遍历也就是把前序遍历的输出、左子树、右子树三个位置调换一下即可但是问题来了在onMouseMove这种高频事件触发的时候我们每次都去计算节点的位置并且采用深度优先遍历是非常耗费性能的所以在这里实现一个典型的空间换时间将当前节点的子节点按顺序全部存储起来如果有节点的变动就直接通知该节点的所有每一层父节点重新计算这里做成按需计算即可这样当另一颗子树不变的时候还可以节省下次计算的时间并且存储的是节点的引用不会有太大的消耗这样就变递归为迭代了另外因为找到了当前的节点在模拟捕获与冒泡的时候就不需要再递归触发了通过两个栈即可模拟。
焦点
平时我做富文本相关的功能比较多所以在实现画板的时候总想按照富文本的设计思路来实现因为之前也说过要实现History以及在编辑面板富文本的能力所以焦点就很重要如果焦点不在画板上的时候如果按下Undo/Redo键画板是不应该响应的所以现在就需要有一个状态来控制当前焦点是否在Canvas上经过调研发现了两个方案方案一是使用document.activeElement但是Canvas是不会有焦点的所以需要将tabIndex-1属性赋予Canvas元素这样就可以通过activeElement拿到焦点状态了方案二是在Canvas上方再覆盖一层div通过pointerEvents: none来防止事件的鼠标指针事件但是此时通过window.getSelection是可以拿到焦点元素的此时只需要再判断焦点元素是不是设置的这个元素就可以了。
无限画布
之前因为没有打算实现平移拖拽也就是无限画布的能力但是后来真的开始通过这个主框架来实现想做的业务功能的时候发现这样是不行的所以在后期想把这个能力加上虽然本身这个能力并不复杂但是因为最开始没有设计这个能力导致后边做的时候有点难受比如Mask批量刷新频率不对齐、ctx的translate应该是偏移值取反、之前多处超出画布不绘制的计算有误等等就感觉在没有设计的情况下突然增加功能确实是有点难受的不过好处是不需要大规模重构只是个别点位的修正。
此外多扯点别的这个项目除了一些辅助性的工具例如resize-observer以及组件库例如arco-design都是自己写的相当于实现了Canvas的引擎特别是在现在的core-delta-plugin-utils结构设计下是完全可以抽离处理作为工具包使用的当然易用性与性能方面肯定比不上那些有名的开源框架。只不过今天我恰好看到了一个评论说的挺好的如果是个人能力提升那么最好是首先理解开源库然后仿照实现开源库的功能主要的目标是学习而如果是商业化的使用那就变成了知名的开源库优先这样可以很大程度上降低成本。
性能优化
在实现的过程中绘制的性能优化主要有:
可视区域绘制完全超出画布的元素不绘制。按需绘制只绘制当前操作影响范围内的元素。分层绘制高频操作绘制在上层画布基础元素绘制在下层画布。节流批量绘制高频操作节流绘制上层画布收集依赖批量绘制。
超链接
众所周知Canvas绘制出来就是纯粹的图片而实际使用导出PDF的超链接是可以点击的而我们当前就单纯只是图片无法做到这一点所以需要解决这个问题我想到的一个解决方案是在导出的时候通过DOM生成透明的a标签覆盖在原本的超链接位置这样就可以实现点击跳转效果了。PDF本身也是文件格式所以是可以借助PDFKit/PDFjs等PDF排版生成工具来导出的通过这种方式也可以直接在导出的时候直接将其写入固定位置并且可以不受浏览器打印的分页限制。
TODO
因为前边提到了我现在还是比较简单的实现方式所以很多功能都不完善还有很多想做的能力
层级调整这个之前我想到了并且在core中设计了这个能力现在只是缺乏调整的按钮用来调用这个UI我还没考虑好应该怎么做。页面配置我发现很多同学的简历都是不是标准的A4纸大小所以这里还需要一个调整页面画布大小的问题。导入导出JSON这个就不用多说了就是把底层数据结构导入导出的能力。排版PDF导出这个应该需要跟页面配置一起做现在的PDF导出是依赖浏览器的打印会有一些分页的限制如果自己排版的话就可以突破这个问题多长的画布都是一页的简历大小。复制粘贴模块在编辑的时候这个操作是很有用的需要增加这个模块。
最后
这次对于Canvas的体验让我感觉还是不错的后边我也会写一些在实现的时候碰到的问题以及如何解决问题的文章不过我目前的主业还是还是写富文本编辑器富文本编辑器也是天坑中的一员后边也可能会先写编辑器相关的文章。