一 概要
1.1 背景
最近在项目中使用了很多从iconfont拿到的图标。使用官网的导入方法有些繁琐,也不易管理。于是捣鼓了一下...
1.2 目的
- 能够像组件一样使用,具有规范性。比如暴露一个type属性,根据不同的type使用不同的主题色。
- 高自由度。可以直接在项目中管理图标,只需要处理从其他图标网站拿到svg即可。
- 提供一个页面总览全部图标,方便使用。
1.3 实现效果
-
图标组件
import { BillIcon } from '@/components/icons'; <BillIcon type="primary" size={16} badge={false}/>
-
Js调用(基于前者,如果图标需要根据某种条件来选用,这种方式很推荐)
import { getIcon } from '@/utils'; <div> { getIcon('bill', { type:'primary', size:16, badge:false }) } </div>
-
图标总览(不重要,无所谓)
项目使用的是webpack打包,所以在配置文件中新增了一个入口和出口,让图标库跟项目绑定,项目用到了什么,就显示什么。如图所示,我直接通过icon.html
来访问,这在调试项目的时候非常好用。
1.4 实现步骤
-
从免费网站中拿到svg。比如我在iconfont拿
-
(重点)把svg封装成组件:每个svg对应一个
tsx/jsx
文件,存储在项目的目录中(比如我存储在src/components/icons
中) -
创建一个入口文件,导出所有的图标组件。同时创建一个字典(map),让每一个图标对应一个key,比如
bill
对应的是<BillIcon/>
(控制图标和key的命名会让后续管理更方便,比如我这里的key取的是图标名称前面第一个单词,同时小写。)
二 图标组件封装与管理
2.1 了解svg的属性
在使用图标的时候,我们实际上只关注它的颜色和大小(我们不设计图标,我们只是图标的搬运工)。
下面是复制过来的一段svg代码,我们只需要关注它的width
、height
和里面path标签的fill
属性(颜色)。
<svg
t="1720764743617"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4480"
width="16"
height="16"
>
<path
d="M853.333333 938.666667H170.666667a42.666667 42.666667 0 0 1-42.666667-42.666667V128a42.666667 42.666667 0 0 1 42.666667-42.666667h682.666666a42.666667 42.666667 0 0 1 42.666667 42.666667v768a42.666667 42.666667 0 0 1-42.666667 42.666667zM341.333333 384v85.333333h341.333334V384H341.333333z m0 170.666667v85.333333h341.333334v-85.333333H341.333333z"
p-id="4481"
fill=""
></path>
</svg>
其中
t
属性和class
属性在jsx/tsx
中会显示有问题,删掉即可。
2.2 封装图标组件
现在问题来了,如果直接在svg外层包一层,意味着我们需要传入width
、height
和fill
的值,这在使用中不方便。我只想传一个type
和size
, 希望内部直接帮我处理成对应的值赋值给svg。
size
比较好处理,而fill
接收的是一个颜色,svg是不可能识别我们定义的type
的。行,那改写一下:
// BackIcon.tsx
type TIconType = 'primary' | 'disabled' | 'white' | 'danger'
const BackIcon = (props: TIconProps) => {
const getColor = (type: TIconType) => {
switch (type) {
case 'primary':
return '#13227a';
case 'white':
return '#ffffff';
case 'danger':
return '#ef6b6b';
default:
return '#8f8f8f';
}
};
const color = getColor(props.type);
return (
<svg
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="2674"
width={props.size}
height={props.size}
>
<path
d="M672 896c-8.533333 0-17.066667-2.133333-21.333333-8.533333l-362.666667-352c-6.4-6.4-10.666667-14.933333-10.666667-23.466667 0-8.533333 4.266667-17.066667 10.666667-23.466667L652.8 136.533333c12.8-12.8 32-12.8 44.8 0s12.8 32 0 44.8L356.266667 512l339.2 328.533333c12.8 12.8 12.8 32 0 44.8-6.4 8.533333-14.933333 10.666667-23.466667 10.666667z"
fill={color}
p-id="2675"
></path>
</svg>
);
};
那问题又来了,我不想在每个图标组件都加这么一串又臭又长的代码呀。没问题!只需要在中间再包一层就好了!
// withIconColor.tsx
// 图标类型
type TIconType = 'primary' | 'disabled' | 'white' | 'danger'
// 图标组件属性
interface IconProps {
type?: TIconType;
size?: number;
badge?: boolean; // 扩展的,给图标右上角加红点,它跟svg无关
}
const getColor = (type: TIconType) => {
switch (type) {
case 'primary':
return '#13227a';
case 'white':
return '#ffffff';
case 'danger':
return '#ef6b6b';
default:
return '#8f8f8f';
}
};
// 参数是一个组件,作用是处理在外层传递进来的props,转化成一定内容后,再还给原组件
function withIconColor(WrappedIcon: React.ComponentType<any>) {
return function (props: IconProps) {
const {
type,
size,
badge = false,
...others
} = props;
const color = getColor(props.type);
return (
<span {...others}>
{badge && (
<div className="absolute w-2 h-2 text-[4px] text-white bg-red-500 rounded-full right-0"></div>
)}
<WrappedIcon color={color} size={size || 24} />
</span>
);
};
}
export default withIconColor;
怎么使用呢?
import withIconColor from './withIconColor';
type TIconProps = {
color: string,
size: number
}
const BackIcon = (props: TIconProps) => {
return (
<svg
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="2674"
width={props.size}
height={props.size}
>
<path
d="M672 896c-8.533333 0-17.066667-2.133333-21.333333-8.533333l-362.666667-352c-6.4-6.4-10.666667-14.933333-10.666667-23.466667 0-8.533333 4.266667-17.066667 10.666667-23.466667L652.8 136.533333c12.8-12.8 32-12.8 44.8 0s12.8 32 0 44.8L356.266667 512l339.2 328.533333c12.8 12.8 12.8 32 0 44.8-6.4 8.533333-14.933333 10.666667-23.466667 10.666667z"
fill={props.color}
p-id="2675"
></path>
</svg>
);
};
export default withIconColor(BackIcon); // 在这里用!
此后,如果还要加什么图标,就创建一个文件,把上面的内容复制一份,换个命名,把中间的svg替换掉就好了!(当然还要把svg原来的width、height和fill属性换成动态传进来的。)
2.3 整体结构
结构因人而异,这里列下我的项目的结构
- src
- components
- icons
- BillIcon.tsx
- UserIcon.tsx
- index.ts
- withIconColor.tsx
- utils
- getIcon
-
组件入口
// src\components\icons\index.ts type IconKey = "bill" | "user" // 图标的key import BillIcon from './BillIcon' import UserIcon from './UserIcon' const keyToIconMap = { bill: BillIcon, user: UserIcon, } const ICON_KEYS = Object.keys(keyToIconMap) as Array<IconKey> export { BillIcon, UserIcon, keyToIconMap, ICON_KEYS }
之后就能在其他地方使用了
import { BillIcon } from '@/components/icons'; <BillIcon />
-
getIcon
import { IconKey, IconProps } from '@/types'; import { keyToIconMap } from '@/components/icons' export default (key: IconKey, props?: IconProps) => { const Icon = keyToIconMap[key]; return <Icon {...props} />; };
之后就能在其他地方使用了
import { getIcon } from '@/utils'; <div> { getIcon('bill', { type:'primary'}) } </div>
三 图标总览页面
在根目录加个入口(其他地方也行)
- src
- iconIndex.tsx
这一步也没那么重要了,有需要就copy吧,样式根据自己需求自定义了。
import './style/index.css';
import { createRoot } from 'react-dom/client';
import { getIcon } from '@/utils';
import { ICON_KEYS } from '@/components/icons';
import toast, { Toaster } from 'react-hot-toast';
import { RadioButton } from './components';
import { useState } from 'react';
const root = createRoot(document.getElementById('root')); // 因为我webpack定义的模板html有个root,所以这样写,因人而异了
function capitalizeFirstLetter(string: string) {
if (!string) return string;
return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();
}
const IconPage = () => {
const handleCopy = async (value) => {
await navigator.clipboard.writeText(value);
toast.success(`复制成功: ${value}`);
};
const [type, setType] = useState('js');
const handleChange = (value) => {
setType(value);
};
return (
<div className="p-2 font-mono">
<Toaster />
<div className="my-2">
<RadioButton
value={type}
onChange={handleChange}
options={[
{ value: 'component', label: '使用组件' },
{ value: 'js', label: '使用js' },
]}
/>
<div className="flex justify-center space-x-2">
{type === 'component' ? (
<div className="p-2 border bg-slate-900 text-slate-50 min-w-[430px]">
{`import { BillIcon } from '@/components/icons'; `}
<br />
<br />
{` <BillIcon type="primary" size={16}/> `}
</div>
) : (
<div className="p-2 border bg-slate-900 text-slate-50 w-fit min-w-[430px]">
{`import { getIcon } from '@/utils'; `}
<br />
<br />
{` <div> { getIcon('bill', { type:'primary', size: 16 }) } </div> `}
</div>
)}
</div>
</div>
<div className="grid-cols-2 lg:grid-cols-8 grid gap-2 ">
{ICON_KEYS.map((key) => {
const value =
type === 'component'
? `${capitalizeFirstLetter(key)}Icon`
: key;
return (
<div
className="rounded-sm flex justify-center cursor-pointer items-center pt-4 shadow-sm flex-col hover:scale-105 transition duration-300 ease-out hover:bg-indigo-100"
onClick={handleCopy.bind(null, value)}
>
{getIcon(key, {
type: 'primary',
})}
<span className="text select-none">{value}</span>
</div>
);
})}
</div>
</div>
);
};
root.render(<IconPage />);
配置webpack
{
// entry: path.ENTRY, // 之前单入口是这样写的
entry: {
main: path.ENTRY,
icon: path.ICON_ENTRY // path.resolve(__dirname, 'src', 'iconIndex.tsx')
},
// ...
plugins: [
new HtmlWebpackPlugin({
template: path.TEMPLATE, // path.resolve(__dirname, 'public', 'index.html'),
filename: 'index.html', // 输出文件名
chunks: ['main'], // 对应entry里边定义的
}),
new HtmlWebpackPlugin({
template: path.TEMPLATE,
filename: 'icon.html', // 输出文件名
chunks: ['icon'], // 对应entry里边定义的
}),
],
}
标签:return,const,svg,React,import,type,图标
From: https://www.cnblogs.com/sanhuamao/p/18298475