简介:面向前端开发、界面设计与技术文档编写者的 SVG 图标素材合集,聚焦 Azure 与 Microsoft 相关产品标识,同时收录多种品牌徽标与抽象通用符号,可用于云服务架构图、产品界面、演示文稿与文档配图的快速取用。压缩包约 2.57MB,上游暂未提供文件总数与类型明细,内容以 SVG 矢量文件为主,全部经过清理与优化,并启用了 removeDimensions 插件,便于直接嵌入页面或导入设计工具后自由缩放而不失真。三套主要 Azure 图标集之间存在部分重叠,各自又保留独有图标,可按需选取,省去自行合并去重的麻烦。资源整理自公开互联网与微软相关站点,作者已说明不主张所有权与版权,仅供合理使用范围内的参考。已有 220 人学习下载,适合希望统一视觉风格、减少重复绘制成本的开发者与设计师。
1. 一套 Azure 图标集解决的到底是什么问题:从截图拼图到可缩放矢量
做云架构图和运维后台的人多半踩过同一个坑:拓扑图里的虚拟机、存储、网关图标是从不同地方截图拼起来的,放大导出 PDF 边缘全糊,产品线换个主题色还得整套重画。要根治,靠的不是多找几张高清图,而是一套命名规整、矢量可缩放、能按产品线检索的 SVG 图标集(iconset)。这份 Azure 与 Microsoft 系图标集合把散落的产品符号收成统一的 18×18 画布矢量文件:编号可排序、文件名可检索、内联进页面还能跟着主题变色。适合三类人——画架构图与方案文档的技术写作者、做云管平台和监控大盘的前端、需要把图标塞进 PDF 报告和 PPT 的售前运维。后面按拆包看结构、接进页面、主题接管、踩坑排查、上线前体检的顺序走,每一步都能直接抄。
2. 拆包看结构:命名编码、viewBox 与 defs 里的三个细节
拿到压缩包后最先要弄清的不是“哪个图标好看”,而是这批 SVG 到底以什么形态存在。很多人解压后双击打开某个文件,看到一片空白或者黑乎乎一坨,就判断文件损坏了——不是坏了,而是这类图标把颜色、尺寸、上下文都交给了宿主页面,浏览器单独打开时没有任何外部环境给它补位。所以第一步永远是先跑两条命令把文件“看不懂”的部分量化出来,再决定怎么入库。
2.1 目录分层与文件名编码:把编号当检索键用
Azure 架构图标的发布形态通常是平铺目录,文件名形如00001-icon-service-App-Services.svg,前缀是五位编号,中间是类别标识,后面是产品名的 kebab-case 写法。这套编码并非装饰:它是稳定的排序键,也是稳定的检索键。产品改名时编号不动,按字母序排就会乱成一锅粥;按编号排,历史列表永远是连续的。
入库前我会先建一张映射表,把“文件名能提供的信息”和“页面真正需要的信息”分开:
| 字段 | 含义 | 示例 | 前台用途 |
|---|---|---|---|
| prefix | 五位分类编号 | 00001 | 图标选择器排序、分组 |
| kind | 图标类别 | icon-service/icon-general | 区分产品图标与通用符号 |
| slug | 产品名 kebab-case | virtual-machines | 组件 id、symbol id、检索关键词 |
| ext | 扩展名 | .svg | 构建期白名单过滤 |
真正落到代码里,前台只认slug,编号只活在选择器里。这样一来,产品名改一次只需要改索引文件,不用动页面里几十处<use>引用。很多人图省事直接拿文件名当组件名,等到Virtual Machines被改成Virtual Machine那天,整个仓库的 grep 就开始了。
批量规范化文件名有个容易翻车的点:产品名里带空格、括号、大写混排,直接mv会把带空格的路径拆成两个参数。稳妥写法是用find -print0配合read -d '':
# 批量规范化:产品名转 kebab-case,统一小写,去掉首尾连字符 mkdir -p ./icons find ./raw-icons -name '*.svg' -print0 | while IFS= read -r -d '' f; do base=$(basename "$f" .svg) # tr 转小写,sed 把非字母数字压成单个连字符,再剪掉首尾多余的 '-' slug=$(printf '%s' "$base" | tr '[:upper:]' '[:lower:]' \ | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//') cp "$f" "./icons/${slug}.svg" done-print0与read -r -d ''是一对,用 NUL 而不是换行做分隔符,路径里有空格也不会被切开。-r关掉反斜杠转义,IFS=保持为空,避免read把行首行尾的空白吃掉。sed里两段s/^-+//和s/-+$//必须分开写,写成s/^-+|-+$//g在 BSD sed 上行为不一致。这段跑完再diff一下文件数量,确认没有因为重名被覆盖——cp是静默覆盖的,不会报错。
2.2 viewBox 与画布尺寸:为什么不能直接看 width/height
这批图标绝大多数用viewBox="0 0 18 18",也就是 18 单位见方的画布。顶层有时带width="18" height="18",有时干脆没有——没有反而是好事,说明尺寸完全交给 CSS。判断该按什么倍数渲染,看的是 viewBox,不是文件头里的 width。
18 这个数字很关键。它不是一个“看起来随便定的”值,而是一个能被 2、3、6、9 整除的栅格。渲染到 18px、36px、54px 时,图形边缘恰好压在整数像素上;一旦你把它塞进 16px 的容器,缩放比变成 0.888,原来落在整数坐标上的描边就落到 0.44 像素这种位置,浏览器做抗锯齿,效果就是边缘发灰、细线糊成一片。这是最常见的“图标不清晰”来源,跟文件质量毫无关系。
入库前先跑一条统计命令,把画布不一致的文件全揪出来:
# 统计每个文件的 viewBox 分布,找出非 0 0 18 18 的异类 grep -oh 'viewBox="[^"]*"' icons/*.svg | sort | uniq -c | sort -rn | head -20 # 再看有多少文件在顶层硬编码了 width/height(会妨碍 CSS 控制) grep -l '<svg[^>]*\bwidth=' icons/*.svg | wc -lgrep -o只输出匹配段,-h去掉文件名前缀,这样uniq -c才能正确计数。如果结果里出现0 0 48 48或0 0 24 24这类画布,说明图标来自不同批次;混用会让同一行图标视觉重量不一致,通常做法是统一重设 viewBox 或者至少在前端按画布分档设置尺寸,别硬塞进同一个width。
我一般会把尺寸档位固定成画布整数倍,写进设计变量里,而不是让每个页面自己写 px:
:root { /* 18 是画布基准,尺寸只取整数倍,避免半像素抗锯齿 */ --icon-size-xs: 18px; --icon-size-sm: 24px; /* 非整数倍,但视觉重量可控 */ --icon-size-md: 36px; --icon-size-lg: 54px; }如果业务确实必须用 16/20 这类尺寸(工具栏按钮常见),那就接受轻微模糊,或者改用描边更粗的那一档图标。这是取舍,不是 bug。
2.3 defs、渐变与 id:每个文件都是一个独立命名空间
第三个细节藏在<defs>里。产品图标里不少带渐变、裁剪、遮罩,结构大致是这样:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"> <defs> <linearGradient id="g1" x1="0" y1="0" x2="1" y2="1"> <stop offset="0" stop-color="#50a0e0"/> <stop offset="1" stop-color="#0b5a94"/> </linearGradient> <clipPath id="c1"><rect x="2" y="2" width="14" height="14" rx="2"/></clipPath> </defs> <path d="M2 3h14v12H2z" fill="url(#g1)" clip-path="url(#c1)"/> </svg>单独打开这个文件时,g1和c1的作用域是整份文档,天然不冲突。可一旦你把两个这样的 SVG 内联进同一个 HTML,DOM 里就有了两个id="g1"。浏览器遇到重复 id 时只取第一个,结果第二个图标也用上了第一个的渐变——视觉上就是“串色”。这个问题的隐蔽之处在于它时好时坏:页面里图标顺序一换,串色的对象就变了,排查时容易怀疑到缓存和构建工具头上。
所以处理策略要前置:如果走内联路线,必须在生成阶段做 id 作用域化(第 3 章给脚本);如果走<img>或 sprite 外部引用路线,天然隔离,不用管。选型在拆包阶段就该定下来,别等做完一半再改。
另外留意 svgo 的cleanupIds插件:它默认会把g1压成a、b、c这种极短 id,单文件体积是下来了,但每个文件都叫a,内联后冲突概率 100%。这不是 svgo 的错,是它假设你在做独立文件。压缩配置里要么关掉cleanupIds的minify,要么加prefix,这个坑在第 5 章会展开。
3. 接进页面的四条路:img、background、inline 与 symbol sprite 怎么选
拆包阶段定的 harness 决定后面所有工作量。图标接入方式没有绝对优劣,只有“要不要改色”“要不要复用”“要不要进构建”这三问的答案不同。我按改色需求和复用规模两条轴来选,基本不会错。
3.1 img 与 CSS background:最省事,但颜色是死的
<!-- 装饰性图标:alt 留空,屏幕阅读器跳过 --> <img class="icon" src="/icons/virtual-machines.svg" alt="" width="18" height="18"> <!-- 语义性图标:alt 写清楚它代表什么,不要写“图标” --> <img class="icon" src="/icons/storage-account.svg" alt="存储账户" width="18" height="18"><img>的边界很明确:SVG 在图片上下文里是一份独立文档,CSS 的fill、color进不去,只能用filter曲线救国。所以凡是需要 hover 变色、禁用态变灰、深色模式换色的场景,这条路直接排除。它的优势是零配置、天然隔离、浏览器缓存友好,适合纯文档类页面和邮件模板。
background版本同理,只是把图标当装饰纹理用:
.icon-vm { display: inline-block; width: 18px; height: 18px; /* contain 保证不裁切,no-repeat 防平铺,center 对齐 */ background: url("/icons/virtual-machines.svg") center / contain no-repeat; }一个细节:background方案下图标不算文档内容,无障碍工具完全读不到。要么加role="img"和aria-label在宿主元素上,要么老老实实留一段可见文字。别拿background去承载“唯一的语义信息”。
3.2 内联 SVG:能改色、能加动效,代价是 id 污染
内联是唯一能让图标完整继承 CSS 的方案。把文件内容直接贴进模板,根节点给 class,路径颜色交给currentColor:
<button class="toolbar-btn" type="button"> <svg class="icon" viewBox="0 0 18 18" role="img" aria-label="新建虚拟机"> <path d="M2 3h14v12H2z" fill="currentColor"/> </svg> <span>新建虚拟机</span> </button>.icon { width: 18px; height: 18px; flex: none; /* 防止在 flex 容器里被文字挤扁 */ vertical-align: -0.125em; /* 让基线跟文字对齐,具体值随字号微调 */ color: #0b6a9e; /* 通过 color 驱动 currentColor */ } .toolbar-btn:hover .icon { color: #084c73; } .toolbar-btn[disabled] .icon { color: #9aa4b0; }flex: none这条很容易被忽略。图标放在 flex 行里时默认flex-shrink: 1,容器一挤,宽高就被压缩成非整数,渲染立刻发虚。vertical-align的负值是把图标往下压,让它的视觉中心跟小写字母 x 的高度对齐;不同字体取值不同,别照抄。
代价就是 2.3 节说的 id 冲突。解决办法是在模板渲染前做一次 id 作用域化,用一个稳定的 scope 前缀(用 slug 或它的短哈希,保证同一图标多次插入时前缀一致):
import re def scope_ids(svg: str, scope: str) -> str: """给内联 SVG 的 id 与所有引用加唯一前缀,避免同页图标互相污染。 scope 用 slug 或其短哈希;同一图标多次插入时前缀稳定,不会重复定义。""" for old in set(re.findall(r'id="([^"]+)"', svg)): new = f"{scope}-{old}" svg = svg.replace(f'id="{old}"', f'id="{new}"') # 三种引用写法都要覆盖:url(#x)、xlink:href="#x"、href="#x" svg = svg.replace(f'url(#{old})', f'url(#{new})') svg = svg.replace(f'xlink:href="#{old}"', f'xlink:href="#{new}"') svg = svg.replace(f'href="#{old}"', f'href="#{new}"') return svg这段的顺序有讲究:必须先处理url(#x)再处理href="#x",否则url(#x)被href规则误伤的概率虽然低,但字符串已经变了,replace就找不到了。用set去重是必要的,同一个 id 可能在<defs>和<use>里各出现一次。这个函数放在构建期跑,不要放到浏览器运行时——线上每渲染一个图标做一次正则替换,列表页几十上百个图标就是明显的卡顿。
3.3 symbol sprite +<use>:一次注入,全站复用
图标规模超过三四十个、页面里反复出现同一批时,内联会让 HTML 体积膨胀得很难看。这时用 sprite:把每个 SVG 包成<symbol id="...">,整份文档注入一次,后面全是引用。
# svg-sprite 生成 symbol 形式的雪碧图,产物为单个 icons.svg npx svg-sprite \ --symbol \ --symbol-dest dist \ --symbol-sprite icons.svg \ "icons/*.svg"--symbol指定输出形式,--symbol-dest是产物目录,--symbol-sprite是产物文件名。生成的 symbol id 默认取自文件名,正好和 2.1 节的 slug 对上。引用侧:
<svg class="icon" aria-hidden="true"> <use href="/dist/icons.svg#virtual-machines"></use> </svg>aria-hidden="true"是必须的:<use>会被读屏软件当成一个空图形,加了这个属性它才会被跳过,语义交给旁边的可见文字。
这里有个绕不开的限制:部分浏览器在file://协议或跨域场景下不允许<use>引用外部文档,图标直接不显示,控制台只有一条警告。稳妥做法是在应用启动时把 sprite 拉回来内联注入:
// 启动时把 sprite 注入 DOM,规避外部 <use> 在协议/同源上的限制 fetch('/dist/icons.svg') .then(res => res.text()) .then(txt => { const box = document.createElement('div'); box.style.display = 'none'; // 不占布局,只提供 symbol 定义 box.innerHTML = txt; document.body.insertBefore(box, document.body.firstChild); });用innerHTML是因为symbol定义必须被解析进 DOM 才生效,textContent塞进去只是一段文本。前提是 sprite 由你自己构建、内容可信;如果图标来自外部上传,这条路径必须换成带白名单的解析器,不能直接innerHTML。
3.4 打包器插件:把压缩和 symbol 化塞进构建流程
手写构建脚本适合一次性迁移,长期维护还是交给打包器。以 Vite 为例,vite-plugin-svg-icons能在构建期完成 svgo 压缩、symbol 包装和自动注入三件事:
// vite.config.js import path from 'node:path' import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' export default { plugins: [ createSvgIconsPlugin({ // 图标源目录,插件会扫描其中所有 .svg iconDirs: [path.resolve(process.cwd(), 'src/icons')], // 生成的 symbol id 模板,[name] 会被替换为文件名 symbolId: 'icon-[name]', svgoOptions: { plugins: [ { name: 'preset-default', params: { // 关键:保留 viewBox,否则图标缩放会退化成按 width/height 拉伸 overrides: { removeViewBox: false } } } ] } }) ] }三个参数里最容易出事的是svgoOptions。preset-default自带一组默认开启的插件,其中removeViewBox在某些版本里是开着的历史包袱,它会把viewBox删掉只留宽高,图标在响应式容器里就完全失去等比缩放能力。overrides是唯一可靠的关法,别指望在顶层写removeViewBox: false能生效。symbolId用icon-[name]是为了和手写<use>的命名习惯对齐,团队里混用两套命名是最容易出“找不到图标”的原因。
4. 主题与状态色:用 currentColor 和 CSS 变量接管图标配色
图标接进来只是开始,真正决定它能不能长期用的是配色策略。这批图标分两类:单色符号(线条、简单形状)和多色产品图标(带渐变、多色块)。前者应该完全交给 CSS,后者必须保留原色。混在一起用同一套规则,必然翻车。
4.1 单色图标接管:currentColor 是唯一正解
单色图标的路径颜色应该全部写成fill="currentColor",这样颜色由最近的color属性决定,天然继承、天然支持 hover 和禁用态。原文件里如果写死了fill="#0b6a9e",需要在入库时批量替换——注意只替换单色图标,多色图标动了就废了。
批量替换可以用 svgo 的convertColors,但它会替换所有颜色值,用之前必须先按目录把单色图标挑出来:
// svgo.config.js —— 只对 mono 目录生效的压缩配置 module.exports = { plugins: [ { name: 'preset-default', params: { overrides: { removeViewBox: false, // 保 viewBox cleanupIds: { minify: false } // 保留 id,内联时自己加前缀 } } }, { name: 'removeDimensions' }, // 去掉顶层 width/height,尺寸交给 CSS { name: 'convertColors', params: { currentColor: /^#(0b6a9e|1a1a1a|333333)$/i } // 只转指定的深色 } ] }convertColors的currentColor参数接受正则,只把匹配到的色值换成currentColor,其余保留。这比无差别替换安全得多。removeDimensions去掉的是顶层宽高,viewBox留着,两者分工明确:前者让 CSS 接管尺寸,后者保证缩放比例。
即使路径上写了fill,也可以用 CSS 强制覆盖,但作用域必须收窄:
/* 只对单色图标类生效,绝不能作用于多色产品图标 */ .icon--mono path, .icon--mono rect, .icon--mono circle { fill: currentColor; } .icon--mono [stroke] { stroke: currentColor; }CSS 属性选择器抓[stroke]是为了处理描边式图标——这类图标fill="none",靠stroke出形,只改 fill 完全没反应。很多人卡在这里,以为图标坏了,其实是抓错了属性。
4.2 深色模式与多色图标:不能一键反色
单色图标在深色模式下只需换变量值:
:root { --icon-primary: #0b6a9e; --icon-muted: #6b7280; } @media (prefers-color-scheme: dark) { :root { --icon-primary: #7cc4ea; /* 深色底上提亮,保持对比度 */ --icon-muted: #98a2b3; } } .icon { color: var(--icon-primary); }用prefers-color-scheme而不是 JS 切 class,是因为前者在首屏渲染前就生效,不会出现“先亮后暗”的闪烁。如果产品需要手动切换主题,那就把变量挂在html[data-theme="dark"]上,两套并存,媒体查询只作为默认值。
多色图标在深色底上是另一回事。这批产品图标里带浅灰、白色元素的不少,直接放到深色背景上会“缺一块”,看起来像是渲染失败。常见做法不是反色,而是给它垫一个浅色底片:
.icon-tile { display: inline-grid; place-items: center; width: 36px; height: 36px; border-radius: 6px; background: #f3f6fa; /* 多色图标永远放在可控底色上 */ } @media (prefers-color-scheme: dark) { .icon-tile { background: #1b2430; } /* 只做轻微提亮,别用 invert(1),渐变会彻底变形 */ .icon-tile .icon { filter: brightness(1.15) saturate(0.95); } }place-items: center是align-items+justify-items的简写,一行搞定居中。filter那条要克制:brightness超过 1.2 白色区域会过曝,saturate低于 0.9 品牌色就不准了。invert(1)这种操作对多色渐变图标是灾难,渐变方向和色相会一起翻掉,看起来像另一个产品。
4.3 状态语义:先定色板,再谈图标
图标颜色一旦散落在各页面硬编码,三个月后没人说得清“为什么这里用蓝色那里用青色”。我一般先定一张状态色板,图标只引用语义变量,不直接写色值:
| 语义 | CSS 变量 | 浅色值 | 深色值 | 典型场景 |
|---|---|---|---|---|
| 主操作 | --icon-primary | #0b6a9e | #7cc4ea | 工具栏、主按钮 |
| 次要 | --icon-muted | #6b7280 | #98a2b3 | 辅助操作、说明文字旁 |
| 成功 | --icon-success | #15803d | #4ade80 | 运行中、健康 |
| 警告 | --icon-warning | #b45309 | #fbbf24 | 降级、配额接近上限 |
| 危险 | --icon-danger | #b42318 | #f87171 | 停止、删除、异常 |
/* 图标只消费语义变量,页面不出现裸色值 */ .icon--success { color: var(--icon-success); } .icon--danger { color: var(--icon-danger); }这张表的价值在于约束:当有人想在页面里写color: #15803d时,review 阶段就能拦下来,让他改成icon--success。图标本身的形状不变,颜色换主题时整体跟着走,不需要逐个文件改。
5. 避坑与排查:五个让图标当场翻车的现场
这一章全是踩过的坑,按“现象 → 原因 → 解决”写,每一条都能对上具体文件和一个可验证的判断方法。遇到类似症状时先按顺序核对,比盲改配置快得多。
5.1 压缩后图标变形或整块变黑
现象:跑完 svgo,一部分图标从原来的渐变形状变成纯黑方块,或者边框消失只剩主体。
原因:preset-default里几个插件默认开启,组合起来破坏了引用关系。cleanupIds把<linearGradient id="g1">压成id="a",同时把fill="url(#g1)"同步改成url(#a)——单文件没问题,但只要同一页面出现第二个被压过的图标,两个a撞车,后者的渐变引用就指向了前者。removeViewBox则会把画布信息删掉,缩放直接按 width/height 拉伸,比例一错就变形。还有一种更隐蔽的:removeUnknownsAndDefaults会删掉“看起来是默认值”的属性,某些描边宽度和fill-rule就在它的清理范围内。
解决:在 svgo 配置里显式覆盖,不要用默认值赌运气。removeViewBox: false必关;cleanupIds要么minify: false要么加prefix;改动后必须做一次视觉回归——拿压缩前后的文件列表逐个渲染截图对比,只看文件体积下降是自欺欺人。稳妥的验证方式是把同一批图标全部内联到一张测试页上,一次性看有没有串色和变形,比单文件打开有效得多。
5.2 同一页面多个图标串色
现象:单独打开每个图标都正常,内联到同一页后,有的图标用了别人的渐变,有的图标渐变完全丢失。
原因:2.3 节说过的 id 冲突。浏览器的url(#x)解析走的是整份文档,重复 id 时取文档中第一个匹配项。图标数量越多、顺序越不稳定,表现越随机,所以经常被误判成“样式加载顺序问题”。
解决:内联前跑 3.2 节的scope_ids,scope 用 slug。验证方法很直接,在控制台执行document.querySelectorAll('[id]')然后统计重复值,一条命令就能确认:
// 在控制台粘贴,返回页面上所有重复 id 及其出现次数 const seen = new Map(); document.querySelectorAll('[id]').forEach(el => { seen.set(el.id, (seen.get(el.id) || 0) + 1); }); console.table([...seen].filter(([, n]) => n > 1).map(([id, n]) => ({ id, count: n })));若列表为空但仍有串色,那就不是 id 问题,往 CSS 的fill继承方向查——父级svg上的fill会被没写 fill 的 path 继承。
5.3 小尺寸下边缘发虚、细线断裂
现象:18px 画布的图标放到 16px 或 20px 的按钮里,看起来糊一层灰,1px 描边偶尔断开。
原因:2.2 节讲过的非整数倍缩放。18 缩到 16 是 0.888,描边落在 0.44、1.33 这种位置,抗锯齿后就是灰边。另外,如果图标根节点被父容器flex压缩成 15.6px 这种值,问题会更严重,因为浏览器内部还会再做一次舍入。
解决:第一,把尺寸改成画布整数倍(18/36/54);第二,给图标加flex: none防止被挤压;第三,确实必须用 16px 的场景,改用描边更粗的那一档图标,或者接受轻微模糊。有个反面做法是加shape-rendering="crispEdges",它会让曲线和圆角直接变成锯齿,得不偿失,只在纯直角图标上偶尔可用。
5.4 改了 fill 完全没反应
现象:CSS 里写了.icon path { fill: red },页面上颜色纹丝不动,开发者工具里能看到规则被划掉或者根本没应用。
原因通常有三种。一是原文件路径上带了内联style="fill:#xxx",内联样式优先级高于外部选择器,只能靠!important或者构建期清掉。二是 CSS 规则作用在<img>引用的 SVG 上——那是独立文档,选择器根本进不去,这条规则从一开始就不可能生效。三是原文件里带<style>块,里面写了.cls-1{fill:#xxx},它在 SVG 文档内部生效,外部选择器打不过它。
解决:构建期统一处理,别在运行时硬碰。用 svgo 的inlineStyles和minifyStyles把<style>块内联到属性上,再统一改写成currentColor;确认接入方式不是<img>;检查文件里有没有残留style=。判断方法很简单:grep -c 'style=' 文件和grep -c '<style' 文件,两个都是 0 才说明干净。
5.5 导出 PDF 或打印时图标消失
现象:页面里显示正常,Ctrl+P 打印预览或者导出 PDF 后,图标位置只剩空白,或者变成一串方框。
原因:<use>引用的外部 sprite 在打印渲染器里加载时机不同,常常来不及解析;background-image默认也不打印。还有一种:图标尺寸依赖em单位,打印样式表把字号重置成pt后,图标被算成了极小值。
解决:打印样式里给图标显式回退。最省事的做法是打印时改用内联 SVG 或 data URI 形式的 background,同时把尺寸单位写死成px:
@media print { /* 外部 sprite 在打印渲染器里不可靠,改用内联背景并锁死像素尺寸 */ .icon-sprite { display: none; } .icon-fallback { display: inline-block; width: 18px; height: 18px; background: url("/icons/virtual-machines.svg") center / contain no-repeat; } }导出前一定要用真实打印预览验证一遍,尤其是浏览器自带的“另存为 PDF”,它的渲染路径跟window.print()不完全一致。只看屏幕效果就交付,这类问题一定会在客户那边爆出来。
6. 上线前跑一遍体检:40 行脚本卡住 90% 的图标问题
前面五章的问题,八成可以在提交前用一个静态检查脚本拦下来。我把它挂在pre-commit上,跑一次不到一秒,比事后翻车再回头查便宜太多。脚本不追求全,只查四件事:文件名规范、viewBox 存在、顶层宽高是否清除、id 是否重复(跨文件查重,因为内联才是主要用法)。
import re, sys from pathlib import Path from xml.etree import ElementTree as ET ## 文件名规范:五位编号 + 类别 + kebab-case 产品名 NAME_RE = re.compile(r"^\d{5}-icon-(service|general)-[a-z0-9]+(-[a-z0-9]+)*\.svg$") def check(path: Path, id_owner: dict) -> list[str]: errs, raw = [], path.read_text(encoding="utf-8") root = ET.fromstring(raw) if not NAME_RE.match(path.name): errs.append("命名不符合 00000-icon-service-name.svg") if "viewBox" not in root.attrib: # 缺 viewBox 无法等比缩放 errs.append("缺少 viewBox") for attr in ("width", "height"): # 顶层宽高会挡住 CSS if attr in root.attrib: errs.append(f"顶层硬编码 {attr}") for el in root.iter(): # 跨文件 id 查重 _id = el.get("id") if _id and _id in id_owner and id_owner[_id] != path.name: errs.append(f"id '{_id}' 与 {id_owner[_id]} 冲突") elif _id: id_owner[_id] = path.name return errs def main(root_dir: str) -> int: id_owner, bad = {}, 0 for p in sorted(Path(root_dir).rglob("*.svg")): errs = check(p, id_owner) if errs: bad += 1 print(f"[FAIL] {p.name}: {'; '.join(errs)}") print(f"checked done, {bad} file(s) with issues") return 1 if bad else 0 if __name__ == "__main__": sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "./icons"))NAME_RE里的[a-z0-9]+(-[a-z0-9]+)*比[a-z0-9-]+严格,能挡住a--b和a-这类脏名字。id_owner字典跨文件累积,同一个 id 出现在两个文件里就立刻报冲突——这是 5.2 节那条坑的前置拦截。返回值用sys.exit透出,才能在 CI 和pre-commit里被正确识别为失败。
跑法就一行,接入 husky 时把python scripts/check_icons.py ./src/icons写进pre-commit钩子即可。误报率高的规则要慎用:比如“禁止任何fill属性”就过于激进,多色图标必须有 fill。规则宁少勿滥,一条稳定生效的规则比十条天天被绕过有用得多。
最后说个习惯。早些年我接手一个云管后台,图标是运营同学从各处凑的,交付前没人管,上线一周后客户反馈“导出 PDF 图标全没了”,我和同事通宵翻了两百多个文件,最后发现是三个文件带了各自的内联<style>块,导致 CSS 覆盖失效、打印回退也没写。从那以后我每次接图标资源,第一件事就是跑一遍体检脚本,第二件事是内联到一张测试页上肉眼过一遍,第三步才是往业务里接。前面两步加起来不到十分钟,能省掉的返工往往是一整天。希望帮到你。
本文还有配套的精品资源,点击获取