简介:Fabric.js前端项目压缩包是一个面向Web前端开发者的Canvas图形编程资源,以HTML5 Canvas为基础,通过Fabric.js库将矩形、圆形、文本等元素封装为可操作对象,帮助开发者快速实现拖拽、旋转、缩放等交互效果。压缩包内共307个文件,主要以JS代码为主(149个js),并配有PNG、SVG等测试与演示素材,以及MD说明文档和构建相关配置,整体体积14.4MB,适合前端学习者与可视化项目开发者参考使用。已有415人学习下载。压缩包还涵盖SVG到Canvas的解析机制、JSDOM服务端模拟以及Node.js Canvas支持,能够帮助读者理解在非浏览器环境中运行Fabric.js的方法。通过学习其中的源码与示例,可掌握对象模型调用、Canvas事件处理、图形导出等核心技能,服务于数据可视化、在线绘图、游戏开发等应用场景。
1. 从原生 Canvas 到 fabric.js:前端图形编辑的第三次重写
做过前端图形项目的开发者大多有类似的经历:第一次用原生 Canvas 画矩形,第二次写拖拽和缩放,第三次面对一堆坐标计算和重绘逻辑,直接想把项目推翻重来。这个以 fabric.js 为核心的前端项目资源,正好解决了这一层痛点——它把 Canvas 变成了一个数据驱动的对象容器,你操作的是矩形、图片、文本这些对象,而不是像素坐标。无论是要做在线海报编辑器、户型图绘制工具,还是图像标注平台,这份资源里的代码骨架都能直接复用。适合的人群很明确:想绕开入门期大量坐标数学、直接进入业务功能开发的初中级前端工程师,以及需要快速产出原型验证逻辑的产品型开发者。
2. 对象模型与坐标系:fabric.js 的“数据驱动”骨架
2.1 Canvas 元素与 fabric.Canvas:静态画布如何变成对象容器
原生 Canvas 的工作方式很像一块黑板:getContext('2d')拿到画笔,然后fillRect()、drawImage()一路画下去,画完就没了,浏览器只保留像素结果。这块黑板不记得你画了什么,想要移动一个矩形,必须先clearRect()清屏,再重画所有图形。当图形数量超过几十个,或者需要频繁编辑时,这种模式很容易失控。
fabric.js 的处理思路完全不同。它用fabric.Canvas包装了原生<canvas>元素,内部维护了一个对象列表。每次你在画布上添加一个图形,fabric 会把底层逻辑“物化”成一个 JavaScript 对象:
const canvas = new fabric.Canvas('c', { width: 800, height: 600, backgroundColor: '#f5f5f5', preserveObjectStacking: true }); const rect = new fabric.Rect({ left: 100, top: 80, width: 160, height: 120, fill: '#4a90d9', rx: 8, ry: 8, stroke: '#1c3f60', strokeWidth: 2, cornerColor: '#4a90d9', cornerSize: 10 }); canvas.add(rect);这段代码做了三件事:第一行创建了一个fabric.Canvas实例,它接管了原生 canvas 元素c的渲染循环;第二段定义了一个矩形对象,left/top是它左上角在画布中的坐标,width/height是尺寸,fill是填充色,rx/ry是圆角半径;最后canvas.add(rect)把对象放进画布内部的对象列表。注意preserveObjectStacking这个参数——它保证对象在重绘时保持图层顺序,这在多对象交叠的场景下很关键。
从这一步起,你不再直接操作像素,而是操作对象。想移动这个矩形,改它的left和top属性;想换颜色,改fill属性;每帧的渲染和清屏由 fabric 内部自己处理。这意味着项目维护成本大幅降低:业务代码只需要关心“对象有哪些属性”,不需要关心“怎么把属性变成像素”。
2.2 核心对象族与公共基类:Rect、Circle、Text、Image 的共性
fabric.js 提供了一整套预制图形类,它们继承自同一个基类fabric.Object。我在实际使用中会把它们分成两组:一组是基本几何形状(Rect、Circle、Triangle、Ellipse、Polygon、Line),另一组是复合内容(Text 和 Image)。
// 圆形 const circle = new fabric.Circle({ radius: 50, left: 300, top: 200, fill: 'rgba(255, 190, 50, 0.8)', stroke: '#b8860b', strokeWidth: 3 }); // 文本 const text = new fabric.Text('标注内容', { left: 400, top: 300, fontSize: 24, fontFamily: 'Microsoft YaHei', fontWeight: 'bold', fill: '#333333', editable: false }); canvas.add(circle, text);因为所有图形都继承自fabric.Object,它们共享一套属性:left/top决定位置,scaleX/scaleY决定缩放,angle决定旋转角度,opacity决定透明度,originX/originY决定定位原点。这套统一的属性名非常值得记住——后续做序列化、批量修改属性时,都是遍历这套公共属性来操作。
一个容易忽略的细节是editable: false。对于文本对象,fabric 默认在双击时进入文本编辑模式,但如果你只是想做展示性质的标注,建议关掉它,否则用户双击画布时会被输入框打断操作节奏。这是一个小参数,但实际对交互体验影响很大。
2.3 序列化机制:为什么说这是可保存、可恢复的绘图结构
真正让 fabric.js 区别于普通 Canvas 封装库的,是它的序列化机制。调用canvas.toJSON()会把整个画布所有对象转成一个 JSON 结构;调用canvas.loadFromJSON(json, callback)则反向恢复画布。这个能力意味着画布状态可以存进数据库,也能随便发到服务端。
// 保存画布状态 function saveCanvasState(canvas) { const state = canvas.toJSON(['id', 'customData']); const jsonString = JSON.stringify(state); localStorage.setItem('editor_state', jsonString); } // 恢复画布状态 function restoreCanvasState(canvas) { const jsonString = localStorage.getItem('editor_state'); if (!jsonString) return; canvas.loadFromJSON(JSON.parse(jsonString), () => { canvas.renderAll(); canvas.requestRenderAll(); }); }toJSON()接收一个数组作为参数,这个数组指定额外需要序列化的自定义属性。上面代码传了['id', 'customData'],意思是除了 fabric 默认的坐标、颜色、尺寸等属性外,再把对象上的id和customData属性也一并序列化。这是多图层编辑器中很常用的做法——你有业务数据要挂到图形上,又不希望它们丢失。
loadFromJSON的第二个参数是回调函数,在画布渲染完成后触发。这里必须注意:loadFromJSON是异步操作,加载完成后需要手动调用renderAll()或requestRenderAll()刷新画布。很多初用者在加载后直接操作对象结果发现画布是空的,原因就是没有强制刷新。我自己的习惯是加载后立即调用requestRenderAll(),它会在下一个渲染帧统一处理,比renderAll()更高效。
3. 交互式编辑:选中、拖拽、缩放与旋转的实现逻辑
3.1 内置交互机制:免费的包围盒与操作手柄
fabric.Canvas默认就开启了对象交互能力,不需要额外编写监听器。用户点击对象,fabric 会生成一个包围盒,并在四角和四边显示控制手柄。拖动手柄可以缩放,拖动上方旋转手柄可以旋转,整个行为都在 fabric 内部完成。
这个机制的核心是fabric.Object上的controls属性。每个控制手柄本质上是一个fabric.Control配置对象,它定义了手柄的位置、触发的动作和光标的样式。默认配置下,四角手柄控制缩放,上方手柄控制旋转,但也可以通过自定义配置改变行为:
const rect = new fabric.Rect({ width: 200, height: 100, left: 100, top: 100, fill: '#7cb342', lockRotation: false, lockScalingX: true, // 锁定水平缩放 lockScalingY: false, // 允许垂直缩放 hasControls: true, hasBorders: true, cornerStyle: 'circle', transparentCorners: false }); canvas.add(rect);lockScalingX和lockScalingY这两个属性用于锁定某方向的缩放。在实际项目中,这个参数很重要——比如图片标注工具要求只能按固定比例缩放,或者某些图形的宽度不能随意改变。hasControls和hasBorders控制是否显示手柄和边框,如果为 false,用户就完全无法通过鼠标操作这个对象。cornerStyle决定手柄的样式是圆形还是方形,transparentCorners决定手柄是否半透明。
这套交互是开箱即用的,但它有一个特性值得注意:fabric 中对象的旋转中心默认在元素的中心点。如果你希望旋转中心贴合元素某个角点,需要修改originX和originY,并且要注意这两个属性在缩放时也会参与计算,改动后需要校准坐标。
3.2 事件系统:mouse:down、object:modified 与自定义事件
做交互编辑器时,事件系统是逃不掉的一环。fabric.js 的事件分为原生画布事件和对象事件两类。原生画布事件包括mouse:down、mouse:move、mouse:up,对象事件包括object:added、object:modified、object:selected等。object:modified是在对象拖拽、缩放或旋转完成(鼠标松开)后触发,这个事件是当前最频繁使用的回调。
canvas.on('object:modified', (e) => { const target = e.target; if (!target) return; console.log('对象被修改', { left: target.left, top: target.top, scaleX: target.scaleX, scaleY: target.scaleY, angle: target.angle, type: target.type }); // 更新右侧属性面板中的数值 updatePropertiesPanel(target); }); canvas.on('selection:created', (e) => { const selectedObjects = canvas.getActiveObjects(); console.log('选中了', selectedObjects.length, '个对象'); });事件回调的参数e上挂着e.target,也就是被操作的对象。通过它读取修改后的坐标、缩放、角度,用于实时同步到属性面板或者服务端保存。在选择多对象时,getActiveObjects()返回当前所有被选中的对象数组,这个情况的典型用途是批量设置颜色、批量删除等操作。
一个容易出问题的地方是重复绑定。某些项目代码在initCanvas()被反复调用时会创建多个fabric.Canvas实例,每个实例都绑定了事件监听器,导致一次操作触发多次回调。比较好的做法是在销毁画布时调用canvas.dispose(),它会把事件监听和画布元素都清理干净。没有这个习惯的话,内存泄漏会在对象增删频繁的编辑器中很明显。
3.3 选中态与多选:group 对象的拾取逻辑
fabric.Canvas上按下鼠标时,fabric 会遍历画布中所有对象做命中检测(hit-testing)。它默认按对象列表的倒序检测——最上层对象最先被检测,这样点击选中时,最上面的对象优先获得焦点。默认情况下矩形只检测边框和填充区域,如果要让透明区域也能被点选,可以设置perPixelTargetFind:
const canvas = new fabric.Canvas('c', { perPixelTargetFind: true });perPixelTargetFind开启后,命中检测精确到像素掩码,透明区域不会响应点击。这个参数对包含 PNG 透明图片或复杂 SVG 路径的场景很有用——不加的话,用户点图片的透明角落也会选中图片,体验很怪。但请注意,这个选项会带来额外的计算开销,对象很多时需要做取舍。
多选时 fabric 会把选中的对象组合成fabric.ActiveSelection,它是fabric.Object的子类,所以可以作为一个整体拖拽、缩放和旋转。缩放多选组时,组内对象的相对位置会根据组的包围盒等比变化。这个逻辑是 fabric 内置的,不需要额外处理,但要注意:多选组内的对象各自保有独立的scaleX/scaleY,如果在多选组缩放后再单独操作某个对象,它的坐标会带上组变换的修正,初次接触时容易对最终坐标感到困惑。
4. 实战:从零搭一个带工具栏的图片标注编辑器
4.1 初始化画布与基础状态管理
直接进入一张完整可运行的项目结构。下面的代码建立一个基础的标注编辑器入口,包含画布初始化、对象计数和画布背景设置。
// editor.js - 编辑器核心逻辑 class AnnotationEditor { constructor(containerId) { this.canvas = new fabric.Canvas(containerId, { width: 900, height: 600, backgroundColor: '#fafafa', preserveObjectStacking: false, enableRetinaScaling: true, selection: true, defaultCursor: 'default', hoverCursor: 'pointer', movingCursor: 'move' }); this.objectCount = 0; this.history = []; this._bindEvents(); } setBackground(url) { fabric.Image.fromURL(url, (img) => { img.set({ scaleX: this.canvas.width / img.width, scaleY: this.canvas.height / img.height, selectable: false, evented: false, excludeFromExport: true }); this.canvas.backgroundImage = img; this.canvas.requestRenderAll(); }, { crossOrigin: 'anonymous' }); } _bindEvents() { this.canvas.on('object:added', () => { this.objectCount++; document.querySelector('#obj-count').textContent = this.objectCount; }); this.canvas.on('object:removed', () => { this.objectCount--; document.querySelector('#obj-count').textContent = this.objectCount; }); } } const editor = new AnnotationEditor('canvas-wrap');setBackground是标注编辑器的核心功能之一:需要把上传的底图铺满画布作为标注参照。实现上通过fabric.Image.fromURL创建图片对象,然后分别计算scaleX和scaleY让它匹配画布尺寸。注意两个参数:selectable: false和evented: false,它们让背景图既不能被选中也不会响应鼠标事件,避免背景干扰标注操作。excludeFromExport: true则是让背景图在toJSON()导出时被排除,只保留标注数据本身——这个细节在有“保存标注结果”需求时很关键。
初始化时设置了多个光标类参数:defaultCursor是画布空白处的光标,hoverCursor是鼠标悬停在对象上的光标,movingCursor是拖拽对象时的光标。这些参数看起来不起眼,但直接影响编辑器的“手感”。
4.2 添加矩形、圆形、文本标注的完整链路
接下来实现工具栏的三个核心动作:加矩形、加圆形、加文本。每个动作的代码会带上差异化的参数,这是标注工具中很常遇到的定制需求。
addRect() { const rect = new fabric.Rect({ left: 50 + Math.random() * 100, top: 50 + Math.random() * 100, width: 180, height: 120, fill: 'rgba(74, 144, 217, 0.3)', stroke: '#4a90d9', strokeWidth: 2, strokeUniform: true, cornerColor: '#4a90d9', cornerSize: 12, transparentCorners: false, borderColor: '#4a90d9', objectCaching: false }); this.canvas.add(rect); this.canvas.setActiveObject(rect); this.canvas.requestRenderAll(); } addCircle() { const circle = new fabric.Circle({ left: 120 + Math.random() * 80, top: 80 + Math.random() * 80, radius: 60, fill: 'rgba(231, 76, 60, 0.25)', stroke: '#e74c3c', strokeWidth: 2, strokeUniform: true, originX: 'left', originY: 'top' }); this.canvas.add(circle); this.canvas.setActiveObject(circle); this.canvas.requestRenderAll(); } addText() { const text = new fabric.Text('双击编辑文字', { left: 80, top: 60, fontSize: 22, fontFamily: 'Microsoft YaHei', fill: '#2c3e50', editable: true, borderColor: '#e67e22', cornerColor: '#e67e22' }); this.canvas.add(text); this.canvas.setActiveObject(text); this.canvas.requestRenderAll(); }三个方法里有一些共性参数值得说明。strokeUniform: true是一个很容易被忽略的细节:默认情况下描边宽度会随对象缩放而等比变化,对象放大两倍后描边也变粗两倍;设为 true 后描边保持恒定像素宽度。做标注工具时一般期望标注框的边框粗细不随缩放改变,所以我会统一设置这个参数。objectCaching: false表示不对该对象做离屏缓存,图形数量少时可以关掉,后面在性能章再展开。
setActiveObject(rect)让新添加的对象立即变为选中状态。这样用户添加完标注后不需要再手动点一下才能调整位置,交互更顺手。三个方法最后都调用requestRenderAll()——这是 fabric 6.x 版本的标准做法,旧版用的是renderAll(),两者的差别是requestRenderAll会合并同一帧内的多次渲染请求,在高频操作下性能更好。
4.3 导出与保存:toDataURL 和 toJSON 的完整链路
标注结果最终要落到两种格式:PNG 图片用于预览、分享,JSON 数据用于回读编辑。下面这段代码把两条导出链写在一起,同时覆盖导入场景。
exportPNG() { // 临时移除选中态,避免导出图片中带手柄和边框 this.canvas.discardActiveObject(); this.canvas.requestRenderAll(); const dataUrl = this.canvas.toDataURL({ format: 'png', multiplier: 2, left: 0, top: 0, width: this.canvas.width, height: this.canvas.height, background: '#ffffff' }); const link = document.createElement('a'); link.download = `annotation_${Date.now()}.png`; link.href = dataUrl; link.click(); } exportJSON() { const json = this.canvas.toJSON([ 'id', 'labelName', 'labelColor' ]); const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' }); const link = document.createElement('a'); link.download = `annotation_${Date.now()}.json`; link.href = URL.createObjectURL(blob); link.click(); } importJSON(jsonString) { const parsed = JSON.parse(jsonString); this.canvas.loadFromJSON(parsed, () => { this.canvas.requestRenderAll(); // 重新绑定图片对象的跨域处理 this.canvas.getObjects().forEach(obj => { if (obj.type === 'image') { obj.setCrossOrigin('anonymous'); } }); }); }exportPNG里第一个动作是discardActiveObject(),这是导出图片的最常用操作。如果不调用,fabric 会把当前选中对象的控制手柄画进导出的图片里——这对交付成果来说是明显的瑕疵。toDataURL里的multiplier: 2控制导出分辨率,值为 2 表示导出两倍尺寸的图片,适合需要高清输出的场景。background参数指定导出图的背景色,不指定的话默认透明背景。
exportJSON里的toJSON传入数组,把id、labelName、labelColor这些自定义属性写进 JSON 里。这类标注业务数据是原生 fabric 对象没有的,必须显式声明才会被序列化。importJSON时我调用了setCrossOrigin('anonymous'),这一步处理的是图片导出到非本域时的 canvas 污染问题,后面在排查章节再详细展开。
5. 避坑与排查:五个高频故障及对应的诊断方法
5.1 控件的交互区域非常小,反复点不中对象
现象:在画布上添加了几个矩形,鼠标几乎要精确到边框才能选中,点击矩形内部完全无反应。元素一多,选中变成了体力活。
原因排查后是对象的fill属性。我用rgba(74, 144, 217, 0.3)这类半透明填充其实没问题,问题出在部分图形使用了fill: 'transparent'或根本没设置填充。fabric 默认的命中检测对完全透明区域是穿透的,无论边框多宽,内部都不会被识别为可点击区域。
解决方法是先检查 fill,如果设置了透明填充,可以忽略检测,但要注意:在fabric.Canvas上设置perPixelTargetFind: false(默认值)时,fabric 对无填充对象只检测边框。需要按大小响应区域同时要求范围内透明交互,可以使用transparentCorners配合strokeWidth加大真实命中范围。我在标注场景里更常用的方案是给透明图形增加一个非常淡的底色调,但视觉上又不影响底图观察。
5.2 导出 Png 出现空白或黑块,但画布看起来正常
现象:画布上添加了底图和标注框,toDataURL 导出的 PNG 要么全白,要么某个区域完全是黑块,画布显示却一切正常。
原因基本确定在图像跨域。当底图来自一个未设置 CORS 的第三方服务器时,浏览器会对 canvas 做“污染”标记,导致 toDataURL 读取像素失败。黑块则是图片尚未加载完成就出发了导出,canvas 把空像素区域解析为黑色。
解决分两步:第一,fabric.Image.fromURL时必须传crossOrigin: 'anonymous';第二,导出前用img.getSrc()校验图片的width属性是否真的大于 0,而不是只看对象存在于画布中。更稳妥的做法是给图片加上load事件回调,在图片onload后再启用导出按钮。如果底图来自自己的服务端,确保响应头里有Access-Control-Allow-Origin。
5.3 拖动对象时被意外触发点击事件
现象:用户从工具栏点了“矩形”,然后开始拖动调整位置,结果画布上不小心生成了一个新的对象。鼠标轻点也会误触。
原因处理是mouse:down与mouse:up的间隔判定。fabric 的mouse:down事件不区分点击还是拖拽,只要按下就会触发。我初始的代码把添加对象监听绑在mouse:down上,所以拖拽的第一步就已经生成了新对象。
解决方法是改用mouse:up且加上位移判断:记录mouse:down时的坐标,在mouse:up时计算位移差,只有位移小于 3px 才视为“点击”并添加对象。另外需要检查 canvas 的skipTargetFind是否为 false,为 true 时所有对象都不可选中,会出现点击无反应,也容易造成这类误判。
5.4 用 loadFromJSON 恢复后,图片全部拿不到
现象:JSON 导出成功,importJSON 后坐标、缩放都在,但所有图片对象都变成空白占位。控制台没有任何报错。
原因卡在loadFromJSON的异步特性。它不是同步恢复,图片对象内部需要重新异步加载资源。如果 callBack 里马上操作对象,图片数据还没就绪,访问img.width或尝试导出就会看到异常值。另一个常见情况是图片的 src 是相对路径,页面从其它入口加载时路径失效。
解决方式是给图片重新绑定加载事件。我一般在loadFromJSON的回调里遍历所有 image 对象,调用image.setSrc(image.getSrc(), () => { ... })触发重新加载。对于需要保证“恢复后能立即导出”的需求,最好将图片资源改为 base64 或同域 url,并在加载回调里打标记,等待全部就绪后再允许导出操作。
5.5 高频调用 add/remove 操作导致画布卡顿掉帧
现象:编辑器支持用户连续添加多个标注框,点一次就生成一个对象,主线程明显卡顿,FPS 下降明显。
原因分析是 fabric 默认每个对象都会做离屏缓存(objectCaching)。对象几何、样式变化后,fabric 要重新生成缓存快照,频繁 add/remove 导致大量缓存失效和重建。
解决方式分两个方向:一是临时缓存优化,对高频操作的画布设置canvas.dispose()前的缓存回收,或者对单个对象设置objectCaching: false,让它实时重绘不缓存;二是控制渲染频率,canvas.requestRenderAll()会合并同一帧内的多次请求,所以连续 add 时可以合并所有变更后统一调用一次,并用renderOnAddRemove: false避免每次增删都自动触发重绘。增加对象后手动requestRenderAll(),对象少时影响很小,但我通常建议在新增操作里显式关闭renderOnAddRemove,改为在批量操作结束后统一渲染。
6. 性能优化与自定义控件:让编辑器再快一步
6.1 渲染与缓存的四个控制开关
fabric.js 对高频重绘场景提供了几个关键开关,合理组合能在实际体验上拉开梯度,用得对可以让编辑器在元素密集时保持手感。
renderOnAddRemove: false可以避免频繁添加删除对象时自动重绘;skipTargetFind在不需要鼠标选中对象时关闭命中检测;objectCaching控制单个对象是否使用离屏缓存;enableRetinaScaling控制高分屏下是否按 devicePixelRatio 做像素映射。
// 高频操作的优化配置示例 const canvas = new fabric.Canvas('c', { renderOnAddRemove: false, // 增删对象不自动重绘 skipTargetFind: false, // 需要交互时保持开启 enableRetinaScaling: true, // 保持高分屏清晰度 selection: true }); // 批量添加对象后统一渲染 function addBatchRect(count) { for (let i = 0; i < count; i++) { const rect = new fabric.Rect({/* 参数 */}); canvas.add(rect); } // 手动触发一次渲染,合并所有绘制动作 canvas.requestRenderAll(); }requestRenderAll()自动把同一事件循环内的多次请求合并,这是多对象批量导入期间不会掉帧的主要原因。如果你重写为renderAll(),每一次 add 都会强制同步绘制,卡顿会很明显。
6.2 自定义控件:给标注框加一个删除按钮
fabric 自带的控件只有缩放、旋转之类,但标注工具几乎总要一个删除入口。与其在外部用点击事件判断,不如直接在对象的控制柄列表里加一个自定义控件,坐标跟随对象移动,点击时直接触发删除回调。
// 定义一个名为"删除"的自定义控件 const deleteControl = new fabric.Control({ x: 0.5, // 位置相对对象中心点的偏移比例 y: -0.5, offsetX: 0, offsetY: -24, // 向上偏移 24px,手柄放置在右上角偏上 cursorStyle: 'pointer', sizeX: 24, sizeY: 24, cornerSize: 24, actionHandler: () => {} }); fabric.Object.prototype.controls.deleteControl = deleteControl; // 在事件回调中捕获点击 canvas.on('mouse:down', (e) => { if (e.target && e.target.controls && e.target.controls.deleteControl) { // 判断点击位置与 deleteControl 的距离 const pointer = canvas.getPointer(e.e); if (Math.abs(pointer.x - e.target.getCenterPoint().x) < 20 && Math.abs(pointer.y - (e.target.getCenterPoint().y - 24)) < 20) { canvas.remove(e.target); } } });fabric.Control的核心参数是x和y,它们表示控件锚点相对于对象中心点的偏移比例。x: 0.5, y: -0.5表示把控件放在对象的右上角;offsetX和offsetY再做像素级微调,让删除按钮悬停在对象外部上方。actionHandler原本是控件拖拽时的处理逻辑,删除功能用不到,所以传一个空函数占位。
用这种方式做自定义按钮有个天然优势:按钮位置会随对象缩放、旋转自动更新,不需要在每次object:modified后手动计算坐标。这个模式在前端图形编辑器的二次开发中非常常用,尽量保留这个思路。
6.3 对象数量过大的降级方案与性能边界
当画布中对象数量超过几百个时,只靠缓存优化还是不够。我遇到过的问题是画布上有大约 600 个标注框时拖拽明显迟滞,用手持设备直接卡顿。此时通常需要按照实际场景选择降级策略。
第一个方式是关闭对象缓存并优化渲染循环,这只对内存占用有帮助,对帧率改善有限。第二个方式是用服务端合成图片替换 client 对象渲染。把标注结果做 JSON 保存,同时由服务端渲染一张静态 PNG 作为预览,前台只读取图片不回放全部对象。第三种是把画布切分成多个区域,只在视口内渲染可见对象,但 fabric 并不直接支持视口裁剪,实现成本比较高。
所以我对这类项目的建议是:标注编辑器尽量只保留当前编辑需要的对象,超出阈值时把历史标注数据归档到 JSON 文件,预览通过导出的 PNG 完成,不要试图让 fabric 在浏览器里长期承载大量图形对象。从那以后,我每次做图形编辑器都会先定一个性能上限预算,用文档或者注释写明:什么时候只渲染近区域对象、什么时候直接切换静态底图,拒绝在画布上堆无上限的元素。这对前端图形项目的长期维护帮助很大,希望帮到你。
本文还有配套的精品资源,点击获取