ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI写图形库:90%代码秒出,剩下10%才是真正考验

AI写图形库:90%代码秒出,剩下10%才是真正考验 最近有朋友问我AI 到底能不能写图形库他看我最近一直在折腾一个内部数据可视化的项目还以为我是全靠 AI 在写代码。我说你这话得分两半回答要问能不能用 AI 写出图形库的大头答案是可以而且快得离谱要问把整个图形库丢给 AI 就完事那答案是我差点被剩下那 10% 坑出心理阴影。事情是这样的我用 AI 辅助开发了一个纯前端的轻量图形库主要做折线图、柱状图、散点图、饼图这类基础图表跑在 Canvas 2D 上不依赖任何第三方图表框架。整个过程中 AI 大概帮我写完了 90% 的代码量从绘图原语到坐标变换再到动画框架速度确实让我这种写惯了业务代码的人有点恍惚。但真正上线前的那几天我几乎全耗在剩下那 10% 上高分屏适配、数据边界防御、事件命中检测、动画资源释放、文字布局错位……每一个都像埋在代码里的雷AI 根本不会主动替你想。今天就把整个过程掰开揉碎讲一遍给所有想用 AI 写图形库、或者已经在坑沿上试探的朋友做个参考。1. 项目概述与整体思路1.1 这个图形库到底要做什么先交代背景。我们内部有个运营看板之前一直用的是开源图表库但需求越来越偏门要自定义交互动效、要跟内部设计规范完全一致、要砍掉我们用不到的一大半功能让首包体积下来。换个成熟图表库当然也行但改起来更费劲不如自己写一个够用的轻量库只保留折线图、柱状图、散点图、饼图四种基本图表加上缩放、tooltip、图例交互就够了。这个定位很重要千万别上来就想做一个小 ECharts。图形库这个东西功能范围一旦放大复杂度是指数级上升的。我跟 AI 协作的前提就是先把这个范围死死锁住不碰 3D、不做 WebGL、不做大数据量的热力图、不做拖拽编辑。这样 AI 写出来的代码才会可靠后面你自己补坑的时候也不会绝望。技术上选的是 Canvas 2D。原因很简单SVG 在处理大量节点时 DOM 开销太大WebGL 对我们这种基础图表又杀鸡用牛刀。Canvas 2D 在中等数据量下性能足够API 也直白最关键的是 AI 对 Canvas 2D 的掌握程度非常高训练语料里这方面的代码太丰富了所以它写出来的前几版代码基本靠谱。1.2 为什么敢让 AI 来写核心代码我承认一开始是有点冒险的。图形库和普通业务页面不一样它涉及大量数学计算、坐标变换、状态管理还有 Canvas 上下文这种一旦搞错就全是黑屏的东西。但我的判断是正因为 AI 见过海量的绘图代码它在这类任务上的表现反而比写复杂业务逻辑要稳。实际操作下来确实如此。AI 生成的绘图原语、比例尺计算、基础动画循环拿过来稍作修改就能跑。我甚至让 AI 先写了几十个针对数据映射的单元测试它的输出正确率高到让我怀疑是不是从哪个开源项目里背下来的。但我也很清楚它的问题在哪AI 写代码是按模式匹配不是按语义理解。它能写出ctx.beginPath()加ctx.stroke()但它不会意识到你的图表在 2 倍屏上会发虚它能写出requestAnimationFrame动画但它不关心组件卸载后这个循环还在不在跑。这些属于从没出过 bug 就不会想到的经验问题恰好是 AI 最缺的部分。所以我的策略是AI 负责 90% 的从无到有我负责 10% 的从对到稳。这也直接引出了整篇文章的核心——那 10% 到底是什么怎么补。1.3 架构设计一开始就把分工想清楚动笔之前我花了一个晚上把整个库分成三层数据层负责接收和清洗原始数据计算层负责把业务数值映射成画布坐标渲染层负责真正调用 Canvas API 画出来。交互单独拎出来挂在渲染层后面不跟核心绘图逻辑混在一起。这个分层对 AI 协作来说无比重要。我只需要把每一层的接口定义好AI 就能在接口约束内生成实现不会越界乱写。而且一旦哪一层出了问题定位范围非常小我能精确到是计算层的问题然后把那一小段代码单独丢给 AI 修而不是把整个库甩给它让它瞎猜。接口定义我用了非常死板的 JSDoc 风格但效果出奇好。AI 看到这种严密类型说明生成的代码完成度明显更高少了很多它自己发明接口的情况。这一点在后面我会详细展开。2. 技术方案与实现路径2.1 数据层先把脏数据挡在门外图形库的第一个坑其实不在绘图在数据。真实业务数据永远不会像教程里的数组那么干净可能出现空值、单个数据点、所有值相等、负值、非数字、极大极小值混在一起。如果数据层不做清洗后面计算层和渲染层怎么补都补不干净。我给 AI 定的数据层接口是输入任意数组输出统一规格的序列数据结构包含name、data、color其中data是已经过滤掉非法值的数组同时记录下哪些位置是空值供折线图断线使用。AI 写出来的清洗函数中规中矩但有一个隐藏 bug它默认数据都是数值型遇到字符串数字会直接返回 NaN。我后来加了类型转换和严格校验这才算把门守住。这个环节的经验是数据层的校验规则必须由人来定不能交给 AI 自由发挥。AI 只会按它见过的最常见场景写但真实数据的丑陋程度远超它的想象。你可以让 AI 实现你定好的规则但规则本身一定要来自你对自己业务的了解。2.2 计算层坐标变换是图形库的心脏计算层是整个库的命门也是最容易出数学坑的地方。核心是把业务数据域比如销售额 0 到 1000 万映射到画布像素域比如 x 从 60 到 540。这个映射关系叫比例尺我用一个简单的类来实现。class LinearScale { constructor(domain, range) { this.d0 domain[0]; this.d1 domain[1]; this.r0 range[0]; this.r1 range[1]; const span this.d1 - this.d0; this.k span 0 ? 0 : (this.r1 - this.r0) / span; this.mid (this.r0 this.r1) / 2; } map(v) { if (this.k 0) return this.mid; return this.r0 (v - this.d0) * this.k; } }这里我特意让 AI 处理了span 0的情况也就是数据最大值和最小值相等的时候。这是 AI 最容易忽略的边界条件如果不处理映射结果全是 NaN图表直接白屏。类似的边界还有空数组、单点数据、全部是负值、包含Infinity。我在定义接口时把这些情况一条条列成注释AI 大部分能接住但接不住的我后面还得自己补。坐标变换设计好之后折线图的路径生成就很简单了遍历数据点每个点做一次scale.map()得到画布坐标然后连起来。柱状图也类似只是把 x 坐标换成柱子的中心位置再根据柱宽算出左右边界。这段代码 AI 跑得飞快几乎没返工。2.3 渲染层Canvas 绘制的核心套路渲染层是我觉得 AI 最出彩的部分。它生成的绘制函数结构非常标准先save()然后设置样式绘制最后restore()。这个习惯对 Canvas 开发至关重要因为 Canvas 上下文是个全局状态机不恢复现场的话上一个图形的样式会污染下一个。function drawLine(ctx, points, style) { ctx.save(); ctx.beginPath(); ctx.strokeStyle style.stroke; ctx.lineWidth style.lineWidth; ctx.lineJoin round; ctx.lineCap round; points.forEach((p, i) { if (i 0) { ctx.moveTo(p.x, p.y); } else { ctx.lineTo(p.x, p.y); } }); ctx.stroke(); ctx.restore(); }还有个细节是折线图的空值断线。如果数据中间有个null折线不应该硬生生连过去而应该断开。AI 第一版没处理这个我让它改成遇到空值就beginPath()重新开始一段新路径这才符合真实图表的预期。这段代码很小但属于图表面向用户的基本素养AI 不会自觉想到。我把渲染层的公共代码抽了几个工具函数绘制网格线、绘制坐标轴文字、绘制图例。坐标轴文字这里有个暗坑Canvas 的fillText是按文字基线对齐的想让文字居中必须用textBaseline middle配合measureText计算宽度。AI 生成的文字绘制经常偏上或偏左肉眼看着不明显但截图对比就很扎眼。2.4 交互层命中检测与坐标换算图表不只需要画还需要响应用户操作。鼠标移到数据点上时展示 tooltip点击柱子时触发回调这些都需要做命中检测。我选择的是最简单的思路在事件监听里遍历所有图元计算鼠标位置与图元的距离小于阈值就算命中。这里第一道坎是坐标换算。Canvas 的getBoundingClientRect()返回的是元素在视口中的位置而clientX/clientY是鼠标相对视口的位置两者相减才能得到相对画布左上角的坐标。这个换算 AI 会写但它经常漏掉一个关键点页面发生滚动时 rect 会变必须每次事件触发时实时获取不能缓存。我第一次测试就是缓存了 rect结果页面一滚动 tooltip 就错位。canvas.addEventListener(mousemove, (e) { const rect canvas.getBoundingClientRect(); const x e.clientX - rect.left; const y e.clientY - rect.top; hitTest(x, y); });命中检测本身也分场景。散点图好办算圆心距离就行柱状图好办判断是否在矩形内折线图最麻烦因为用户往往点的不是数据点本身而是折线上任意一段。所以我用了一个点到线段的距离函数阈值设为 8 像素这样鼠标靠近折线附近就能命中。function pointSegDist(px, py, x1, y1, x2, y2) { const dx x2 - x1; const dy y2 - y1; const l2 dx * dx dy * dy; if (l2 0) return Math.hypot(px - x1, py - y1); let t ((px - x1) * dx (py - y1) * dy) / l2; t Math.max(0, Math.min(1, t)); const cx x1 t * dx; const cy y1 t * dy; return Math.hypot(px - cx, py - cy); }这段代码算法不难但它的意义在后面排查时体现出来了。没有它折线图就只能点中端点体验极差。AI 一开始给的是点到直线距离的版本没有 clamp 到线段范围内导致点在折线延长线上也会命中那是一种非常诡异的手感。3. 实操过程AI 写出的那 90% 是怎么来的3.1 我给 AI 的需求文档式提示词说句实话AI 写代码的上限很大程度上取决于你怎么描述需求。我不是写那种帮我画个折线图的模糊指令而是把每个模块拆成一个独立的提示词任务附上接口定义和几个关键约束。举个例子我给比例尺模块的提示词大概是这样的结构模块目标、输入输出格式、必须处理的边界情况、参考实现风格。AI 看到这种提示词基本能一次写出可运行的代码。我后来总结出一套适合自己的模式先给骨架接口签名再给血肉具体需求最后给边界清单。边界清单尤其重要AI 不会主动想到的坑你把它列进去它就能处理。这一步省下的时间非常可观。手动实现一个坐标比例尺带边界防御大概要二十分钟AI 十秒出稿我花两分钟检查效率差距是碾压级的。3.2 AI 写单测我来补业务断言很多人写 AI 辅助代码时忽略测试我觉得这是最亏的。图形库这类纯计算密集的模块恰恰是最适合写单元测试的因为输入输出高度可预测。我让 AI 先把LinearScale、pointSegDist、数据清洗函数这些纯函数测试写出来测试用例我来补业务相关的部分。这里有个小技巧AI 生成测试用例时通常会写常规情况比如输入 0 到 100 映射到 0 到 500。这种当然能过但不代表代码没问题。我要求自己在测试里补上这些全相等数据、单点数据、负数数据、包含null的数据、极大值极小值。这些边界用例才是图形库稳定性的基本盘。实测下来AI 写的代码在常规用例上几乎全过但在边界用例上暴露了不少问题这正好印证了前面说的AI 的强项是模式覆盖弱项是对异常世界的想象力。3.3 动画系统AI 的框架是对的细节是空的图表库一般需要入场动画比如柱状图从底部长出来、折线图从左到右画过去。动画框架无非是requestAnimationFrame循环里逐步推进进度值然后每帧重新渲染。这个框架 AI 写得没毛病但它有一个致命遗漏没有提供销毁接口。组件卸载了、图表换数据了动画循环还在跑每帧还在继续重绘。在一两个图表时看不出问题到十几个图表同时挂载卸载时CPU 占用直接拉满页面明显卡顿。这个 bug 不是 AI 写不出来而是它压根不知道你的图表会被反复创建销毁。start(progress) { if (this._rafId ! null) return; this._startT performance.now(); const tick (t) { const p Math.min((t - this._startT) / this._duration, 1); progress(this._easing(p)); if (p 1) { this._rafId null; return; } this._rafId requestAnimationFrame(tick); }; this._rafId requestAnimationFrame(tick); } destroy() { if (this._rafId ! null) { cancelAnimationFrame(this._rafId); this._rafId null; } }我在所有图表实例上都挂了一个destroy()方法统一处理取消动画、移除事件监听、断开 ResizeObserver、释放引用。这不只是动画的问题是整个组件生命周期的兜底。AI 帮你写了出生你要负责写死亡。3.4 性能优化能省则省的渲染策略图形库最容易被人诟病的就是性能。数据量一上来每帧把全部图元重绘一遍Canvas 再快也扛不住。我的方案是分级处理静态图表的背景网格和坐标轴只在初始化时画一次数据层有变化时只重绘数据区域动画进行中才全量重绘动画结束就切回静态模式。AI 对这个优化方案的理解很快但实现起来有个共享状态的问题它把当前是否处于动画中这个标记放在模块全局而多个图表实例共享同一份状态导致一个图表在动画时另一个静态图表的背景也被强制重绘了。这属于典型的AI 写代码不考虑实例隔离我花了一晚上把所有全局状态收归到每个实例内部才算彻底解决。4. 让我差点崩溃的那 10%4.1 高分屏模糊最经典的 AI 盲区第一天上浏览器实测我就发现图表文字边缘发虚线条粗细不均截图放大后像蒙了一层纱。原因很明确没有处理 devicePixelRatio。现代笔记本大多是 2 倍屏物理像素是 CSS 像素的两倍Canvas 默认按 CSS 像素绘图等于用一个低分辨率画布拉伸到高清屏上不糊才怪。解决办法是让 Canvas 的位图尺寸等于 CSS 尺寸乘以 DPR然后通过setTransform把绘图坐标缩放到 CSS 像素坐标系。function fitCanvas(canvas, cssWidth, cssHeight) { const dpr window.devicePixelRatio || 1; canvas.width Math.round(cssWidth * dpr); canvas.height Math.round(cssHeight * dpr); canvas.style.width cssWidth px; canvas.style.height cssHeight px; const ctx canvas.getContext(2d); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); return ctx; }这个代码看起来简单但放到整个库里牵扯面很大所有坐标计算基于 CSS 像素所有绘制通过setTransform放大事件命中坐标用的是getBoundingClientRect返回的 CSS 像素所以完全不受影响。我让 AI 排查这个 bug 时它给的第一个建议是提高 canvas 的分辨率完全不提 DPR 这个词。最后还是我自己定位并改完的。经验涉及物理像素和逻辑像素的问题AI 目前很难主动识别这是第一类必踩的坑。4.2 数据边界的 NaN 陷阱图形库上线后收到的第一个线上 bug是某个报表的销售额字段连续一个月都是同一个值。按理说数据正常但图表渲染出来一片空白控制台全是NaN。根因就是我前面说的比例尺在d1 - d0 0时返回中点的逻辑没生效因为 AI 在处理数据经过某种转换后再喂给比例尺的路径时中间层把数值变成了NaN。这个 bug 排查起来非常恶心。表面现象是比例尺这边输出了NaN但实际源头在数据清洗的排序逻辑里——AI 的排序函数把包含null的数组默认按undefined排到最后然后求最大值最小值时没过滤掉undefined于是一切归零。我后来定了一个铁律所有入口数据进计算层之前必须经过一道统一的sanitize函数把null、undefined、NaN、Infinity全部处理掉。这个函数我人肉写死不让 AI 自己发挥。事实证明这是整个项目最值的投资。4.3 命中检测的精度与性能平衡折线图 tooltip 命中调试是另一个差点让我放弃的瞬间。一开始点的反馈断断续续鼠标明明在折线上方tooltip 不出现鼠标离折线老远tooltip 却弹出来了。后来发现两个问题叠加一是点线段距离函数的阈值设置不合理8 像素在 2 倍屏下实际是 4 物理像素手感偏紧二是命中检测遍历了所有数据点数据量到几千时每次鼠标移动都要算几千次距离肉眼可见卡顿。解决方案有两步。阈值改成按 DPR 归一化保证不同屏幕下手感一致性能方面加了一个简单的空间索引先把数据点按 x 坐标排序鼠标移动时先用二分查找圈定 x 附近的候选点再对候选点做精确距离计算。这样几千个点的命中检测只要算几十次手感直接起飞。这段优化 AI 帮了一部分但空间索引的思路完全是我提出的。AI 能写二分查找但它不会主动想到鼠标命中检测需要性能优化。这是第二类必踩的坑AI 默认数据量是小规模你要在提示词里显式告诉它大数据场景的预期。4.4 动画与事件监听的资源泄漏前面提到了动画循环泄漏实际线上环境更复杂。我们的看板有标签页切换每次切走再切回来图表组件都会重新创建。如果没有正确的销毁机制每切换一次就多一个永远跑不完的requestAnimationFrame循环十几个标签来回切几轮页面直接变成幻灯片。类似的泄漏还有事件监听。我把mousemove监听直接绑在 canvas 上销毁时只移除图表实例的引用忘了removeEventListener导致旧 canvas 虽然从 DOM 里摘了但监听函数还挂在上面鼠标一动就报错。这种问题 AI 完全不会预警它写代码时不会想这个组件以后会被销毁吗。我的改进方案是把事件绑定和销毁统一封装成一个bindEvents/unbindEvents对所有图表实例强制走这个通道不允许手动散装监听。4.5 文字布局和颜色插值的暗坑最后补两个小坑虽然小但特别影响观感。第一个是文字布局。坐标轴刻度标签稍长一点就会跟轴线重叠。AI 生成的绘制逻辑用的是fillText加固定偏移没有考虑文字宽度。我改成用measureText量出实际宽度再根据刻度位置动态计算对齐方向这才避免了刻度数字互相打架的丑态。第二个是颜色插值。Pie 图 hover 高亮时需要一个过渡色我尝试让 AI 写一个十六进制颜色插值函数。它写出来的版本没错但只处理了 6 位 hex遇到 3 位 hex 或带透明度的rgba就直接返回NaN色值。后来我统一把所有颜色先转成 RGB 分量再做线性插值输出rgb()字符串兼容性直接拉满。function parseHex(hex) { let h hex.replace(#, ); if (h.length 3) { h h.split().map((c) c c).join(); } const n parseInt(h, 16); return { r: (n 16) 255, g: (n 8) 255, b: n 255 }; } function lerpColor(c1, c2, t) { const a parseHex(c1); const b parseHex(c2); const r Math.round(a.r (b.r - a.r) * t); const g Math.round(a.g (b.g - a.g) * t); const bl Math.round(a.b (b.b - a.b) * t); return rgb(${r},${g},${bl}); }5. 常见问题与排查技巧实录5.1 问题速查表把这段时间踩过的坑整理成一张表遇到类似症状可以直接对照。症状根因解决方案图表文字发虚、线条粗细不均未处理高分屏 DPRfitCanvas统一缩放渲染结果全空白且控制台报 NaN数据含空值且清洗不彻底入口处强制sanitize鼠标点击无法命中图元事件坐标未换算或 rect 缓存失效每次事件实时getBoundingClientRect折线点中延长线也能触发 tooltip点到直线而非线段距离函数做 clamp页面切换后 CPU 飙高、卡顿动画循环和事件监听未释放统一destroy()接口图表在容器缩放后变形未监听容器尺寸变化ResizeObserver重设画布柱状图边缘溢出绘图区未做裁剪save/clip/restoretooltip 跟随有半格偏移未考虑 DPR 或 CSS 像素换算统一以 CSS 像素为坐标基准饼图 hover 颜色异常十六进制颜色解析未兼容 3 位统一转 RGB 分量插值文字跟坐标轴重叠未测量文字实际宽度measureText动态计算偏移这张表不是理论推导全是实测排障的真实记录。每个问题背后至少花掉我半天到一天的时间这也是我说剩下 10% 差点把我坑死的直接原因。5.2 让 AI 补救代码的独门技巧踩坑踩多了我总结出一套让 AI 配合补后 10%的方法分享给有类似场景的朋友。第一把边界条件做成 checklist 贴进提示词。每次让 AI 改某个函数我都会在末尾追加一行请特别注意数据全相等、空数组、单点数据、负值、非数字值这五种边界情况。这行字看着简单但能显著降低 AI 回归出新 bug 的概率。第二先给测试再让 AI 改实现。遇到 bug 时我先把失败的测试用例和断言贴给 AI让它根据测试反推修复方案。这比直接问你帮我看看哪错了有效得多因为 AI 在对着明确目标时表现更好。第三把 AI 当结对编程的 junior而不是自动补全器。让它先讲思路再写代码或者在提示词里加一句先解释你打算怎么修再输出代码。这样我能提前发现它思路不对不用等代码写完再返工。第四人工 review 一份雷区清单。这个清单包含有没有处理 DPR、有没有释放动画帧、有没有移除事件监听、有没有做数据边界防御。每次 AI 交稿我逐条对照检查漏一条就打回重改。这个过程虽然繁琐但确实保证了最后的质量。6. 踩过坑之后的几点体会说回开头那个问题——AI 能写图形库吗我的答案变了能但它写的是从零到差不多能用的那 90%剩下那 10% 需要的是对渲染引擎的理解、对真实数据丑恶面的认知、对组件生命周期的敬畏这些恰恰是 AI 目前最薄弱的地方。你让 AI 独立完成整个图形库就像让一个背熟菜谱的人独立开饭店菜谱他能背但食材变质、燃气不稳、客人过敏这些事菜谱上不会写。我个人在实际操作中的体会是AI 辅助写图形库的正确姿势不是甩手掌柜而是带着框架的管理者。框架、边界、生命周期、性能预期这些设计层面的东西必须由人来定AI 负责在框架内高效填充实现。你给它的约束越清楚它产出的质量越接近可用反过来你越指望它自己发现问题它越会在你最信任它的地方给你埋雷。最后再分享一个小技巧算是我这段时间最值钱的一条经验给 AI 生成的每个纯函数都配上边界测试测试用例里故意写一些正常人不会给的输入比如只有一个点的折线图、全是相同数值的柱状图、包含Infinity的散点数据。这些测试不是为了好看而是为了让 AI 的盲区暴露在可控的测试环境里而不是暴露在生产环境的线上报错里。毕竟图形库这种项目出一次线上事故的代价抵得上你写一百个测试用例的时间。
返回列表