背景
在有一定历史的项目中,使用 icon-font 来实现图标功能非常常见。但是随着项目规模的增长,icon-font 会有以下几个问题:
- 图标体积过大,影响资源加载速度
- 在我的项目中 379 个彩色图标(使用 svg symbol 引用),iconfont.js 体积为 494KB
- 661 个线性图标(使用 font-class 引用),iconfont.ttf 体积为 170KB
- 使用的时候必须在首屏加载,否则会有闪烁的问题,延长白屏时间
- 如果只想使用几个图标,那么需要引入整个 iconfont.js 或者 iconfont.ttf,造成浪费
在我的场景中,我需要为一个已经开发了几年的 PC 端产品添加移动端版本,需要使用 PC 端的一部分图标。 如果直接使用 icon-font ,那么需要引入所有的图标,这个资源大小在移动端是不可接受的。 但是如果要 UI 重新为移动端新建一个 icon-font 项目,把所需图标放进去,又会造成两边可能不同步的问题。
那么有没有一种方法,可以只使用 icon-font 的部分图标呢?
解决方案
使用 UnoCSS 的 icons preset,就能实现图标的按需引入。
有关纯 CSS 图标的概念和原理可以参考 antfu.me/posts/icons... 这里我就不介绍了
核心是所有的图标都是用 svg 格式,然后通过 UnoCSS 的按需加载能力,做到图标按需引入。 同时配合脚本自动解析原有 icon-font 的图标,转换为 svg 格式。
1.将原有 icon-font 转换为 UnoCSS 需要的格式
UnoCSS 所需的图标需要一个 JSON 文件,内容是一个键值对,key 是图标名称,value 是图标 svg 内容。类似
json
{
"theright-line": "<svg viewBox=\"0 0 1024 1024\">....</svg>",
"describe-line": "<svg viewBox=\"0 0 1024 1024\">....</svg>"
}
这里我写了一个 node 脚本,可以自动解析原有 icon-font 的图标,转换为 svg 格式。
js
import { readFile, writeFile } from 'fs/promises';
import { readdir, rm, mkdir } from 'fs/promises';
import { join } from 'path';
// 需要从 iconfont 下载的部署文件,类似 font_3519475_tursfnn52si 这种,放在 iconfonts 目录下
const iconsDir = './iconfonts';
try {
await rm('./dist', { recursive: true, force: true });
await mkdir('./dist');
const dirs = await readdir(iconsDir, { withFileTypes: true });
const iconDirs = dirs.filter((dir) => dir.isDirectory());
// 遍历每个 iconfont 项目文件夹
for (const dir of iconDirs) {
const dirPath = join(iconsDir, dir.name);
const jsonPath = join(dirPath, 'iconfont.json');
try {
const jsonContent = await readFile(jsonPath, 'utf-8');
const infoObject = JSON.parse(jsonContent);
const glyphs = infoObject.glyphs;
const setName = infoObject.name;
const fullSvgFileContent = await readFile(
join(dirPath, 'iconfont.js'),
'utf-8'
);
// 提取 iconfont.js 中的 svg 内容
// iconfont.js 中的所有图标都定义在一个 svg 下,使用 <symbol> 标签定义每个图标
const svgContent =
fullSvgFileContent.match(
/window\._iconfont_svg_string_\d+='([^']+)'/
)?.[1] || '';
// 提取 svg 中的 <symbol> 标签
const symbols = [];
const symbolRegex = /<symbol[^>]*id="([^"]*)"[^>]*>([\s\S]*?)<\/symbol>/g;
let match;
while ((match = symbolRegex.exec(svgContent)) !== null) {
const [svg, id, content] = match;
// 提取 symbol 的 viewBox 属性
const viewBox = svg.match(/viewBox="([^"]*)"/)?.[1] || '';
symbols.push({
id,
content,
viewBox,
});
}
const iconSet = {};
// 判断是否是彩色图标(这里需要根据你们项目的实际情况修改)
const isColorIcon = setName.includes('color');
symbols.forEach((symbol) => {
// 这里的 du-icon 是我们项目中图标的前缀,需要根据实际情况修改
// UnoCSS 图标在使用中需要指定图标集的名字,所以可以去掉
iconSet[symbol.id.replace('du-icon-', '')] = `<svg viewBox="${
symbol.viewBox
}">${
isColorIcon
? symbol.content
: // 这里修改了纯色图标的 path 的 fill 属性,做到可以在 css 中使用 color 属性
symbol.content.replaceAll('<path', '<path fill="currentColor"')
}</svg>`;
});
writeFile(
join('./dist', `${setName}.json`),
JSON.stringify(iconSet, null, 2)
);
} catch (err) {
console.error(`Error reading iconfont.json in ${dir.name}:`, err);
}
}
} catch (err) {
console.error('Error reading icon directories:', err);
}
2.配置 uno.config.ts 来使用图标
ts
import {
presetIcons,
} from 'unocss';
// 我们使用了 monorepo 来管理项目,所以这么使用,只要能拿到这个 json 文件即可
import duiLinesIcon from '@some-package/unocss-icon/dist/dui-icon-lines-copy.json';
import duiColorIcon from '@some-package/unocss-icon/dist/dui-icon-color.json';
export default defineConfig({
...
presets: [
...
presetIcons({
collections: {
// 这里可以定义多个图标集,每个图标集可以有不同的前缀
dui: duiLinesIcon,
'dui-color': duiColorIcon,
},
customizations: {
// 这里可以自定义图标的默认大小
iconCustomizer(collection, icon, props) {
props.width = '1rem';
props.height = '1rem';
},
},
}),
],
});
在项目中使用的时候就很简单了,直接使用图标集的名字即可。线性图标能直接修改颜色
vue
<template>
<div class="i-dui:theright-line text-primary"></div>
</template>