1、前言
我们在网页开发中,无时无刻不在与 icon 打交道, 那么怎么样的实现方式才是最优的呢? 之前在开发中本着"省事"的目的,一直将就,开发过程中每次遇到icon的问题都比较头疼, 尤其是在icon频繁导出,代码重复编写上,着实是一件难忍的事。 实际上"省事"并不"省事", 相反非常头痛, 最近进行项目重构,于是着手将代码中icon的使用进行梳理, 重新进行替换
2、问题与诉求
本人在工作多年,一直使用icon的途中, 遇到过很多问题, 记录如下
- 1、 icon 用图片png 还是 svg
- 2、 图片用网络加载还是本地加载,然后打包到项目中
- 3、 我想要体积更小,使用最方便
- 我想要网络只加载一次
- 我想打包体积更小
- 4、 svg 在项目中的使用过程中, 如何优雅的使用
- 我只想引入一次
- 需要满足hover支持变色
- 满足传入颜色、背景色
- 可以通过一行代码搞定、包括父元素hover 也能让icon变色
3、解释
先简单解释一下上面的问题
1、 对于第一个问题: 图片和svg其实各有优势, svg是 矢量图,有不失真的优势, 是靠线条画出来的, 对于简单的侧重线条类的 icon 建议使用 svg, svg是一段代码,体积非常小, 但是对于颜色丰富、色彩绚丽,可能 图片 尤其是 png 格式的图片更加合适, 实际开发中,我们为了保证导出的图片不失真,通常导出 2倍图, 通过css控制实际宽高低于导出的宽高, 那么导致的问题就是图片体积又非常的大。 那么其实对于公司通用的icon来说,一般都是通过figma或者其他平台直接导出, 对于icon来说,可以和设计师一起商量, 设计出大小一致, 尺寸和常用色一致的 svg icon
2、对于第二个问题,其实也是比较取舍的, icon 通过网络加载,就会占用网络带宽, 如果用本地打包资源就会使打包后的体积增大, 通常会根据具体情况进行综合考虑。一些项目可能选择混合使用,将一些核心的、不经常变更的图片打包到项目中,而将一些可能经常更新或较大的图片通过网络加载
4、 方案
4-1、 react 中 svg icon 的使用 与 问题
js
import svgr from "vite-plugin-svgr";
//...
plugins: [react(), svgr(), splitVendorChunkPlugin(), visualizer()],
//...
通常我们在react中加载svg 是需要在vite.config 中配置一下vite-plugin-svgr 使用时, 导入svg 像导入组件一样
js
import { ReactComponent as IconCountry } from "./country.svg";
// use
<IconCountry className="icon"></IconCountry>
我们可以在icon中通过类的样式进行更改icon的大小, 但是对于icon的边框颜色和填充色,通常就不是那么容易了, 我们可能需要写出以下的代码
css
.icon {
width: 10px;
height:10px;
& > svg {
width: 40px;
height: 40px;
}
& > rect {
fill: #5051ff;
}
& > path {
stroke: #fff;
}
}
像代码写的,如果改svg的颜色,通过样式改通常要将里面的各种路径元素颜色都要更改,非常的不优雅, 这还是没有添加 hover , 添加 hover, 又要对内部 元素全部添加一遍, hover 变色可能还可以通过改样式解决, 那么active, 选中态呢? 增加更多逻辑交互后, 通常我们只能再多导入一个 变色后的icon,才能让代码没有那么恶心
4-2、 使用 react-svg-sprite-generator 解决方案
其实解决上面的问题, 思路就是我们需要通过工具先将所有的svg打包成精灵图, 进行压缩, 去掉本身的边框颜色和fill的颜色, 然后我们可以通过svg <use xlinkHref={
{spriteUrl}#{name}} />
的方式,再项目中找到对应的资源中的icon,并进行设置。 再npm中寻找这个工具的时候恰好找到了这个包, 地址 这个包的使用非常的简单
js
npm i react-svg-sprite-generator -D
npx svgsprite --src ./icons --dest ./src/icons
可以写到package.json 中 每次 npm run svg
即可
只需要安装后, 在目录下的终端执行一下命令,即可打包出来一个 sprite.svg 的精灵图, 那么使用就很简单了
js
<svg
style={{
width: size + "px",
height: size + "px",
}}
strokeWidth={strokeWidth}
stroke={strokeColor}
>
<use xlinkHref={`${spriteUrl}#${写入想展示的icon的名字(大写的)}`} />
</svg>
使用后发现颜色和大小都可以灵活控制了, 而且相比原始的 svg 文件, 打包出来的进行了压缩与处理, 原始的svg文件其实就不再需要了, 当然代码中不使用, 也不会打包到dist目录中, 那么build后的体积其实大幅降低了
那么为了更方便使用, 我们再进一步进行封装, 代码如下
js
import * as IconNames from "./names";
import spriteUrl from "./sprite.svg";
import { useEffect, useState } from "react";
const SvgIcon = ({
name,
color = "#222222",
hoverColor = "",
size = 16,
strokeWidth = 3,
classNames,
fill,
}: {
name: keyof typeof IconNames;
size?: number;
color?: string;
hoverColor?: string;
strokeWidth?: number;
classNames?: any;
fill?: string;
}) => {
const [strokeColor, setStrokeColor] = useState(color);
const onMouseHover = () => {
if (hoverColor) {
setStrokeColor(hoverColor);
}
};
const onMouseLeave = () => {
if (hoverColor) {
setStrokeColor(color);
}
};
useEffect(() => {
setStrokeColor(color);
}, [color]);
return (
<svg
style={{
width: size + "px",
height: size + "px",
}}
strokeWidth={strokeWidth}
stroke={strokeColor}
fill={fill || "none"}
onMouseOver={onMouseHover}
onMouseLeave={onMouseLeave}
className={classNames}
>
<use xlinkHref={`${spriteUrl}#${name}`} />
</svg>
);
};
SvgIcon.IconNames = IconNames;
export default SvgIcon;
使用
js
<SvgIcon name={IconNames.PRODUCT} color="#f00" hoverColor="#ff0" size={24} />
使用起来就非常简单了, 我们可以方便的控制颜色和尺寸了, 但是我们上面讲过, 能不能实现一行代码就实现样式的 hover, 在组件里 不进行 更新hover状态也能改变颜色呢 , 答案也是可以的, 那么就需要我们使用 tailwind CSS 通过 className的方式传入了
js
<div className="group bg-red-100 hover:bg-red-300">
<div className="group-hover:text-[#0f0]">111</div>
<SvgIcon name={IconNames.PRODUCT} classNames="group-hover:stroke-[yellow] group-hover:fill-[#f00]" size={24} />
</div>
如图: 就是我们通过 tailwindcss 的方式, 实现了hover到父元素,同时控制子元素中的文本和icon变色的例子, 到此,我们就基本实现了我们的需求, 代码也非常的简洁
5、总结
本文通过总结工作中遇到的icon使用的问题, 进行一步步分析拆解, 最终通过使用 npm 中的一个 svg 包,生成了压缩的合并svg的精灵图, 通过svg 的use 使用, 进一步封装成了组件, 然后通过使用 tailwind css 的方式,实现了一行代码即可灵活控制icon的hover变色、更改大小、边框等等需求, 在减少代码build体积、提高开发速度等方面成效显著, 可以说是一劳永逸