使用 UnoCSS 替换 icon-font,优化图标使用

背景

在有一定历史的项目中,使用 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>
相关推荐
卸任18 小时前
Vibe coding工具使用心得
前端·ai编程
brien919 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance19 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M19 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写19 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李19 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易20 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马20 小时前
前沿视界
前端
kyriewen20 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易20 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员