前端的icon实现对比,强大的iconify+unocss解决方案All in

3,080 阅读8分钟

Icon在web应用中的作用

图标,是信息的图像表达方式。在设计页面的时候使用icon有很多优点,icon可以将某个功能进行直观的视觉表达,减少用户的认知成本。大脑处理图片的速度比处理文字的快60000倍。

总结下来,icon的作用主要是视觉信息化和增强交互体验

1. 视觉信息化:

  • 行为引导

  • 特征标识

  • 状态标识

2. 增强交互体验:

动态图标,为页面添加动感

使用Icon的几种基本实现方式

1. 图片图标

  1. 直接使用img标签加载图片资源-png|jpg|jpeg|svg|gif.......

<img src="https://cdn-icons-png.flaticon.com/512/1581/1581942.png" />

  1. 通过css设置元素的背景图,可灵活调整位置等,例如:抖音

<style>
	.icon {
		background: url("https://lf3-cdn-tos.bytegoofy.com/obj/goofy/ies/douyin_web/media/nav_light_entry_optimize_v3.249d142a09878f8c.png");
		width: 24px;
		height: 24px;
		background-repeat: no-repeat;
		background-size: 1056px auto;
	}
	.icon-1 {
		background-position: 0px 0px;
	}
	.icon-2 {
		background-position: -24px 0px;
	}
	.icon-3 {
		background-position: -48px 0px;
	}
</style>
<div style="display: flex; gap: 10px;">
	<div class="icon icon-1"></div>
	<div class="icon icon-2"></div>
	<div class="icon icon-3"></div>
</div>

3. 通过css的background-image和mask-image的实现,可以参考Antfu的聊聊纯CSS图标

<style>
	.icon {
		background-color: currentColor;
		width: 20px;
		height: 20px;
		mask-image: url("http://sj8ohpa5l.hd-bkt.clouddn.com/star.png?e=1725371564&token=FamrxkTXFOJ6bzL__LD8nDA7n5SQwS3EBXgF-lRK:Up0ML8cFV35lEkOgBmv9EuGlBb8=");
		mask-size: 100%;
		mask-position: 0 0;
	}
</style>
<div style="color: pink;" class="icon"></div>

2. 字体图标

2.1. 原理

  1. 将图标转为字体文件,字体文件中含有unicode和图标轮廓信息的映射关系
  2. 在html导入字体文件
  3. 设置元素的字体属性
  4. 使用unicode编码获取图标进行渲染

2.2. 使用

以iconfont为例iconfont使用,早期的字体图标不支持多色图标,后期iconfont通过COLR字体文件类型得以支持

  1. 直接使用unicode编码
<style>
	@font-face {
		font-family: 'iconfont';
		/* Project id 4635479 */
		src: url('//at.alicdn.com/t/c/font_4635479_tjy5p6jrx4n.woff2?t=1722148062923') format('woff2'),
			url('//at.alicdn.com/t/c/font_4635479_tjy5p6jrx4n.woff?t=1722148062923') format('woff'),
			url('//at.alicdn.com/t/c/font_4635479_tjy5p6jrx4n.ttf?t=1722148062923') format('truetype'),
			url('//at.alicdn.com/t/c/font_4635479_tjy5p6jrx4n.svg?t=1722148062923#iconfont') format('svg');
	}

	.iconfont {
		font-family: "iconfont"!important;
		font-size: 16px;
		font-style: normal;
		-webkit-font-smoothing: antialiased;
		-moz-osx-font-smoothing: grayscale;
	}
</style>
<!-- 使用对应的字体类,输入图表对应的unicode编码 -->
<i class="iconfont">&#xe600;</i>

2. 编写icon-class封装,将unicode通过伪元素和css类名进行映射,更好识别和记忆


<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
  @font-face {
    font-family: "iconfont";
    /* Project id 4635508 */
    src: url('//at.alicdn.com/t/c/font_4635508_hl4msbmuno.woff2?t=1724578708140') format('woff2'),
      url('//at.alicdn.com/t/c/font_4635508_hl4msbmuno.woff?t=1724578708140') format('woff'),
      url('//at.alicdn.com/t/c/font_4635508_hl4msbmuno.ttf?t=1724578708140') format('truetype');
  }

  .iconfont {
    font-family: "iconfont" !important;
    font-size: 16px;
    font-style: normal;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }

  .icon-tag_car:before {
    content: "\e601";
  }

  .icon-shouye:before {
    content: "\e60f";
  }
</style>

<i class="iconfont icon-shouye"></i>

3. SVG图标

直接通过SVG标签插入HTML中,通过width,height等修改大小,通过fill修改颜色(通过使用fill="currentColor"来继承父元素字体颜色,方便在外层修改),注意viewBox需要设置合理

<svg width="50px" height="50px" viewBox="0 0 50 50">
    <circle cx="25" cy="25" r="25" fill="currentColor" />
</svg>

4. 不同实现的对比

  1. img标签和css的background-image加载时机的区别

  1. 使用mask-image可实现动画+大小+颜色的灵活修改,另外mask-image的功能强大,可以通过mask-image实现很多的动画效果,另外bilibili的弹幕防遮挡的效果,也是通过这个属性,配合语义分割算法生成大量的遮罩图片来实现

    1. 通过一个div实现遮罩动画
<style>
@keyframes mask {
	0% {
		-webkit-mask-position: 0px 0px;
	}

	25% {
		-webkit-mask-position: 619px 0px;
	}

	50% {
		-webkit-mask-position: 0px 0px;
	}

	75% {
		-webkit-mask-position: 308px 0px;
		-webkit-mask-size: 100%;
	}

	100% {
		-webkit-mask-size: 1000%;
	}
}

.mask {
	width: 700px;
	height: 392px;
	background: black url("http://sj8ohpa5l.hd-bkt.clouddn.com/bg.jpg?e=1725371507&token=FamrxkTXFOJ6bzL__LD8nDA7n5SQwS3EBXgF-lRK:rcG47nomrr54ZBtqA0N4jDeivAw=");
	-webkit-mask-image: url("http://sj8ohpa5l.hd-bkt.clouddn.com/mask.jpg?e=1725371554&token=FamrxkTXFOJ6bzL__LD8nDA7n5SQwS3EBXgF-lRK:07auJhtQ13tElcygkJWAt5f1CbE=");
	animation: mask 5s linear infinite forwards;
}
</style>
<div class="mask"> </div>
// 实现效果:http://sj8ohpa5l.hd-bkt.clouddn.com/mask.webp?e=1725371581&token=FamrxkTXFOJ6bzL__LD8nDA7n5SQwS3EBXgF-lRK:q6QXkjvWgztRaaSTyWJyYFUylXg=

2. 通过mask-image和语义分割实现弹幕防遮挡的原理,参考地址

框架中的使用(目前仅介绍React)

1. 项目维护svg文件,封装SVGIcon组件

  1. 基于svgr+svgo自行封装React-SVGIcon组件或者使用使用Antd的icon组件
import type { DashboardTreeItemType } from '@/types/finds/dashboard';
import classNames from 'classnames';
import React, { CSSProperties } from 'react';
import { ReactComponent as AreaSvg } from '../imgs/area.svg';
import { ReactComponent as CarSvg } from '../imgs/car.svg';
import { ReactComponent as OrgSvg } from '../imgs/org.svg';
import { ReactComponent as StaffSvg } from '../imgs/staff.svg';

interface ICellIcon {
	name: DashboardTreeItemType;
	className?: string;
	style?: CSSProperties;
	size?: number;
	color?: string;
}

const IconMapping = {
	AREA: AreaSvg,
	ORG: OrgSvg,
	CAR: CarSvg,
	STAFF: StaffSvg,
};

const CellIcon: React.FC<ICellIcon> = ({ name, className, style, size, color }) => {
	const SVGIconComponent = IconMapping[name];
	return (
		<SVGIconComponent
			className={classNames([size ? `w-[${size}px] h-[${size}px]` : 'w-[15px] h-[15px]', className])}
			color={color}
			style={style}
		/>;
	)
};

	export default CellIcon;

2. SVG sprite

2.1. SVG sprite 插件

  1. 配置SVG sprite 插件,配置文件存放路径和图标的命名规范等
  2. 在main.js中注册import 'virtual:svg-icons-register'
  3. 在组建中使用svg-use使用组件
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import svgr from "vite-plugin-svgr";
import { resolve } from 'path';
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'

// https://vitejs.dev/config/
export default defineConfig({

  resolve: {
    alias: {
      '@': resolve(__dirname, './src'),
      '@assets': resolve(__dirname, './src/assets'),
    }
  },
  plugins: [
    react(),
    createSvgIconsPlugin({
      iconDirs: [resolve(process.cwd(), 'src/assets/icons')],
      symbolId: 'icon-[dir]-[name]',
    }),
    svgr(),
  ],
})

//main.js文件
import 'virtual:svg-icons-register'

//组件
<svg color="pink">
  <use href={`#icon-react`}/>
</svg>

2.2. 基于第三方图标库,比如阿里iconfont进行图标管理

  1. 直接将iconfont中的symbol的js链接下载(iconfont提供链接不可靠,建议下载本地)
  2. 然后直接使用script标签加载js文件后直接在组件中使用svg-use指向对应的id即可

3. Antd+iconfont

  1. 使用antd组件然后使用添加上述iconfont提供的symbol形式链接
  2. 这种方式已经可以很好地做到多项目的图标共享和管理,但是antd的icon目前还是比较有限,并且强依赖iconfont,iconfont提供的js文件并不稳定
import React from 'react';
import { createFromIconfontCN } from '@ant-design/icons';
import { Space } from 'antd';

const IconFont = createFromIconfontCN({
	scriptUrl: [
		'//at.alicdn.com/t/font_1788044_0dwu4guekcwr.js', // icon-javascript, icon-java, icon-shoppingcart (overridden)
		'//at.alicdn.com/t/font_1788592_a5xf2bdic3u.js', // icon-shoppingcart, icon-python
	],
});

const App: React.FC = () => (
	<Space>
		<IconFont type="icon-javascript" />
		<IconFont type="icon-java" />
		<IconFont type="icon-shoppingcart" />
		<IconFont type="icon-python" />
	</Space>
);

export default App;

4. Iconify(推荐)

iconify.design/

Iconify的核心是消费json:将svg转为json方便传输,然后客户端组件解析json转为svg进行消费

4.1. 优势

  1. 100+个图标集,200W+个图标,预设图标丰富
  2. 同一个库支持多种前端框架,支持原声WebComponent,React,Vue...
  3. 内置工具丰富,可以将本地图标进行深度清洗,比svgo的压缩清洗程度更高
  4. 可私有部署,添加私有源
  5. 可结合UnoCSS预设图标插件,直接使用css图标,具体请参考。这对不支持SVG标签的容器是非常友好的,比如微信小程序(⭐⭐⭐肥肠推荐!!!)

4.2. 不足

目前多项目之间共享自定义图标还不够丝滑,但是可以通过私有部署后,建立一套私有的图标库,目前iconify开源的node服务api非常友好,搭建成本并不是很高。

4.3. 使用

  1. 通过icons插图标
  2. 查到图标复制内容
  3. 建议安装vscode插件@iconify-json/vscode-icons

以下均以React为例进行演示

4.3.1. React直接使用Iconify提供的组件
// terminal
yarn add --dev @iconify/react
// 业务组件
import { Icon } from '@iconify/react';
<Icon icon="mdi-light:home" />

icon使用规则:"@数据源:数据集:图标名",参考

注意,此中使用方式首次请问图标会通过http请求先下载icon对应的json,然后存到localStorage中,后续访问页面走缓存

4.3.2. 私有部署

允许icon源自多个服务,只要能够提供json即可,iconify提供了可私有部署的node服务github.com/iconify/api.

import { addAPIProvider, Icon } from '@iconify/react';

addAPIProvider('local', {
	// Array of host names
	resources: ['http://localhost:3000'],
});

// Demo using provider in icon name
export function renderHomeIcon() {
	return <Icon icon="@local:material-icons:home" />;
}

通过iconify提供的api,进行二开,完成简单的本地图标上传私有服务

本地代码

import {
    importDirectorySync,
    cleanupSVG,
    runSVGO,
    parseColors,
    isEmptyColor,
} from '@iconify/tools';
import axios from 'axios';
import path from 'path';


console.log("__dirname", process.cwd())

// Import icons
const iconSet = importDirectorySync(path.resolve(process.cwd(), 'src/assets/icons'), {
    prefix: 'iconSetName',
});

// Validate, clean up, fix palette and optimise
iconSet.forEachSync((name, type) => {
    if (type !== 'icon') {
        return;
    }

    const svg = iconSet.toSVG(name);
    if (!svg) {
        // Invalid icon
        iconSet.remove(name);
        return;
    }

    // Clean up and optimise icons
    try {
        // Clean up icon code
        cleanupSVG(svg);

        // Assume icon is monotone: replace color with currentColor, add if missing
        // If icon is not monotone, remove this code
        parseColors(svg, {
            defaultColor: 'currentColor',
            callback: (attr, colorStr, color) => {
                return !color || isEmptyColor(color)
                    ? colorStr
                    : 'currentColor';
            },
        });

        // Optimise
        runSVGO(svg);
    } catch (err) {
        // Invalid icon
        console.error(`Error parsing ${name}:`, err);
        iconSet.remove(name);
        return;
    }

    // Update icon
    iconSet.fromSVG(name, svg);
});

axios.post("http://127.0.0.1:3000/upload-icon-set", {
    icons: iconSet.export()
}).then(res => {
    console.log("success", res)
}).catch(err => {
    console.log("upload-error", err)
})

node端代码

import fs from 'fs';
import path from 'path';
import { triggerIconSetsUpdate } from '../../data/icon-sets.js';

const ICONSETDIR = 'icons'
export function handleUpload(req, res, callback) {
    const {icons} = req.body;
    if (!icons) return res.status(400).send('icons 数据缺失');
    const { prefix } = req.body.icons;
    if (!prefix) return res.status(400).send('prefix 数据缺失');
    const cwd = process.cwd();
    const iconSetDir = `${cwd}/${ICONSETDIR}`;

    const filePath = `${iconSetDir}/${prefix}.json`;
    
    if (!fs.existsSync(iconSetDir)) {
        fs.mkdirSync('icons');
    }
    if (fs.existsSync(iconSetDir) && !fs.existsSync(filePath)) {
        try {
            fs.writeFileSync(filePath, JSON.stringify(icons), 'utf8');
        } catch (error) {
            console.log('white error', error);
            res.status(400).send('iconSet 创建失败')
        }
				// iconify node内置的更新方法
        triggerIconSetsUpdate((update) => {
            if (update) {
                res.send('update');
            } else {
                res.send('nothing update');
            }
        });
    }
    else {
        res.status(400).send('iconSet 已存在');
    }
}
4.3.3. 结合UnoCSS使用
  1. 下载图标,预设配置,以及iconify工具包
pnpm add -D @unocss/preset-icons @iconify/json @iconify/utils

2. 配置uno.config.js

import { defineConfig, presetIcons, presetUno } from 'unocss';
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders';
import {
	SVG,
	cleanupSVG,
	runSVGO,
	deOptimisePaths,
} from '@iconify/tools';


export default defineConfig({
	presets: [
		presetUno(),
		presetIcons({
			warn: true,
			prefix: ['i-'],
			collections: {
				// 配置本地图标,结合iconify的API,进行清洗,压缩,格式化
				"custom": new FileSystemIconLoader(
					'./src/assets/icons',
					(svg) => {
						// 构造iconify中的SVG对象
						const svgObj = new SVG(svg);
						// 清洗
						cleanupSVG(svgObj);
						// svgo清洗
						runSVGO(svgObj);
						// Update paths for compatibility with old software
						deOptimisePaths(svg);
						// 删除标签空白间隙
						const newContent = svgObj.toMinifiedString();
						// 格式化,统一设置大小颜色
						return newContent.replace(/(<svg.*?width=)"(.*?)"/, '$1"1em"')
							.replace(/(<svg.*?height=)"(.*?)"/, '$1"1em"')
							.replace(/fill=".*?"/, 'fill="currentColor"')
					}
				),
				// 配置私有化部署的资源
				"remote": async (iconName) => {
					return await fetch("http://127.0.0.1:3000/iconSetName/"+iconName+".svg").then(res => res.text())
				}

			},
			extraProperties: {
				display: 'inline-block',
				// -0.125em用于和同行文字居中对齐
				'vertical-align': '-0.125em',
			},
		})
	]
})

3. 在main中导入unocss

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
import 'virtual:uno.css'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

4. 业务代码使用

function App() {
  return (
    <>
      Welcome to use unocss iconPresets <div className="i-custom-notice" />
    </>
  )
}

export default App

4. 效果

可以看出,unocss的icon是通过姜svg转为base64,然后通过background-color时延图标颜色,mask-image描绘图标轮廓,可以说是纯css方案

4.4. 小程序的unocss使用

可参考该社区解决方案+本文的unocss图标预设配置,在taro中使用


总结

  1. 图片图标(jpg,png等)的管理方式不够优雅,切换颜色需要换图。非矢量,在当前的移动端开发中其实不再是很好的选择,往往需要通过多张不同倍率的图片进行分辨率的适配
  2. 字体图标其实在使用使用上还是比较方便的,但是管理和更新相对比较繁琐,往往依赖第三方平台。且动画实现艰难,无法按需映入
  3. SVG已然成为目前Icon的首选,矢量图形,灵活的大小形状变换,复杂动画效果实现。
    1. SVG sprite在单项目维护使用体验良好,一次性插入SVG至HTML,可在多处通过svg-use重用。但是需要自己维护一个基础的SVG组件
    2. Antd+iconfont的图标管理方式,可以做到自定义组件多项目共享,使用体验良好,但是需要借助第三方平台
    3. Iconify的多数据源图标,满足了开发者的基础需求,且可以通过自定义本地图标,自己维护图标数据源。也可以达到自定义图标多项目共享,并且搭配unocss可以很好地解决小程序不支持SVG的问题,可以将h5项目,通过taro+unocss快速迁移至小程序。