ARTICLE DETAIL

资讯详情

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

Iconify离线部署全攻略:解决图标不显示的四套可靠方案

Iconify离线部署全攻略:解决图标不显示的四套可靠方案 “图标不显示”这件事放在在线环境里通常五分钟就能定位放到离线环境里就成了玄学。iconify 作为目前覆盖面最广的图标方案很多人平时只用到它在线加载的那一面却忽略了它本质上依赖一个远程 API一旦项目要部署到无外网的内网环境或者你在做本地工具链、离线打包那些原本秒开的图标会毫无征兆地集体消失。我最近大半年在几个项目里把 iconify 离线使用反复折腾过一遍从构建时按需编译到自托管 API 都试过也踩了不少文档里不会写明白的坑。这篇文章我会把几条可行的离线路线、每一步操作、以及每条路线背后的原理讲清楚给正在做内网项目、离线打包或者单纯不想让页面依赖外部接口的朋友一个可以直接参考的落地清单。内容不挑基础熟悉 Vue 或 React 基本用法就能跟完。1. 先理清 iconify 在线模式的依赖链路1.1 它本质上不是一套图标包而是一套数据协议iconify 和 iconfont 那种“打好的字体文件”完全不是一回事。社区把海量图标集整理成标准化 JSON 数据每个图标集对应一个 JSON 文件里面记录每个图标的 SVG body、尺寸、别名等信息。iconify/iconify、iconify-icon、iconify/vue这些库都只是渲染器它们本身不携带任何图标数据。比如 mdi 这个图标集里的 home 图标JSON 大概是这样的{ prefix: mdi, icons: { home: { body: path d\M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z\/, width: 24, height: 24 } }, width: 24, height: 24 }body就是要插入页面的 SVG 内部内容。这个数据可以是几 KB 的单图标也可以是几 MB 的整个图标集。离线使用的第一条核心思路就是让这些数据在你需要的时候出现在本地。1.2 在线渲染时一个图标从出现到呈现在页面经历了什么在线模式下页面里只要存在span classiconify>npm create vitelatest icon-offline-demo -- --template vue cd icon-offline-demo npm install然后安装离线图标化需要的两个依赖npm install -D unplugin-icons iconify/jsoniconify/json是数据源里面是所有社区维护的图标集 JSON 文件安装包比较大是正常的一百多 MB 不要慌它只是开发依赖不会进产物。unplugin-icons则是构建插件负责在编译阶段把图标数据抽出来变成组件。这里有一个小提示不要开启让插件帮你自动安装依赖的开关因为自动安装需要访问 npm registry在离线环境里反而会卡住构建。老老实实把包一次性装好最省心。3.2 在 vite.config.js 里接入插件import { defineConfig } from vite import vue from vitejs/plugin-vue import Icons from unplugin-icons/vite export default defineConfig({ plugins: [ vue(), Icons({ compiler: vue3 }) ] })compiler参数要根据框架来Vue3 写vue3Vue2 写vue2React 写jsxSvelte 写svelte。这个参数决定了虚拟模块最终生成的是哪种组件。如果你用的是 TypeScript还需要在env.d.ts里加一行类型声明否则编辑器会对虚拟模块路径报红/// reference typesunplugin-icons/types/vue /3.3 在页面里直接使用编译好的图标用法非常简单虚拟模块的路径规则是~icons/{图标集前缀}/{图标名}template div IconHome stylefont-size: 32px; / IconSettings / /div /template script setup import IconHome from ~icons/mdi/home import IconSettings from ~icons/mdi/cog /script构建时unplugin-icons会打开本地的iconify/json/mdi.json取出对应图标的 body把它包装成一个 Vue 组件。最终产物里是完整的 SVG 内容整个过程不出现任何运行时网络请求。3.4 动态图标与离线场景的冲突最容易被卡住的地方上面这个做法写死的图标名没问题但实际项目里很多图标来自后端配置比如菜单栏接口返回mdi:home、mdi:cog这样的字符串。由于构建时看不到运行时才出现的字符串~icons/mdi/home这种虚拟模块根本没法直接解析。有几种处理方式。第一种白名单映射。如果你系统里可能出现的图标总数是可控的可以维护一张映射表import IconHome from ~icons/mdi/home import IconCog from ~icons/mdi/cog export const iconMap: Recordstring, any { mdi:home: IconHome, mdi:cog: IconCog }模板里动态渲染template component :isiconMap[menu.icon] || DefaultIcon / /template第二种放弃构建时编译改用运行时注册子集 JSON配合iconify/vue的Icon组件渲染。我后面第 5 节会专门讲这个切换过程中容易踩的坑。第三种直接切换到自托管 API 方案后端随便下发什么图标名前端不用管。排除法选择图标数量 50 个以内用白名单数量可控但想灵活一点用子集注册数量不可控、多个系统共用就自托管。3.5 构建产物验证怎么确定真的没有远程请求很多人做完了还是不放心我教你一个快速验证方法。先执行构建npm run build然后在构建产物目录里搜一下远程 API 的域名grep -r api.iconify.design dist/如果没有输出说明产物里根本不存在这个远程地址这个项目部署到任何断网环境都不会再因为这个域名找不到而图标空白。如果还想看图标模块的体积可以接入rollup-plugin-visualizer打开打包报告后能看到每个图标组件都是一个独立的小 chunk这就证明按需编译生效了而不是把整个 mdi.json 全量塞了进去。4. 实操记录内网环境下自托管 Iconify API4.1 什么场景才需要自托管 API我一开始也觉得自己写个接口是小题大做直到遇到一个已经上线两年的存量后台前端根本没有做任何本地化封装页面里全是span classiconify>npm install express然后在项目里放一个server.jsconst express require(express) const { readFileSync, existsSync } require(fs) const path require(path) const app express() const cache new Map() const BASE path.join(__dirname, node_modules/iconify/json/json) function loadCollection(prefix) { if (cache.has(prefix)) return cache.get(prefix) const file path.join(BASE, ${prefix}.json) if (!existsSync(file)) return null const data JSON.parse(readFileSync(file, utf8)) cache.set(prefix, data) return data } app.get(/:prefix.json, (req, res) { const { prefix } req.params const data loadCollection(prefix) if (!data) { return res.status(404).json({ error: collection not found }) } const names String(req.query.icons || ).split(,).filter(Boolean) if (!names.length) { return res.json(data) } const result { prefix, icons: {}, aliases: {}, width: data.width, height: data.height } for (const name of names) { if (data.icons[name]) { result.icons[name] data.icons[name] } else if (data.aliases data.aliases[name]) { result.aliases[name] data.aliases[name] const parent data.aliases[name].parent if (parent data.icons[parent] !result.icons[parent]) { result.icons[parent] data.icons[parent] } } } res.json(result) }) app.listen(3000, () { console.log(icon service running at http://127.0.0.1:3000) })这个接口做的事情很简单按图标集前缀读取本地 JSON 文件把请求参数icons里指定的图标裁剪出来返回。cache这个 Map 是为了避免每次请求都重新解析几 MB 的大 JSON这在图标集文件很大的时候非常关键。如果图省事也可以直接把整个 JSON 对象抛回去内网带宽一般不在乎这点流量。但为了模拟在线 API 的按需行为裁剪一下更利落。4.3 让前端代码完全不动的配置方式接口跑起来后接着把请求路由指过去。如果你有条件在内部 DNS 上添加解析记录把api.iconify.design解析到这台服务器的内网 IP这是最干净的方式。没有内部 DNS开发机临时改 hosts 也能撑住。服务器上再用 Nginx 做一层反向代理server { listen 80; server_name api.iconify.design; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; } }这样前端所有对api.iconify.design的请求都会落到本地 Node 服务上。存量的页面不需要重新编译、不需要改一行代码刷新后图标就从内网数据源加载了。还要注意跨域问题如果前端页面和图标接口不在同一个域名下需要在 Nginx 里补上 CORS 头add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Headers *;4.4 数据预热与请求日志内网服务虽然简单还是有几个可以优化的地方。第一数据预热。mdi.json这类大文件首次解析可能需要几百毫秒如果某个图标集被高频使用建议服务启动时就把常见前缀加载进内存避免第一个请求的用户承受这个延迟。const PRELOAD_PREFIX [mdi, tabler, material-symbols] PRELOAD_PREFIX.forEach(loadCollection)第二请求日志。加一句最简单的console.log(req.path, req.query.icons)跑上一段时间就能知道系统里真实用到了哪些图标集、哪些图标。这份数据后面做子集裁剪时非常有用。第三压缩。在 Nginx 里开一下 gzipgzip on; gzip_types application/json;这样几 MB 的图标集也能压缩到几百 KB内网访问更快。5. 实战避坑离线方案最容易翻车的几个现场5.1 图标“时而正常时而空白”先看 Network 再下结论我遇到过最迷惑的现场是同一个页面A 机器图标全显示B 机器一只都没有过一会儿再看又好了。排查时不要先去猜缓存先打开 DevTools 的 Network 面板筛选api.iconify.design相关请求。如果看到请求发出去了但在等待响应说明当前代码还在用在线加载模式。那些碰巧显示出来的图标是因为之前某个时刻请求成功过、数据被浏览器或脚本缓存下来了。这只能算侥幸不是修好了。最终结论通常是两个要么把数据彻底打包进应用要么把接口换到内网。千万别指望“多刷新几次”能解决离线环境里这是概率问题。5.2 全量引入 iconify/json 后打包体积失控有人觉得运行时注册很香就直接写了一行import mdi from iconify/json/json/mdi.json addCollection(mdi)页面确实能显示图标一个 mdi 全集可能有七千多个图标原始 JSON 超过 2MB打包后字符串膨胀一下常见的结果是主 JS 体积直接多出 3 ~ 5MB。哪怕你只用了十个图标代价也是一样的。正确的姿势是用脚本提前裁剪子集。比如我在项目里维护一个图标清单文件icons.txtmdi:home mdi:account mdi:cog mdi:logout然后用一个 Node 脚本读取iconify/json里的原始数据生成子集 JSONconst { readFileSync, writeFileSync } require(fs) const prefix mdi const list [home, account, cog, logout] const src JSON.parse( readFileSync(node_modules/iconify/json/json/${prefix}.json, utf8) ) const subset { prefix, icons: {}, aliases: {}, width: src.width, height: src.height } for (const name of list) { if (src.icons[name]) { subset.icons[name] src.icons[name] } else if (src.aliases src.aliases[name]) { subset.aliases[name] src.aliases[name] } } writeFileSync(src/assets/icons/${prefix}.subset.json, JSON.stringify(subset))应用入口处import { addCollection } from iconify/iconify import mdiSubset from ./assets/icons/mdi.subset.json addCollection(mdiSubset)这样打包进产物的就是一份几 KB 的 JSON不是几 MB。5.3 别名alias和旋转翻转字段才是隐藏炸弹裁减子集时有一个特别容易踩的坑iconify 数据里很多图标名不是真实存在的图标而是别名。它并不直接拥有body字段而是指向另一个图标还可能带有旋转、翻转等附加属性。如果你裁减脚本只拷贝了icons字段没有保留aliases字段那么这些图标名在本地数据里对应不到任何 SVG显示的时候就是一个空位。排查方法也不复杂打开原始 JSON搜索这个图标名看它是在icons下还是aliases下。如果是别名就需要把aliases一起带进子集并且保证它指向的父级图标也在子集里否则客户端解析不出实际路径。类似的坑还有rotate、hFlip、vFlip这些字段。你从原始 JSON 里抄数据时如果只抄了body其他属性全丢了图标可能会方向不对或尺寸异常。5.4 注册数据后图标还是闪一下空白运行时注册方案里另一个常见问题是注册时机太晚。有人在组件的onMounted里异步加载 JSON再调用addCollection结果组件第一次渲染的时候图标数据还没进来自然出不来内容。iconify/vue这个库本身是纯同步渲染的它不会像在线模式那样自己去请求远程数据只认当前内存里已经注册过的数据。所以如果你决定用运行时注册就要把addCollection放在应用入口的最前面确保任何组件渲染之前数据已经就位。如果数据来源非得是异步接口那页面上要加一个 loading 状态等注册完成后再渲染图标区域。用一个可响应的ready变量控制是最简单的办法。5.5 uniapp 离线打包时 iconify 的落地方式uni-app 离线打包是热搜词里的另一个高频场景这里单独说几句。离线打包出来的 App 是没有外网保障的如果 H5 端或 App-vue 页面里直接用了 iconify 在线加载模式打包后打开页面必然是图标空白。我推荐的落地方式看页面类型来定App-vue 页面如果项目能改 vite 配置依然可以用unplugin-icons把固定图标编译成组件图标多变就提前生成子集 JSON用iconify/vue动态渲染。App-nvue 页面SVG 支持比较弱更稳妥的做法是把需要的图标导成 SVG 文件放到 static 目录或者合并转换成 iconfont 字体文件用字体方式渲染。如果你已经接了一些 uts 插件做离线打包的原生能力扩展图标这块建议还是留在前端层处理别把依赖外部 API 的 JS 包直接塞到原生层逻辑里否则排查问题会很痛苦。一句话总结这个场景先把图标数据变成安装包里的本地资源再考虑怎么渲染。5.6 自托管 API 模式下遇到的跨域和 HTTPS 问题自托管 API 方案也不是配完 Nginx 就万事大吉。如果前端页面域名和api.iconify.design不是同一个源浏览器默认会拦跨域请求需要把 CORS 头补上。还有一个更隐蔽的问题如果前端页面是 HTTPS而内网图标接口只走了 HTTP浏览器会以混合内容为由拦截。两个办法要么内网也配置证书并把根证书发到每台客户机要么整个内网系统统一走 HTTP 域名并在安全策略允许的范围内使用。这个限制不是代码能不能写的问题而是浏览器层面的安全机制提前想清楚能省很多沟通成本。6. 不同团队规模该怎么选离线方案6.1 个人项目和小团队无脑 unplugin-icons如果是单系统、图标以少量固定图标为主直接选构建时按需编译。它写起来最简单体积最小也不会因为运行时数据没注册而产出空图标。就算有少量动态图标一张白名单映射表足够覆盖绝大多数场景。6.2 中大型后台、图标由后端配置子集 JSON 运行时注册后台管理系统里菜单、按钮图标往往由后端的配置下发动态字符串是常态。这种项目建议维护一份图标清单用脚本生成子集 JSON在入口同步注册然后用iconify/vue根据字符串动态渲染。这样既保留灵活度又不需要在运行时碰网络请求。你只需要保证新增图标时同步更新一下清单和脚本。6.3 多系统统一治理自托管 API当你有三四个系统都在使用 iconify而且不想每个前端都改自托管 API 是长期最省心的方案。部署一次所有系统共享一个内网图标数据源后续新增图标集只需要把 JSON 文件放到服务器上。代价是你要为它负责心跳、日志、扩容这些基础设施问题。如果公司内网有统一网关也可以把接口挂到网关后面省掉独立 Nginx。6.4 我现在的默认做法结合这些项目经验我给自己的默认选择是面向内网的 B 端产品优先用构建时按需编译 白名单映射表。这张白名单能覆盖掉大多数业务场景构建产物干净、稳定不需要额外维护服务。万一后端突然下发了一个白名单里没有的图标名我会在页面里兜底显示一个本地默认占位 SVG同时在日志里记录下来而不是让整个页面因为一个图标就出现空白区域。等这类新图标累积到一定数量再把它加进清单、重新构建。最后再分享一个我保留了很久的习惯不管选哪种离线方案项目里一定放一个icons.txt清单记录所有使用到的图标名。它既是子集裁剪的依据也是一份天然的前端图标使用规范。离线这件事没有银弹但把数据链路控制在自己手里以后至少不会再有“部署完页面所有 icon 全没了”的深夜事故。
返回列表