前端阅读 05月28日 05:32
如何在 JavaScript 中操作 SVG?核心方法与常见坑
用 JavaScript 操控 SVG,本质就是操作 DOM——只不过多了个命名空间的坑。SVG 元素挂在 DOM 树上,所以 querySelector、addEventListener 这些老朋友都能用,但创建元素时必须用 createElementNS,这是新手最容易栽的地方。本文覆盖 SVG 元素选择、属性修改、事件绑定、动画实现、坐标换算、拖拽交互这些核心操作,顺带聊几个实际开发中踩过的坑。命名空间:第一个坑HTML 元素用 document.createElement('div') 就行,SVG 不行——你必须指定命名空间:const SVG_NS = 'http://www.w3.org/2000/svg';const circle = document.createElementNS(SVG_NS, 'circle');忘掉 NS 后缀会怎样?浏览器不会报错,但创建出来的元素不属于 SVG 命名空间,渲染不出来,调试半天才发现是这个原因。这类 bug 特征是:元素确实被插入了 DOM,但页面上什么都看不到。另一个容易忽略的是 xlink:href 属性。SVG 的 <use>、<image> 等元素引用外部资源时用的是 xlink:href,它有自己的命名空间:const XLINK_NS = 'http://www.w3.org/1999/xlink';useEl.setAttributeNS(XLINK_NS, 'xlink:href', '#icon');新规范中 href 已经可以直接用 setAttribute 设置,但兼容旧浏览器时还是得走 xlink。选择元素:和 HTML 一样SVG 元素的选择没有特殊之处,标准 DOM API 直接用:const circle = document.getElementById('myCircle');const allCircles = document.querySelectorAll('svg circle');const filledElements = document.querySelectorAll('[fill="red"]');需要注意的是,如果你用 <img> 标签引入 SVG,JavaScript 是无法访问内部元素的。必须用内联 SVG(直接写在 HTML 中)或 <object> / <iframe> 加载,才能用 JS 操作。修改属性和样式SVG 元素的属性分为两类:呈现属性(如 fill、stroke、r)和 样式属性。两者都可以改,但走不同的路:// 方式一:setAttribute 修改呈现属性circle.setAttribute('fill', 'red');circle.setAttribute('r', '60');// 方式二:style 对象修改样式circle.style.fill = 'green';circle.style.opacity = '0.5';一个常见的困惑是:setAttribute('fill', 'red') 和 style.fill = 'red' 有什么区别?CSS 样式的优先级高于呈现属性,所以 style.fill 会覆盖 setAttribute('fill', ...)。这和 CSS 层叠规则一致。带连字符的属性(如 stroke-width、font-size)不能用点语法赋值,circle.stroke-width = 4 会报错。必须用 setAttribute 或驼峰写法 style.strokeWidth。事件绑定SVG 元素天然支持 DOM 事件,点击、悬停、拖拽都能绑定。唯一要注意的是键盘事件——SVG 元素默认不可聚焦,需要手动加 tabindex:circle.setAttribute('tabindex', '0');circle.addEventListener('keydown', (e) => { if (e.key === 'Enter') { circle.setAttribute('fill', 'red'); }});对于大量同类元素(比如数据可视化中的几十个柱子),逐个绑定事件很浪费内存,用事件委托更合理:svg.addEventListener('click', (e) => { const bar = e.target.closest('rect.bar'); if (bar) { highlightBar(bar); }});动画:CSS 过渡 vs requestAnimationFrame简单动画用 CSS 过渡就够了,改个属性值浏览器自动补间:circle.style.transition = 'all 0.3s ease';circle.setAttribute('r', '80');需要精确控制的动画(比如沿路径运动、物理模拟)则要用 requestAnimationFrame。一个容易犯的错是用 setInterval——它不跟浏览器刷新率同步,动画会卡顿。requestAnimationFrame 的回调在浏览器下一次重绘前执行,能保证流畅:let angle = 0;function animate() { angle += 0.02; const x = 150 + Math.cos(angle) * 100; const y = 150 + Math.sin(angle) * 100; circle.setAttribute('cx', x); circle.setAttribute('cy', y); requestAnimationFrame(animate);}requestAnimationFrame(animate);性能优化有一条核心原则:尽量动画 transform 和 opacity,别动布局属性。transform: translate() 走 GPU 合成,不触发重排;改 cx、cy 则会触发重排。当元素数量多时差距明显。获取鼠标在 SVG 中的坐标鼠标的 clientX/clientY 是页面坐标,要换算成 SVG 内部坐标需要做矩阵变换:function getSVGPoint(svg, event) { const pt = svg.createSVGPoint(); pt.x = event.clientX; pt.y = event.clientY; return pt.matrixTransform(svg.getScreenCTM().inverse());}svg.addEventListener('click', (e) => { const { x, y } = getSVGPoint(svg, e); console.log(`SVG 坐标: ${x}, ${y}`);});getScreenCTM() 返回 SVG 坐标系到屏幕坐标系的变换矩阵,.inverse() 取逆矩阵,就能从屏幕坐标映射回 SVG 坐标。如果 SVG 做过 viewBox 缩放或 transform,这一步是必须的。拖拽实现拖拽是把鼠标坐标换算和事件监听组合起来的典型场景:let dragging = null;let offset = { x: 0, y: 0 };function getMousePos(svg, e) { const CTM = svg.getScreenCTM(); return { x: (e.clientX - CTM.e) / CTM.a, y: (e.clientY - CTM.f) / CTM.d };}svg.addEventListener('mousedown', (e) => { dragging = e.target; const pos = getMousePos(svg, e); offset.x = pos.x - parseFloat(dragging.getAttribute('cx')); offset.y = pos.y - parseFloat(dragging.getAttribute('cy'));});svg.addEventListener('mousemove', (e) => { if (!dragging) return; const pos = getMousePos(svg, e); dragging.setAttribute('cx', pos.x - offset.x); dragging.setAttribute('cy', pos.y - offset.y);});svg.addEventListener('mouseup', () => { dragging = null; });触摸设备上要把 mousedown/mousemove/mouseup 换成 touchstart/touchmove/touchend,或者用 Pointer Events 统一处理。什么时候该用 SVG,什么时候该用 Canvas?这不是本文主题,但做 SVG 开发迟早会遇到这个问题,简单说下判断依据:用 SVG:需要交互(每个元素可点击/悬停)、需要无障碍访问、图形数量在几千以内、需要 CSS 动画用 Canvas:大量元素(超过 3000 个)、像素级操作、实时游戏渲染、不需要单个元素的交互实际项目中,图表用 SVG(D3.js / ECharts 的 SVG 模式),游戏用 Canvas,这是比较成熟的选型。几个实际开发中的坑innerHTML 可以用但别滥用。svg.innerHTML = '<circle cx="50" cy="50" r="40"/>' 在现代浏览器中能工作,但它不经过命名空间检查,序列化时可能出问题。动态创建元素还是老老实实用 createElementNS。getBBox() 获取元素边界。想知道一个 SVG 元素实际占了多大空间,用 getBBox() 返回 { x, y, width, height },这个值不受 transform 影响,是元素自身的原始尺寸。SMIL 动画(<animate> 标签)正在被边缘化。Chrome 曾一度要移除 SMIL 支持,虽然后来撤回了,但趋势是尽量用 CSS 动画或 JavaScript 替代 SMIL。SVGO 压缩 SVG。从设计工具导出的 SVG 通常包含大量冗余属性(编辑器元数据、无用空白等),用 SVGO 压缩可以减小 30%-70% 的体积,线上必须走一遍。