十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

纯SVG实现圆环进度条:从原理到组件封装

纯SVG实现圆环进度条:从原理到组件封装 我在做后台管理系统的时候经常需要画那种圆环进度条——用户上传文件、数据导入、权限评分、套餐使用量总之哪个页面都少不了它。早期我习惯直接用图片库里的素材或者让设计切一张PNG后来发现这种方式扩展性太差换个颜色要重做图改个粗细要重做图连百分比数字都不能跟着动。后来我把圆环进度条改成纯SVG绘制一个标签加几行样式就搞定了整个过程并没有想象中复杂。SVG全称是可缩放矢量图形本质是用XML描述图形跟PNG那种位图完全不是一回事。用它画圆环进度条核心思路其实很朴素在SVG里画一个圆然后用虚线描边再用偏移量控制虚线的显示区间进度到了百分之几就显示弧长百分之几。听起来像绕口令实际操作起来非常直观而且这套方案不依赖任何第三方库浏览器原生支持移动端桌面端表现一致动画流畅度也很能打。这篇文章我会从原理讲到落地包括完整的示例代码、组件封装思路以及我在实际项目中踩过的坑。无论你是刚接触SVG的初学者还是想优化现有进度条方案的前端老兵应该都能找到用得上的内容。1. 为什么选SVG三条技术路线对比1.1 Canvas、CSS与SVG的取舍圆环进度条的实现路径业内基本有三条CSS的conic-gradient圆锥渐变、Canvas 2D绘制、SVG描边。我三种都试过最终稳定用SVG主要原因不是某一种技术吊打其他而是SVG在“易用性、灵活性、性能”三者之间最均衡。CSS方案用conic-gradient画圆环代码很短一个background属性就能出效果但它有一个致命短板——圆锥渐变本身是一个完整的扇形想画出一个“有缺口的圆环”必须再叠加一个圆形遮罩。这个遮罩层跟底色的耦合度很高一旦页面背景是渐变、花纹或动态颜色遮罩就很容易穿帮。另外在CSS里让圆环某一段高亮、显示不同颜色操作起来复杂度会迅速上升远没有SVG那么灵活。Canvas方案正好相反灵活性极高从弧度、线宽、颜色到渐变都能用JavaScript控制绘制路径的API也很丰富。但Canvas是非矢量位图屏幕分辨率越高越需要处理DPI缩放它的内容也不能像HTML那样直接绑定事件、被爬虫读取、被浏览器无障碍树识别。为了一个进度条引入Canvas体系还要处理重绘时机、销毁清理多少有点杀鸡用牛刀的意味。SVG方案的处理方式非常“声明式”图形就是DOM节点圆的半径、描边宽度、颜色都是属性或CSS结构一目了然JavaScript要改变它只需修改一个属性值。缩放在任何设备上都清晰锐利事件也能直接绑在节点上甚至可以做无障碍标注。对业务开发来说这种“配置式”的控制方式最节省心智负担。1.2 SVG方案带给开发者的红利除了技术特性本身SVG还有一个容易被忽略的优势——工具链成熟。现在社区里大量的图标库、素材站都提供SVG格式源文件很多设计工具也支持一键复制SVG代码我可以直接在别人的SVG基础上改结构不必从零开始算坐标。日常开发中我经常从一些免费SVG素材站点下载图形打开源码看看别人怎么写path和circle这本身就是学习SVG的最佳途径。另外SVG的调试体验也比Canvas舒服得多。浏览器开发者工具能直接高亮一个SVG节点、查看它的包围盒、实时编辑stroke-dashoffset参数甚至能断点调试。这种“所见即所得”的体验对做精细化UI的需求来说极其宝贵。如果你想快速验证某个圆环进度条的粗细、配色是否合适直接在控制台改属性就能看到预览效果整个链路非常舒服。所以我的结论很明确圆环进度条这种“图形简单、更新频繁、需要被CSS和JS同时控制”的UI组件SVG是当前综合成本最低的方案。2. 圆环进度条的核心原理两个属性的配合2.1 stroke-dasharray先理解“画虚线”很多初学者第一次看到圆环进度条的代码会被stroke-dasharray和stroke-dashoffset这两个属性劝退。其实换个角度看就非常容易理解——SVG里的描边本质上跟你在设计软件里给一个圆设置“虚线”效果是一模一样的。stroke-dasharray用来设置虚线的“实线长度”和“间隔长度”。比如stroke-dasharray10 5表示一段10px的实线、一段5px的空白不断循环。如果我想让圆的描边变成一整条连续的实线就让“实线长度”恰好等于圆的周长并把间隔也设成同样数值例如stroke-dasharray502.65 502.65。这样看起来还是一整圈实线但它的底层结构已经变成了一段刚好完整的虚线。圆周长怎么算小学数学内容公式是2πr。比如我们常画的半径为r 80的圆周长就是2 * 3.14159 * 80 ≈ 502.65。这里有一个非常关键的操作点既然圆形的周长是确定的那“显示多大一段弧线”就完全取决于虚线的偏移量。我们只需要把一整圈虚线当作一个“可滑动的窗口”窗口的滑动距离就是stroke-dashoffset。2.2 stroke-dashoffset的计算公式stroke-dashoffset的中文意思叫“虚线偏移量”它表示虚线起点向后移动的距离。需要注意的是这个偏移是“倒着退”的偏移量越大你能看见的虚线实线部分就越少。用公式来表达更清楚。设圆的周长为C进度百分比为p取值范围0到1需要显示的弧长 C * p需要隐藏的弧长 C * (1 - p)stroke-dashoffset C * (1 - p)为什么要用剩下的部分去算偏移而不是直接偏移C * p因为SVG的虚线绘制方向是从初始位置开始先画实线段再画空白段。当偏移量增加时虚线起点被“往后拽”实线段被拽入空白区域从而看起来描边在缩短。所以想让弧线变短偏移量反而是变大的。刚开始用这个属性的人十有八九会搞反方向我建议记住这个公式就够了offset 周长 * (1 - 百分比)。2.3 最小可运行示例手写第一版理解了原理代码其实非常简单。先来个不加任何JavaScript的最小示例——直接把进度写死在SVG属性里svg width200 height200 viewBox0 0 200 200 !-- 灰色底环 -- circle cx100 cy100 r80 fillnone stroke#e9ecef stroke-width12 / !-- 蓝色进度环显示75% -- circle cx100 cy100 r80 fillnone stroke#4e8cff stroke-width12 stroke-linecapround stroke-dasharray502.65 stroke-dashoffset125.66 transformrotate(-90 100 100) / /svg这段代码里进度环显示多少是stroke-dashoffset125.66决定的。因为502.65 - 125.66 376.99显示出来的弧长约376.99占周长的比例是376.99 / 502.65 ≈ 0.75正好是75%。transformrotate(-90 100 100)是把整个圆环逆时针旋转90度。不旋转时SVG圆形的描边起点在“右侧三点钟方向”旋转后起点移到“顶部十二点钟方向”更符合日常仪表盘的视觉习惯。如果将来要做仪表盘那种270度扇形也是从这个旋转思路延伸出来的。3. 完整实操一个可复用的圆环进度条组件3.1 HTML结构与SVG分层直接写在属性里的进度值是写死的实际业务中进度肯定要跟着数据跑。下面我给出一个可复用的圆环进度条实现前端项目都能直接用。先看HTML结构。我习惯把“底层灰色轨道”和“上层彩色进度环”分成两个circle这样当进度为0%时页面依然能看到一个圆环轮廓不至于像环消失了div classprogress-wrap svg classprogress-ring viewBox0 0 200 200 circle classprogress-track cx100 cy100 r80 / circle classprogress-bar cx100 cy100 r80 / /svg div classprogress-value0%/div /div外层用viewBox而不是固定的width和height是为了让SVG能跟随容器尺寸自动缩放。我把cx、cy设为100是因为viewBox是0 0 200 200圆心正好在正中半径80周边留了20px空间足够容纳12px宽的描边而不被裁切。3.2 CSS样式与起点旋转接下来是CSS。这里有个比较容易忽略的点我把旋转写在了SVG元素上而不是写在外层容器。写在SVG元素上用transform: rotate(-90deg)它会连同坐标系一起旋转圆环的圆点不受影响写在外层容器则会导致整个容器旋转定位其他元素时容易混乱。.progress-wrap { position: relative; width: 200px; height: 200px; } .progress-ring { width: 100%; height: 100%; transform: rotate(-90deg); } .progress-track { fill: none; stroke: #e9ecef; stroke-width: 12; } .progress-bar { fill: none; stroke: #4e8cff; stroke-width: 12; stroke-linecap: round; transition: stroke-dashoffset 0.35s ease; } .progress-value { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 28px; font-weight: 600; color: #333; }注意进度环的stroke-dasharray和stroke-dashoffset我并没有写死在CSS里而是打算在JavaScript里动态设置。原因很简单每个圆环的半径不同周长就不同写死会让组件失去通用性。用JavaScript统一计算是更稳妥的做法。3.3 用JavaScript控制进度值现在写JavaScript部分。我封装了一个小的控制器调用setProgress(percent)就能实时更新进度和百分比数字const radius 80; const circumference 2 * Math.PI * radius; // 约 502.65 const bar document.querySelector(.progress-bar); const valueEl document.querySelector(.progress-value); function setProgress(percent) { const offset circumference * (1 - percent / 100); bar.style.strokeDasharray ${circumference}; bar.style.strokeDashoffset offset; valueEl.textContent ${Math.round(percent)}%; } // 示例设置进度为 75% setProgress(75);这里有几个细节值得展开。第一bar.style.strokeDasharray ${circumference}这一行其实可以放在初始化时执行一次。但放在setProgress里是为了保证这段代码迁移到其他项目时即使样式表被覆盖也不会出错。第二浮点精度问题。圆周率取到Math.PI已经很高精度了但计算出来的数值仍然是无限小数赋给SVG属性完全没有问题浏览器会自行处理。你不需要像网上很多教程那样手动写成502.65直接用2 * Math.PI * radius计算最靠谱。第三transition: stroke-dashoffset 0.35s ease这个CSS属性是实现平滑动画的关键。stroke-dashoffset从旧值变化到新值时浏览器会自动补间动画肉眼看起来就是进度条在“走”。动画时长我一般取300到500毫秒太长显得拖沓太短又感受不到过渡。3.4 场景化改造环形仪表盘与下载进度页基础的圆环进度条能跑通后稍微改造就能适配很多业务场景。比如做“环形仪表盘”展示服务器CPU、内存占用率这类数据。你可以把颜色改成随数值区间变化小于60%显示绿色60%到85%显示橙色超过85%显示红色。实现方式是给stroke赋不同的十六进制值其余逻辑完全不变。我在运维平台里就是这么用的视觉上比文字数字直观得多。再比如下载进度页通常需要显示“已下载 / 总大小”以及百分比。这类场景中数字增长和圆环增长要保持同步。我一般不会在JS里同时修改textContent和dashoffset而是让文本节点和SVG共用同一个百分数变量只改一处数据源从根上避免两边数值对不上的问题。还有一个常见的需求进度到100%时环变绿或者出现一个对勾提示。你可以在setProgress里加一个判断percent 100时切换bar的stroke为成功色同时显示一个对勾图标。这套组合拳做下来一个功能完整的进度组件基本就成型了。4. 进阶玩法渐变、多环与交互动画4.1 渐变描边让进度条从呆板变精致纯色进度条看久了确实有点单调加入渐变能明显提升质感。SVG原生支持linearGradient和radialGradient两种渐变用在圆环描边上非常方便。我在这部分用一个简单的示例演示如何给圆环加线性渐变svg classprogress-ring viewBox0 0 200 200 defs linearGradient idgradient x10% y10% x2100% y2100% stop offset0% stop-color#4e8cff / stop offset100% stop-color#9b5cff / /linearGradient /defs circle classprogress-track cx100 cy100 r80 / circle classprogress-bar cx100 cy100 r80 strokeurl(#gradient) / /svg在这个例子里defs标签定义了渐变色板linearGradient的x1 y1 x2 y2表示渐变方向。这里取对角线方向颜色从左上角的蓝色渐变到右下角的紫色。进度环的stroke从固定的颜色值改成了url(#gradient)也就是引用上面定义好的渐变。实际操作中你可以在一个SVG里定义多个渐变用id区分不同的圆环分别引用即可。需要注意id在同一个页面里必须是唯一的如果组件被多次复用要确保每一个实例的渐变id不同否则浏览器会串色。4.2 多环叠加一套SVG展示多维度数据有时一个圆环承载不了那么多信息。比如后台统计“本月预算使用情况”可能要展示“已使用、预留下月、可用余额”三个维度。这种情况下我倾向于在同一块SVG里叠加多个圆环每个圆环用不同的半径和颜色。我将半径分别设为80、60、40这样三层圆环从外到内排列。每个圆环的周长不同所以JavaScript里要为每个环单独保存一个radius值再分别计算各自的circumference。代码结构上可以把每个环的数据整理成一个数组const rings [ { element: .ring-budget, radius: 80, percent: 70, color: #4e8cff }, { element: .ring-reserve, radius: 60, percent: 40, color: #9b5cff }, { element: .ring-available, radius: 40, percent: 20, color: #ff9f43 } ]; rings.forEach((ring) { const bar document.querySelector(ring.element); const circumference 2 * Math.PI * ring.radius; const offset circumference * (1 - ring.percent / 100); bar.style.strokeDasharray ${circumference}; bar.style.strokeDashoffset offset; bar.style.stroke ring.color; });多环设计要注意一个问题内环比外环更容易显得拥挤最内层的半径如果太小中心区域留给文字的空间就很少。所以一般保证内环半径不低于30否则中间区域放百分比文字会非常局促观感也很差。4.3 让圆环在进入视口时才播放动画页面很长的时候如果一进来就把所有圆环动画播放完用户的视线根本捕捉不到过程动画等于白做。正确的做法一般是等用户滚动到圆环所在区域时再触发进度动画。这个需求的实现方式不难核心是IntersectionObserverconst observer new IntersectionObserver((entries) { entries.forEach((entry) { if (entry.isIntersecting) { // 进入视口时把进度从0过渡到目标值 setProgress(75); observer.disconnect(); // 只触发一次 } }); }, { threshold: 0.3 }); observer.observe(document.querySelector(.progress-wrap));threshold: 0.3表示圆环出现30%的面积时才触发这个值可以根据页面布局微调。因为CSS里设置了transitionsetProgress改变stroke-dashoffset后动画会自动播放。如果刷新页面后你想让圆环先归零再跑到目标值那就在初始化时先调用一次setProgress(0)再在观察回调里调用setProgress(75)效果会更自然。除了滚动触发还有一种常见的交互动画是点击切换数值。实际测试下来SVG的快速连续换值非常稳定transition会做插值过渡不会出现闪烁或断层这一点比Canvas频繁重绘要省心得多。5. 常见问题与排查技巧实录5.1 描边被裁切或者位置偏移之前团队里有个同事做的圆环进度条在浏览器里总是被切掉一块弧线他百思不得其解。我检查后发现他的半径设置为96stroke-width为12viewBox却还是0 0 200 200。描边的中心在圆周上向内、向外各占一半宽度所以外边缘最大到96 6 102已经超出了viewBox的100边界边缘当然会被裁掉。解决方案有两个一是把半径缩小保证r stroke-width / 2不超过100二是扩大viewBox比如改成0 0 220 220圆心改成110。两者都可以但推荐优先调整半径因为viewBox改动后整个坐标系同步变化比较麻烦。经验法则r strokeWidth/2一定要小于viewBox边长的一半留出一定余量更保险。5.2 数字与圆环进度对不上百分比数字显示的是50%圆环看起来却只有45%左右这种问题多半出现在带圆头stroke-linecap: round的圆环上。圆头的存在会让弧线两端各延伸到半径方向之外占掉一小段长度。当进度较小时这个“多出来”的圆头占总体比例就很明显视觉上会比实际进度长不少。处理方法有两种。如果对精确度要求不高接受这一点点视觉误差团队里大多数项目其实都这样处理的。如果要求严格可以计算圆头凸出的弧长——对于半径为r、线宽为strokeWidth的圆环每个圆头大致多占strokeWidth / 2的弧长实际显示的弧度可以在计算offset时稍微加一点补偿。不过说实话业务中很少遇到这个级别的精度需求知道原因就行了。5.3 容器尺寸变化后圆环变形圆环变成椭圆这个问题的根源通常是外层容器不是正方形。SVG的viewBox虽然能等比缩放但那是按“保持比例”的原则缩放的如果容器的宽高比例不是1:1浏览器为了填满容器会强行拉伸SVG圆就变扁了。解决方法非常简单给外层容器加上aspect-ratio: 1 / 1;强制保持正方形比例。如果你需要圆环自适应屏幕宽度可以给容器设置width: 100%; aspect-ratio: 1 / 1;然后height不设或者设autoSVG会根据viewBox自行适配。5.4 兼容性与浏览器差异排查SVG的核心属性stroke-dasharray和stroke-dashoffset有着非常扎实的浏览器兼容性支持范围极其广泛完全不输CSS动画。不过在实际开发中我确实遇到过一类奇怪的问题transition写在CSS里在某个低版本浏览器上失效进度条“啪”地一下就跳到终点了。排查后的结论是那个旧浏览器对SVG外观属性的CSS过渡支持不完整。解决方案也很粗暴用JavaScript的requestAnimationFrame手动做补间动画或者升级浏览器。写到这里顺带提一句如果你把SVG用作img标签的src或者background-image那么内嵌的JavaScript、事件绑定、CSS外部引用都无效这类纯展示场景不需要担心过渡动画但如果有交互需求务必把SVG作为HTML节点直接嵌入页面。6. 心得体会这套方案的真实开发收益我真的把很多类型的进度展示都切换到SVG方案之后最大的感受是“可控性好到不可思议”。以前做UI调整我要找设计师要一张新图再切图、换文件、改压缩名现在调整圆环粗细、颜色渐变、端帽形状就是在样式表里改几个数字设计师随口一句“要不再细一点”我两秒钟就能改完整个沟通链条缩短了不少。还有一件事让我印象深刻。有次一个页面需要同时展示8个不同数据的圆环每个圆环的半径、颜色、进度都不同。如果让我用图片那得准备8张素材图还得专门维护参数对应关系。可换成SVG方案后我只是维护了一个数组8个圆环的差异全部集中在数据里逻辑和模板完全复用。从那时起我对“能用代码生成UI就别用静态素材”这句话有了更深的体会。最后再分享一个小技巧在调试圆环进度条时我经常在浏览器控制台手动改stroke-dashoffset的值观察不同数值下圆环的缺口状态。这个习惯帮我快速理解了偏移量方向对显示效果的影响也让我在写公式时不会方向搞反。如果你刚开始接触这个方案我也建议你打开开发者工具亲手拖拽一下那些数值比看十篇文章都管用。
返回列表