ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

自研图表设计器:Canvas渲染、坐标换算与可视化编辑器实现

自研图表设计器:Canvas渲染、坐标换算与可视化编辑器实现

画过图的人都知道,画“一张”图从来不是问题,真正让人头疼的是画“一堆”风格统一、规范清晰、随时能改的技术图。尤其是当你需要频繁产出系统架构图、业务流程图、网络拓扑图的时候,市面上那些通用在线画图工具用起来总有种隔靴搔痒的感觉:模板风格千篇一律,节点样式改起来费劲,导出的图片清晰度不稳定,还得担心数据往第三方平台上传的隐私问题。我自己动手写了个轻量级的图表设计器,项目代号就叫“diagram-design”,目的很直接——把画图这件事的掌控权拿回自己手里,顺带把可视化编辑器内部那套东西彻底吃透。这篇文章就是把整个项目的思路、选型、核心实现和踩坑过程完整记录下来,适合那些对图形编辑器实现原理感兴趣、或者想自己做一套可定制画图工具的开发者参考。

1. 先聊清楚:这个图表设计器到底解决了什么问题

1.1 为什么非要自己造一个画图工具

先说背景。我日常的工作需要产出大量系统设计文档,配图是刚需,样式也有硬性要求:统一的线条颜色、统一的节点圆角、统一的字体大小、统一的高宽比例。用通用画图工具确实省事,但问题也很明显。首先是样式规范的问题,一套图要保证所有节点尺寸一致、连线颜色一致、文字位置一致,在通用工具里只能靠手动对齐,几十个节点一张张调整,效率极低;其次是批量导出的问题,一个模块的文档往往需要十几张配图,每张都要手动调整导出尺寸和清晰度,重复劳动多;再就是内容安全上的考虑,手头不少图涉及内部业务逻辑,直接传到在线平台始终不踏实,本地工具又大多需要按年付费。

于是我决定自己写一个图表设计器。目标非常明确:只要能拖拽节点、能连线、能缩放画布、能导出高清图片,就够了,核心诉求是“对我自己的数据完全可控”。这类工具技术上不复杂,但要把细节做好,涉及到的知识点非常密:Canvas渲染、坐标系统转换、事件命中检测、贝塞尔曲线绘制、图片导出、数据结构设计,每一项展开都是实打实的内容。

1.2 项目核心能力与适合谁来用

放到具体场景里看,diagram-design能做的事情大概是这四类:

第一类是技术文档配图。画架构分层图、模块依赖图、部署拓扑图,节点样式统一,插入文档后整体观感好,颜色风格全部由代码控制,换主题只需要改配置文件。第二类是方案宣讲的示意图。在给团队或外部讲解系统设计时,现场画一张清晰直观的结构图,比贴一页Word文字强太多。第三类是交互原型的简略示意。产品经理画线框图、研发画功能流程图,不追求像素级视觉还原,更看重逻辑表达的速度和改动的灵活性。第四类是批量化生成图形素材。因为整个项目的数据结构是自己定义的,完全可以写个脚本批量生成节点和边,再统一渲染输出,这在通用画图工具里几乎做不到。

从这个定位就能看出,diagram-design面向的读者不一定非得是专业前端,只要你对“一个画图软件是怎么做出来的”这件事充满好奇,并且有一定编程基础,跟着这篇文章把核心逻辑捋一遍,收获会很大。即便是零基础,我也会把很多基础概念用大白话解释清楚,比如坐标换算为什么那么重要、缩放为什么会跑偏、导出的图片为什么会发虚,这些东西搞明白之后,你再看市面上任何一款图形编辑器,视角都会完全不一样。

2. 技术选型的关键决策:Canvas还是SVG

2.1 两种技术路线的真实差异

动手之前我翻了不少资料,还去看了几款主流画图工具的实现思路,发现它们底层渲染方案基本分两派:一派用Canvas,另一派用SVG,也有少数两者混用的。这两条路线的选择直接决定了后面所有开发的路线,非常值得花点篇幅掰开讲清楚。

Canvas渲染的特点是“一次性把像素画上去,画完就没了”。它的操作方式像一支画笔:你告诉它“画一个圆、填充蓝色、描边灰色”,它就执行一次绘制,结果直接体现在画布上,内存中并不保留“这个圆”的对象信息。唯一的查找方式是遍历你手动维护的数据数组。Canvas的优势是节点数量大时性能表现好,因为读取的超大DOM节点树不会占用内存;劣势是交互麻烦,画布上点了一个图形,它自己不知道“你点到了谁”,需要你手动做几何运算来判断命中。这听起来简单,实际做起来坑不少,后面我会专门讲。

SVG渲染则相反,每个图形就是一个真实存在的DOM元素。你在SVG里画一个圆,浏览器里就真的有一个对应标签包裹的节点,它有自己的属性,能直接注册鼠标事件,能通过CSS控制样式,调试时打开开发者工具还能直接看到结构。SVG在小规模图形场景下开发效率非常高,事件、样式、动画都有天然的解决方案。但它的性能瓶颈同样明显:当节点数量上升到几百上千个时,大量DOM节点会带来渲染和事件响应的明显卡顿。

用一个表格来对比会更直观:

对比维度CanvasSVG
渲染原理逐像素绘制,无DOM结构每个图形是DOM元素
大规模图形性能高,与节点数成正比但可控低,DOM节点多时明显卡顿
事件处理需手动命中和坐标计算元素自带事件绑定
导出图片原生支持,直接输出位图需额外转换或序列化
开发复杂度中等偏高,图形细节都要处理低,浏览器原生能力强
调试体验偏弱,不能直接看图形结构强,开发者工具可展开检查

如果你是做节点几十个以内的编辑器原型,SVG绝对上手更快,甚至能省一半代码。但如果目标是做一个能支撑大体量节点、还要自己控制绘制细节的工具,Canvas这条路更稳。我最终选择了Canvas,理由在后面展开。

2.2 为什么不直接套现成的图形库

既然市面上已经有非常成熟的图形编辑器开源方案,为什么不直接拿一个来改?这个问题当时也反复权衡过。主要是四个原因:第一,很多现成方案是“全套工具箱”,图形库、数据流、渲染引擎、UI组件一整套打包,引进来之后代码自由度就受限了,想改一个节点样式、加一种特殊连线,往往得深入别人封装好的内部逻辑,成本比预期高得多;第二,项目定位本身就是“小而专”,核心场景就是架构图和流程图,不需要支持UML全要素也不需要多人实时协同,维护体积和心智负担越小越好;第三,用最朴素的Canvas API自己实现一遍,相当于把整个可视化编辑器的核心机制完整过了一遍,这个学习价值比直接调库大得多;第四,自研渲染逻辑意味着代码完全掌控在自己的数据结构之上,后面加批量生成、主题定制、格式导出这些扩展功能,改起来都非常顺畅,不会被底层框架卡住。

框架层面最终用了最轻量的方式:渲染层全部原生JavaScript操作Canvas,界面上的工具栏、属性面板借助一个轻量前端框架快速搭起来。这样做的边界很清楚——数据模型和渲染核心是自研的,UI交互是借助框架的,哪一层出了问题都容易定位。这个选型思路后来验证是对的,很多看似简单的功能,因为底层是自己实现的,排查问题时心智负担比想象中小很多。

3. 核心功能到底怎么拆:画布、拖拽、连线和导出

3.1 画布缩放与坐标换算的底层逻辑

任何一个图形编辑器,最绕不开的就是坐标系统转换。很多人第一个版本做出的工具“拖拽时节点乱飘”“缩放时画面停在了左上角”“画画总差一截”,根因几乎都是坐标换算没搞对。

我先说判断原理。画布在浏览器里显示是有两层概念的:CSS像素尺寸和Canvas内部像素尺寸。Canvas的宽高属性决定它的实际缓冲区大小,CSS的宽高决定它显示多大。如果你的Canvas设置的是宽1000像素、高800像素,显示在浏览器里还是1000乘800的CSS尺寸,那么鼠标坐标能直接用。但一旦引入缩放和偏移,就必须区分“屏幕坐标”和“画布逻辑坐标”。

屏幕坐标是指鼠标在页面上的位置,通常通过事件对象的offsetX和offsetY就能拿到相对于Canvas左上角的值。画布逻辑坐标则是指你在绘制内容时使用的坐标系统,比如某个节点的真实位置是(200, 300)。这两个坐标之间的换算关系由缩放比例scale和偏移量offset决定,公式是:

逻辑坐标 = (屏幕坐标 - offset) / scale

具体来说,渲染时我会先把Canvas的坐标系进行变换:

ctx.save(); ctx.translate(view.offsetX, view.offsetY); // 先把画布原点移到偏移位置 ctx.scale(view.scale, view.scale); // 再整体缩放 // 之后所有绘制都使用逻辑坐标(模型里的真实位置) // 节点、连线、文字等绘制在这里完成 ctx.restore();

当鼠标发生事件时,就需要把屏幕坐标转换成逻辑坐标,否则你在缩放100%时觉得点对了位置,一旦放大到150%,再用原始屏幕坐标去判断命中的节点,就会发现有明显的偏移:

function screenToLogic(mouseX, mouseY) { return { x: (mouseX - view.offsetX) / view.scale, y: (mouseY - view.offsetY) / view.scale }; }

这个换算逻辑是整个编辑器地基中的地基,最理想的做法是在项目的第一个版本就写对。我早期偷懒没仔细处理,后面所有功能都在这个不准确的基础上堆,改成正确版本时几乎把交互层重写了一遍。所以这里敲个黑板:坐标换算的细节怎么强调都不过分,别想着后面再补。

3.2 节点拖拽:从命中检测到实时渲染

画布能滚动缩放了,接下来就是最直观的节点拖拽。拖拽这个功能看起来简单,实际拆解成工程问题之后,会分成四个明确的步骤。

第一步是鼠标按下时的命中检测。因为Canvas没有DOM元素可以“点谁是谁”,我必须自己遍历所有节点,判断鼠标坐标是否落在某个节点的包围盒里。假设节点是矩形,判断条件就是鼠标坐标是否在矩形的left、top、right、bottom四条边围成的区域里。这一步还要考虑层级关系,通常选中最上层的那个节点,我是在数据结构里维护一个渲染层级序号,默认取序号最大的命中文节点。

第二步是记录拖拽的初始状态。鼠标按下时,记录两样东西:鼠标的起始逻辑坐标,以及被拖拽节点的原始位置。这样鼠标移动时,新位置和起始位置之间的差值就是节点的位移增量。

第三步是鼠标移动时的实时更新。在pointermove事件里,不断用当前坐标减去起始坐标,加到节点原始位置上,更新节点的逻辑坐标:

function onPointerMove(e) { if (!draggingNode) return; const current = screenToLogic(e.offsetX, e.offsetY); const dx = current.x - dragStart.mouseX; const dy = current.y - dragStart.mouseY; draggingNode.x = dragStart.nodeX + dx; draggingNode.y = dragStart.nodeY + dy; // 标记需要重绘,等下一帧渲染 requestRender(); }

第四步是渲染循环的配合。这里有个重要优化:不能每次鼠标移动都立刻重绘画布,否则高频事件会把帧率拖垮。正确做法是用requestAnimationFrame做一个节流,任何时候数据变了就打个标记,浏览器下一帧统一重绘一次。这样即使一秒钟产生了几百个移动事件,真正绘图次数也不会超过屏幕刷新率,界面看起来还是流畅的。

3.3 连线功能:贝塞尔曲线与锚点吸附

节点拖拽能动了之后,制图工具的核心价值就已经体现出一半了,接下来就是连接线。节点之间用直线相连当然简单,但真实场景里节点位置会不断变化,直线往往会被其他图形遮挡,视觉上特别乱。我选择了三次贝塞尔曲线作为连线默认路径,它长什么样可以想象成一条平滑的S型曲线:从起点出来后逐渐弯到中间,再从中间弯向终点。

三次贝塞尔曲线需要四个点:起点、终点、两个控制点。控制点的位置决定了曲线在起点和终点处“出发的方向”。为了让连线看起来比较自然,我的做法是让控制点始终沿着水平方向延伸,也就是说,控制点和对应的端点保持在同一水平线上,再根据两端水平距离调整控制距离:

function makeEdgePath(fromNode, toNode, sourceAnchor, targetAnchor) { const start = { x: fromNode.x + sourceAnchor.x, y: fromNode.y + sourceAnchor.y }; const end = { x: toNode.x + targetAnchor.x, y: toNode.y + targetAnchor.y }; const dx = Math.max(Math.abs(end.x - start.x) * 0.5, 40); const cp1 = { x: start.x + dx, y: start.y }; const cp2 = { x: end.x - dx, y: end.y }; return `M ${start.x} ${start.y} C ${cp1.x} ${cp1.y}, ${cp2.x} ${cp2.y}, ${end.x} ${end.y}`; }

这里有个小细节值得说明:控制距离不能写死一个固定值,而应该根据两端水平距离动态计算,同时设一个最小值下限。如果两条连线差距特别大,固定值会导致曲线拐得太急,整体视觉效果很不协调。

锚点吸附也是连线模块里一个特别提升手感的功能。实际操作中,鼠标要准确点到节点边缘那个几像素宽的锚点上非常费劲,所以我在鼠标靠近锚点时增加了一个“磁吸”逻辑。具体实现是:鼠标移动时,遍历当前节点所有锚点,计算距离,一旦小于某个阈值(我设置的20像素),就直接把当前逻辑坐标替换成锚点坐标。这样用户不需要精准瞄准,只要差不离点下去,系统就会自动帮你吸上去。这个细节对整体体验的提升远大于开发成本,强烈推荐加上。

3.4 导出图片:PNG和SVG两条技术路线

画好的图要能用起来,导出的质量是关键。Canvas做导出分两种场景。一种是位图导出,直接利用Canvas原生能力,一行代码就能把当前画布内容变成图片数据。核心流程是:新建一个临时Canvas,设置好需要的输出尺寸,将当前画布内容按比例绘制过去,再调用toDataURL或toBlob。这里需要注意设备像素比的问题,后面我会专门讲为什么导出的图片经常会发虚。

另一种是矢量导出。Canvas画出来的东西本质上是像素,官网导出的图片拿到印刷场景或高DPI屏幕上很容易不够清晰,这是Canvas技术的天然短板。所以我的导出模块里专门实现了SVG导出。思路不复杂:既然每个节点、每条连线的数据都完整保存在数据模型里,那就从模型出发,重新生成一份对应的SVG结构。比如一个矩形节点,我可以生成对应的SVG rect元素,把坐标、宽高、填充色、圆角半径一一映射过去,连线则生成path元素。这个方案的好处是导出的SVG完全无损,在任何分辨率下都清晰,而且还能在矢量工具里再次编辑。顺带提醒一句,导出SVG时要注意加上字体、样式相关的描述,否则某些环境中可能无法正常显示。

3.5 把渲染流程整理成稳定的主循环

在做完上面这些功能之后,我逐渐意识到一个更根本的问题:所有交互的本质都是“改数据然后重绘”。节点拖拽是修改了节点的坐标然后重绘;画布缩放是修改了view的scale和offset然后重绘;连线操作是新增了一条edge然后重绘。哪怕执行操作的用户不同,底层模式是完全一样的。于是我没有把渲染逻辑散落在各个事件处理函数里,而是统一整合成了一个主循环:

用户交互事件 → 更新数据模型 → 标记需要重绘 → 渲染循环执行

数据模型上维护了三个核心部分:画布视图状态(宽高、scale、offset)、节点集合、连线集合。渲染函数只做一件事:从这个模型中读取全部数据,用Canvas API画出来。这样带来的直接好处是任何一次重绘都经过同一个渲染入口,不会出现“这个操作改了DOM但忘了调重绘”的遗漏问题,调试定位问题也方便。这就是把“数据驱动渲染”的思想落到具体实践中,虽然听起来抽象,但实际代码写起来就是维护一个对象,改字段,然后调用渲染函数,非常朴素。

4. 实操复盘:从一张白纸到一个能用的设计器

4.1 第一步:搭起画布骨架,绕开设备像素比的坑

整个项目动手的第一步,是创建一个最基本的页面框架和一块铺满容器的Canvas。这一步看起来简单,但有一个非常容易被忽略、影响却贯穿全程的参数——devicePixelRatio。

设备像素比是物理像素和CSS像素的比例,笔记本电脑上通常是1或1.5,高分屏手机上可能是2甚至3。如果不处理这个参数,Canvas的绘制缓冲区尺寸会被浏览器按CSS尺寸直接设置,在高分屏下就会出现文字发虚、线条模糊的情况。正确的做法是让Canvas的缓冲区尺寸等于CSS尺寸乘以设备像素比,然后通过scale调整坐标系:

function setupCanvas(canvas, width, height) { const dpr = window.devicePixelRatio || 1; canvas.width = width * dpr; canvas.height = height * dpr; canvas.style.width = width + 'px'; canvas.style.height = height + 'px'; const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); return ctx; }

后面所有的绘制工作,坐标值直接按CSS像素尺寸来写,不用再关心屏幕的物理像素密度。这个设置应该在项目第一天就做对,不然后期所有功能都显示正常了,唯独导出的图片总是不够清晰,排查起来非常痛苦。

4.2 第二步:拖拽、缩放、连线的调试过程

画布骨架搭好之后,我按“拖拽节点、缩放画布、连接节点、导出图片”这个顺序逐个功能过了一遍。拖拽的过程中,第一次调试就踩到了坐标换算的坑。当时写了拖拽逻辑,但屏幕坐标没有经过反缩放换算,直接把原始offsetX作为逻辑坐标用,导致当我用200%缩放查看画布时,鼠标哪怕点到了节点上,节点也会像受惊一样跳到很远的地方。排查这个问题花了我不少时间,最后通过打印鼠标的屏幕坐标和节点的逻辑坐标反复对比,才定位到是缩放因子没有参与换算。这个经历让我彻底意识到,坐标换算不是一个后期优化的细节,而是一开始就要设计周全的地基。

缩放功能做的是滚轮放大缩小,同时还要求能够拖动空白区域平移画布。滚轮事件本身不难拿到,难点在于缩放中心必须是鼠标位置。也就是说,当我把鼠标停留在画布某个区域滚动滚轮时,那个区域的画面内容应该保持不动,周围的图形围绕它放大或缩小。这个效果看着很自然,实现起来却需要小学数学外面的比例换算知识:缩放前后,鼠标在逻辑坐标上对应位置不能变。公式我在5.3节会给出完整推导。实现完这个功能之后,整个操作体验才真正像一个成熟的画图工具了,人坐在那里滚轮缩放、拖动节点,非常跟手。

连线功能的开发比预想中简单,因为前期的数据结构设计已经给边预留了存储空间,渲染逻辑也写好了。主要是加了两块东西:一个是在渲染节点时额外绘制锚点的小圆点,另一个是当用户从一个锚点拖拽到另一个锚点附近时,实时画一条跟随鼠标的临时连线。等鼠标落在另一个锚点上松开,就把这条临时连线固化成一条真正的edge。整个交互流程非常符合使用直觉。

4.3 第三步:数据结构设计给扩展性留后路

画图画到一半,我开始觉得单纯用矩形节点、直角风格还是不够灵活,想加圆角卡片风格的节点。这时候就体现出数据结构设计的价值了。因为节点在数据模型里只是一个对象,里面存了类型、坐标、宽高、样式,渲染函数根据节点的type字段去调用对应的绘制方法,新增一种节点类型完全不影响既有代码。扩展一种新的节点类型只需要两步:加一个type值,写一个对应的绘制函数并注册到渲染表里。我甚至把一个节点的样式单独拆出来作为style对象,包括填充色、边框色、圆角半径、阴影开关等,这样后续要做主题切换,只需要换一套样式对象即可。

这个设计也是我在写这个项目的过程中积累下来最值得推广的经验:图形编辑器这类项目,数据模型的重要性永远高于具体的渲染代码。做的任何功能,先把“这个东西在数据里长什么样”想明白,再去写绘制函数。数据模型稳定了,整个系统的稳定性就有了基本盘。

5. 常见问题与排查技巧实录

5.1 节点拖起来“乱飘”:坐标换算的三个常见错误

拖拽乱飘是新手做画布工具最容易遇到的问题,大多数情况是坐标换算三个环节中的某一个出了问题。

第一是没有把屏幕坐标换算成逻辑坐标,缩放比不等于1的时候,拖拽位移明显比鼠标快或慢,表现为“不跟手”。第二是忘记把节点位置自身加进拖拽公式,鼠标移动多少,节点也移动多少,看起来节点是在动,但和鼠标的相对位置每帧都在漂。第三是devicePixelRatio被混入了逻辑坐标计算,比如你在Canvas初始化用了ctx.scale(dpr, dpr),但事件处理里又直接用dpr参与坐标计算,导致高分屏上偏移严重。正确处理方式是:逻辑坐标始终按照CSS像素来,不要在事件换算中额外乘以dpr。

5.2 导出的图片发虚:设备像素比与导出分辨率

导出图片发虚几乎是Canvas工具避不开的坑。原因是Canvas的缓冲区尺寸可能只有CSS尺寸的设备像素比倍数,如果导出时直接把canvas.toDataURL拿过来用,那么在1倍屏上看起来还好,在2倍屏的显示器上看就全是锯齿。

我的做法是,导出时新建一个更大的临时Canvas,用目标尺寸重建坐标系,再在一次绘制过程中按照原有数据和视图状态完整渲染一份。很多人会尝试直接把原始canvas的context重新resize再画,但Canvas一旦resize,之前的绘制内容会被清空。正确方案是用数据模型重新渲染到新Canvas上,这再次印证了数据驱动渲染的必要性——只要模型在,导出什么尺寸都不愁。

5.3 滚轮缩放时画面停在了左上角:缩放中心的计算

滚轮缩放如果没有显式处理缩放中心,默认会围绕Canvas原点缩放,效果就是滚轮一滚,内容跑到左上角去了。要让内容围绕鼠标所在位置缩放,核心是保证缩放前后鼠标对应的逻辑坐标不变:

function zoomAt(mouseX, mouseY, newScale) { const oldScale = view.scale; // 鼠标在逻辑坐标上的位置 const logicX = (mouseX - view.offsetX) / oldScale; const logicY = (mouseY - view.offsetY) / oldScale; // 更新缩放比例 view.scale = clamp(newScale, minScale, maxScale); // 重新计算偏移,让鼠标所指的逻辑坐标回到原位 view.offsetX = mouseX - logicX * view.scale; view.offsetY = mouseY - logicY * view.scale; }

这里还要做两个数值上的保护:一个是缩放比例需要设置上下限,不然用户滚轮一甩,画面要么缩到看不清,要么放大到节点比屏幕还大,体验很糟;另一个是mouseX和mouseY在缩放前后要保持同一个意义,实际上就是上面这段代码里对offsetX、offsetY的重算逻辑,很多跑偏问题本质上就是这个公式写错了。

5.4 连线对不齐锚点:吸附阈值与最近点选择

在给连线做锚点吸附的时候,一开始我天真地按“遍历所有锚点,第一个距离小于阈值的就吸附”来写。实际用起来发现了两个问题:一是锚点密集时,明明想连的是下面这个节点,鼠标却被上面节点的锚点“吸跑”了;二是阈值设置过大时,鼠标在节点之间移动时连线会非常“神经质”地跳来跳去。

后来调整成两步策略:第一步,每次移动时把所有锚点距离都算一遍,只保留距离小于阈值的锚点;第二步,在候选中选择距离最近的那一个。同时把阈值从60像素降到20像素左右,吸附体验明显好了很多。这个“选最近而不是选最先命中”的思路,在做类似图形交互的命中逻辑时非常通用。

5.5 常见问题速查表

现象可能原因解决方法
拖拽节点不跟手屏幕坐标未换算为逻辑坐标统一通过screenToLogic函数转换后再计算位移
放大后画面内容偏移缩放中心未处理缩放时重算offsetX和offsetY,见5.3公式
高DPI屏幕文字发虚未处理devicePixelRatioCanvas初始化时按dpr缩放缓冲区
导出的PNG模糊直接导出了原Canvas用数据模型另起新Canvas,按目标尺寸重绘
连线吸附混乱锚点阈值过大阈值降到20px以内,只吸附最近锚点
画面拖动时“掉帧”高频事件触发重绘用requestAnimationFrame节流,而非每帧强制绘制

6. 最后分享几点做这类项目的真实心得

如果问我做diagram-design这一路下来,什么东西对自己的帮助最大,我的回答不是某个具体的技术实现,而是对整个图形编辑器项目结构的整体认识。最开始我沉浸在画线、画矩形这些细节里,做得越多越觉得数据模型才是一切的中心。节点、连线、视图状态,全部收敛到模型上之后,拖拽、缩放、连线、导出这些功能都变得顺理成章,它们只是在不同维度上修改同一份数据并触发重绘而已。如果你的项目也做到了一定阶段,觉得代码到处都是、改一个功能影响一片,我的建议是先停下新功能的开发,把数据模型理清楚。

再就是迭代顺序的取舍。如果让我重新做一次,我会按照“画布渲染基础 → 坐标换算 → 拖拽 → 导出图片 → 连线 → 缩放平滑优化”这个顺序来,而不是一开始就贪心想把所有功能一口气做完。坐标换算和画布基础是地基中的地基,先把它们做扎实,后面所有功能都会顺畅很多。

最后一个建议是给想动手做的朋友的,做这类工具不用追求功能大而全,能把“拖拽、连线、导出图片”这三件核心事情做好,就已经能覆盖日常绝大多数画图需求了。在这个基础上每扩展一个功能,都是对数据结构设计的一次验证和打磨,这种动手学到的经验是看十篇文章也换不来的。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进