简介:此压缩包提供基于Fabric.js的前端项目源码,面向需要在网页画布上实现复杂交互图形的中高级开发者。Fabric.js用面向对象模型封装了原生画布接口,将矩形、圆形、文本等视为独立对象,可高效完成添加、删除、旋转、缩放等操作;同时内置SVG解析能力,可直接将矢量图形导入画布,并借助JSDOM与Node.js画布支持,在服务器端也能生成图形预览或导出图片。资源共307个文件,其中包括149个JavaScript文件(主要为核心功能与示例)、86个PNG图片、40个SVG矢量图,以及文档、配置和字体等辅助文件,压缩包总大小约14.4MB,目录结构清晰,便于按模块查阅。目前已有415人学习下载,可应用于数据可视化、在线绘图工具、用户界面设计等场景。学习后能够掌握Fabric.js的对象操作、事件绑定、SVG转换及服务端渲染等关键技能,为构建可拖拽、可缩放、可旋转的交互式图形应用提供一套可直接参考的实现方案。
1. 前端项目 fabric.js 在解决什么问题:不是画图,是让画布上的对象可交互
接到一个“在线海报编辑器”的需求时,第一反应往往是拿原生 Canvas 画矩形、画图片。画到第三天就会发现,鼠标拾取要自己算矩阵,拖拽要自己做坐标变换,选中框要自己画,重绘逻辑越堆越乱。fabric.js 解决的核心不是“把图形渲染到屏幕上”,而是把 Canvas 从“画完就忘的位图”升级成“可维护、可交互、可序列化的对象集合”。标题里的“前端项目 fabric.js”,指的就是围绕这个库搭建的可交互画布工程。它适合在线编辑器、标注工具、商品配置器、图形化白板这类需要用户直接在画布上拖拽、缩放、旋转元素的场景。理解了这一点,后面所有代码和参数才有落脚点。
2. 对象模型与渲染管线:先弄清 fabric.js 在浏览器里帮我们扛住了什么
2.1 从“画图指令”到“对象集合”:fabric.js 的核心抽象
原生 Canvas 是一套命令式 API:fillRect、arcTo、strokeText执行之后,像素留在画布上,程序里并没有“这个矩形”的概念。要做选中,只能自己维护一份对象数组,每次重绘时遍历数组,把几何变换、样式、图层顺序全部重新算一遍。这个模式对简单的图表展示够用,但做到“用户能拖、能改、能保存”的编辑器级别,复杂度会迅速失控。
fabric.js 的做法是把每个图形封装成fabric.Object的子类实例:fabric.Rect、fabric.Circle、fabric.Path、fabric.Image、fabric.IText。每个对象自带left、top、width、height、angle、scaleX、scaleY、fill、stroke等属性。你在画布上add(rect)之后,拖动它,fabric 内部会自动更新left/top并触发重绘,业务代码不需要碰任何坐标矩阵。
const rect = new fabric.Rect({ left: 100, top: 60, width: 120, height: 80, fill: '#4a7ef0', rx: 6, angle: 15 }); canvas.add(rect); rect.set('fill', '#ff6b6b'); canvas.requestRenderAll();逻辑说明:这段代码做完,画布上就出现了一个可以选中、拖拽、旋转、缩放的矩形。set修改的是对象属性,requestRenderAll通知画布在下一帧刷新。angle是对象绕自身中心旋转的角度,rx是只有矩形类型才有的圆角半径,left/top是相对画布左上角的坐标。
选择 fabric.js 而不是其他方案,最直接的理由是“对象模型 + 序列化”是一体的。同类库中有的渲染性能更强、结构更轻,但 fabric 把toJSON/loadFromJSON、SVG 解析、选中框控制点都内置了。做一个中后台编辑器,这些能力能省下至少一周的造轮子时间。它的性能上限不如专门为高频渲染设计的库,但常规业务场景下完全够用。
2.2 按需渲染与对象缓存:为什么一百个对象拖动还不算卡
fabric.js 不是游戏引擎,不会每帧循环全量绘制。它维护了一套脏标记机制:requestRenderAll在requestAnimationFrame回调里统一执行renderAll;渲染时,大部分对象会先生成到离屏缓存上,主画布只负责合成。对象属性被修改后,fabric 会把该对象的缓存标记为 dirty,下次合成只更新这一个对象。
用一个事件验证这个机制最直观:
canvas.on('object:modified', (e) => { const obj = e.target; console.table({ left: obj.left, top: obj.top, angle: obj.angle, scaleX: obj.scaleX, scaleY: obj.scaleY, dirty: obj.isCacheDirty() }); });逻辑说明:object:modified在鼠标释放后触发,打印出拖拽或缩放结束后的最终变换参数。isCacheDirty()为 true 表示离屏缓存待重建。如果拖拽过程中这个值一直为 true,说明缓存策略没有生效。
参数配置上,new fabric.Canvas(el, { renderOnAddRemove: true })默认在add/remove时自动请求重绘。批量添加大量对象时,我一般会先把它设为false,全部添加完再手动requestRenderAll一次,避免中间多次无意义渲染。另外preserveObjectStacking默认是false,意味着对象选中时会被临时提到画布顶层显示,层级视觉会跳动,做编辑器通常要设为true。这里存在一个常见误用:把 fabric 当游戏引擎用,手动开一个setInterval循环渲染,完全没必要,反而会打断内部缓存机制。
静态展示场景下,用fabric.StaticCanvas替代fabric.Canvas更省:它没有选中、没有交互命中检测,渲染路径更短。判断标准很简单——画布上的东西要不要被用户点选,要就 Canvas,不要就 StaticCanvas。
3. 把 fabric.js 项目跑起来:一份最小可复现的工程模板
3.1 工程结构与引入方式
解压一个 fabric.js 前端项目包,常见做法是这样组织文件:
fabric-project/ ├── index.html ├── css/ │ └── app.css ├── js/ │ ├── fabric.min.js │ ├── app.js │ └── toolbox.js └── assets/ └── images/fabric.min.js是第三方库文件,app.js里初始化画布,toolbox.js挂工具栏按钮事件。选择本地引入还是 CDN,看实际部署环境。内网或离线环境必须用本地包;有构建体系的项目直接安装 fabric 包再import,官方也提供 ESM 版本。我一般会按项目里锁定的版本来跑,不随手升大版本,因为 fabric 不同大版本之间的 API 差异不小,升级往往伴随对象行为变化。
HTML 侧只需要一个canvas标签,fabric 会用它创建实例:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="css/app.css"> <script src="js/fabric.min.js" defer></script> <script src="js/app.js" defer></script> </head> <body> <div id="toolbar"> <button>const canvas = new fabric.Canvas('editor', { preserveObjectStacking: true, renderOnAddRemove: false, selection: true, backgroundColor: '#f5f6f8', enableRetinaScaling: true });参数作用与选型:preserveObjectStacking设为 true,防止选中对象时层级视觉跳动;renderOnAddRemove设为 false,批量操作时手动控制渲染节奏;selection开启框选;backgroundColor是画布底色,会进入序列化和导出,适合编辑器保持统一底色;enableRetinaScaling在高分屏上不能关,否则文字和线条明显发虚。实测在 Chrome 和 Firefox 上,这些配置的交互表现基本一致,主要差异在触控板的滚轮事件频率,不影响正确性。
3.3 第一个可交互对象与画布缩放平移
往画布上添加对象,并且让画布支持缩放平移,编辑器的基础能力就齐了:
const rect = new fabric.Rect({ left: 120, top: 80, width: 160, height: 100, fill: '#2f7ef7', rx: 4, stroke: '#ffffff', strokeWidth: 2, cornerStyle: 'circle', cornerColor: '#2f7ef7', transparentCorners: false }); canvas.add(rect); canvas.requestRenderAll(); canvas.on('mouse:wheel', (opt) => { const delta = opt.e.deltaY; const zoom = canvas.getZoom() * Math.pow(0.999, delta); if (zoom > 0.05 && zoom < 20) { canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom); } });逻辑说明:add之后矩形就自带完整的交互能力——拖拽移动、四角控制点缩放、旋转手柄。mouse:wheel里用zoomToPoint以鼠标位置为锚点缩放,体验接近桌面设计软件。限制缩放范围在 0.05 到 20 之间,避免缩太小找不到对象,或者放太大拖动困难。
参数说明:cornerStyle、cornerColor、transparentCorners控制选中框控制点外观;strokeWidth会影响矩形实际占用的包围盒尺寸;deltaY的符号在 Windows 鼠标和 macOS 触控板上的方向习惯不一致,团队项目里最好统一封装一个方向处理函数。还要注意区分内容缩放和画布缩放:add进去的对象坐标和viewportTransform是两套坐标系,后续做拖拽命中时要留意这个差异。
4. 让画布状态可保存、可导出:序列化与图片输出的工程化姿势
4.1 用 toJSON 与 loadFromJSON 做存档恢复
编辑器最基本的要求是“用户上次编辑的内容,下次打开还在”。fabric 提供了一套完整的序列化方案,核心就两个方法:
function saveSnapshot() { const payload = canvas.toJSON(['id', 'name', 'data']); localStorage.setItem('editor-snapshot', JSON.stringify(payload)); } function restoreSnapshot() { const raw = localStorage.getItem('editor-snapshot'); if (!raw) return; const json = JSON.parse(raw); canvas.clear(); canvas.loadFromJSON(json, () => { canvas.requestRenderAll(); }); }逻辑说明:toJSON默认序列化每个对象的几何属性、填充、描边、透明度、旋转角等信息,额外传入的数组会把自定义业务字段也带进 JSON,比如id、name、data,后续恢复时才能把图形和业务数据对上。clear()会重置画布,背景和相关变量需要重新赋值,因为background也在序列化结果里。
注意:
loadFromJSON的回调必须保留。图片加载是异步的,不在回调里requestRenderAll,会出现画面空白或者旧画面没刷新的“假死”现象。排查这类问题先看回调有没有写,比查任何配置都管用。
4.2 导出 PNG / JPEG / SVG 的参数设置
导出能力决定了编辑器产出的文件能不能用于打印或投放。fabric 的toDataURL和toSVG都有几个关键参数:
function exportPNG() { const dataUrl = canvas.toDataURL({ format: 'png', quality: 1, multiplier: window.devicePixelRatio || 1, left: 0, top: 0, width: canvas.getWidth(), height: canvas.getHeight() }); const a = document.createElement('a'); a.download = 'export.png'; a.href = dataUrl; a.click(); }逻辑说明:multiplier是导出分辨率倍率,设为 2 就相当于把逻辑尺寸放大一倍输出,适合高分屏预览或印刷。left/top/width/height组合起来可以只导出画布局部区域。
参数对照表:
| 参数 | 可选值 | 说明 |
|---|---|---|
| format | png / jpeg | jpeg 没有透明通道,纯色背景场景下文件体积更小 |
| quality | 0 到 1 | 仅 jpeg 生效,png 会忽略 |
| multiplier | 1 / 2 / 3 | 导出倍率,建议 2,过大容易在低端设备上白屏 |
| left/top/width/height | 数字 | 裁剪导出区域,默认整张画布 |
SVG 导出用canvas.toSVG(),返回的是 SVG 字符串,包一层Blob下载就不会出现位图锯齿。这里有一个常见坑:画布上存在跨域图片时,toDataURL会被浏览器拦截或导出空白。另外,多标签页同时打开编辑器时,localStorage快照可能互相覆盖,稳妥做法是每次保存带时间戳版本号。
4.3 删除、复制与图层顺序操作
选中态操作是编辑器的高频路径,删除、复制、调整层级这三件事必须做得干脆:
function deleteActive() { const active = canvas.getActiveObject(); if (!active) return; canvas.remove(active); canvas.discardActiveObject(); canvas.requestRenderAll(); } function duplicateActive() { const active = canvas.getActiveObject(); if (!active) return; active.clone((dup) => { dup.set({ left: active.left + 16, top: active.top + 16 }); canvas.add(dup); canvas.setActiveObject(dup); canvas.requestRenderAll(); }); } function bringToFront() { const active = canvas.getActiveObject(); if (!active) return; canvas.moveTo(active, canvas.getObjects().length - 1); }逻辑说明:getActiveObject取当前选中对象。clone是异步的,必须在回调里操作,否则复制出来的对象属性不完整。moveTo的第二个参数是目标索引,length - 1就是顶层。多选时getActiveObject返回一个Group,此时删除的是整个组。
漏掉discardActiveObject是这里最容易翻车的点:删除对象后,工具栏还停留在“选中态”样式,点空白区域也不会取消。这个交互状态不同步的问题,比渲染问题更容易被测试发现,也更容易被忽略。
5. fabric.js 避坑指南:高清屏、跨域与状态恢复的四个实战问题
5.1 导出图片发糊:devicePixelRatio 没乘进去
现象:画布在 2 倍屏上显示很清晰,导出 PNG 后在手机或大屏上打开明显发虚,文字边缘全是锯齿。
原因:fabric 的toDataURL默认按逻辑像素导出。屏幕显示时浏览器按 CSS 尺寸拉伸,导出却只输出了一倍图的分辨率。
解决:导出时把multiplier设为window.devicePixelRatio,或者固定为 2:
const dataUrl = canvas.toDataURL({ format: 'png', multiplier: window.devicePixelRatio || 1 });同时注意multiplier不要无脑调大,4 倍以上导出在中低端移动设备上很容易内存爆掉白屏,建议限制最大 3。这个参数只影响导出分辨率,不影响画布当前显示效果。
5.2 背景图能显示但导出空白:Canvas 被跨域图片污染
现象:页面里加载了某个图床的图片,画布上显示完全正常;一调用canvas.toDataURL就报SecurityError,或者导出 PNG 中图片区域是空白。
原因:浏览器安全策略禁止把未通过 CORS 校验的跨域图片绘制进可导出画布。fabric.Image.fromURL默认不带crossOrigin,图片一上画布,画布就被标记为“受污染”。
解决:加载远程图片时显式带上crossOrigin: 'anonymous',图片服务器同时要返回Access-Control-Allow-Origin响应头。本地调试记得用http://localhost起服务,直接用file://打开 HTML,同源策略的表现会更隐蔽,排查难度直线上升。
fabric.Image.fromURL('https://example.com/bg.jpg', (img) => { img.set({ left: 0, top: 0 }); canvas.add(img); }, { crossOrigin: 'anonymous' });最省心的方案是把图片先转成 base64 再加载,彻底绕开跨域限制,但 base64 会明显增大内存和序列化体积,只适合小尺寸素材。
5.3 loadFromJSON 之后对象不能选中:事件绑定是代码态,不是数据态
现象:存档恢复后,画布上的图形都在,但点击图形不出现选中框,拖动无反应,工具栏里的修改操作全部失效。
原因:fabric 的 JSON 只序列化对象属性,不序列化事件监听。如果业务代码里对某个对象实例直接绑定了obj.on(...),恢复出来的对象是一批全新实例,之前的绑定全部丢失。
解决:从源头统一改成 canvas 级事件代理,在selection:created、object:modified这类事件里按对象类型分发业务逻辑:
canvas.on('selection:created', (e) => { const obj = e.selected[0]; if (obj.type === 'textbox') { // 更新文本编辑工具栏 } else { // 更新样式面板 } });或者用loadFromJSON的第三个参数reviver在恢复对象时重新挂自定义类和事件。推荐事件代理方案,因为只写一份绑定逻辑,恢复后不需要重新初始化。
5.4 对象一多拖动就卡:命中检测和缓存策略要一起调
现象:画布上对象数量到 300 左右,拖动任意对象都能感觉到掉帧,框选时卡顿最明显。
原因:fabric 的交互命中检测会遍历所有selectable对象计算变换矩阵,对象多开销就大;而大图对象默认开启离屏缓存后,缓存本身占用内存,频繁修改样式还会导致缓存反复失效。
解决:分层处理。
function optimizeForLargeScene() { canvas.getObjects().forEach((o) => { o.selectable = false; o.evented = false; o.objectCaching = true; }); canvas.requestRenderAll(); }把不参与编辑的对象设为evented: false,命中检测会直接跳过;局部区域用Group打包,交互和序列化都按一个单元处理;超大图可以关闭objectCaching,缓存一张大离屏图反而比直接绘制更慢。超过 500 个对象时,优先考虑静态渲染加局部覆盖层的方案。
6. 用三个自查动作验证 fabric.js:序列化、性能与吸附网格
6.1 在 console 里检查序列化状态
const json = canvas.toJSON(); console.log(json.objects.length, json.objects[0]);打开第一个对象,检查是否残留了异常的scaleX/scaleY。用控制点缩放对象后,fabric 会把缩放记录在scale上而不是改写width/height,这是正常行为;但如果用户反复缩放,累计的 scale 值会越来越奇怪。规范做法是在编辑结束时调用obj.set({ width: obj.width * obj.scaleX, height: obj.height * obj.scaleY, scaleX: 1, scaleY: 1 }),把缩放归一化进宽高。
6.2 录一段性能轨迹
DevTools 的 Performance 面板里,拖动画布三秒,看 Rendering 耗时占比。如果接近满帧,先把evented: false的对象调出来对比;如果明显好转,说明卡顿在命中检测而不是渲染。这个动作能快速定位“到底是谁拖慢了画布”的争论,比凭感觉猜可靠得多。
6.3 用吸附网格验证鼠标坐标链路
canvas.on('object:moving', (e) => { const obj = e.target; const grid = 10; obj.set({ left: Math.round(obj.left / grid) * grid, top: Math.round(obj.top / grid) * grid }); });能按 10px 网格干净吸附,说明鼠标坐标、对象变换、重绘这条链路是通的。吸附逻辑我会做成一个可开关的全局配置,避免影响自由排版场景。这是编辑器里一个小而实用的功能,也是验证坐标正确性的最直接手段。
曾经有一次交付前发现所有对象恢复后位置错位,排查到最后发现是loadFromJSON回调里漏了requestRenderAll。从那以后,序列化恢复三步走——clear、loadFromJSON、回调里renderAll——就成了我的固定模板。希望帮到你。
本文还有配套的精品资源,点击获取