• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

Icon 图标组件是非常常用的组件。

它用起来非常简单,只要复制图标的组件名,直接渲染即可:

它也有一些 props:

spin 是让图标不断转圈:

rotate 是指定图标旋转角度:

antd 内置了很多图标组件,如果觉得不够用,还可以自己扩展:

import React from "react";
import Icon from "@ant-design/icons";
import type { GetProps } from "antd";

type CustomIconComponentProps = GetProps<typeof Icon>;

const HeartSvg = () => (
    <svg width="1em" height="1em" fill="currentColor" viewBox="0 0 1024 1024">
        <path d="M923 283.6c-13.4-31.1-32.6-58.9-56.9-82.8-24.3-23.8-52.5-42.4-84-55.5-32.5-13.5-66.9-20.3-102.4-20.3-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5-24.4 23.9-43.5 51.7-56.9 82.8-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3 0.1-35.3-7-69.6-20.9-101.9z" />
    </svg>
);

const HeartIcon = (props: Partial<CustomIconComponentProps>) => (
    <Icon component={HeartSvg} {...props} />
);

const App: React.FC = () => <HeartIcon style={{ color: "pink" }} />;

export default App;

只要对 Icon 组件包一层,component 参数传入图标的 svg,那就是一个新的图标组件。

而且如果你的项目用了 iconfont,你也可以把 iconfont 图标封装成 Icon 组件:

import React from "react";
import { createFromIconfontCN } from "@ant-design/icons";
import { Space } from "antd";

const IconFont = createFromIconfontCN({
    scriptUrl: "//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js",
});

const App: React.FC = () => (
    <Space>
        <IconFont type="icon-tuichu" />
        <IconFont type="icon-facebook" />
        <IconFont type="icon-twitter" />
    </Space>
);

export default App;

用 createFromIconfontCN 的方法,传入 scriptUrl,就可以直接用 IconFont 的组建了。

Icon 组件就这么多用法,还是挺简单的。

接下来我们自己实现一下:

npx create-react-app --template=typescript icon-component

用 cra 创建个项目。

添加 Icon/index.tsx

import React, { PropsWithChildren, forwardRef } from "react";

type BaseIconProps = {
    className?: string;
    style?: React.CSSProperties;
    size?: string | string[];
    spin?: boolean;
};

export type IconProps = BaseIconProps &
    Omit<React.SVGAttributes<SVGElement>, keyof BaseIconProps>;

export const Icon = forwardRef<SVGSVGElement, PropsWithChildren<IconProps>>(
    (props, ref) => {
        const {
            style,
            className,
            spin,
            size = "1em",
            children,
            ...rest
        } = props;

        return (
            <svg ref={ref} style={style} fill="currentColor" {...rest}>
                {children}
            </svg>
        );
    }
);

这里有 style、className、spin、size、children 这些参数,都很好理解。

此外,因为 Icon 就是对 svg 的封装,所以我们也接受所有 svg 的属性,透传给内部的 svg。

这里还用了 forwardRef 来把 svg 的 ref 转发出去:

还有,size 默认为 1em 也就是用 font-size 的大小:

填充颜色用 currentColor,也就是 color 的值:

这就是为什么我们能通过 font-size 和 color 来修改 Icon 组件的大小和颜色。

然后处理下 size 参数:

size 可以传 [10px, 10px] 分别指定宽高,也可以传 10px 来同时指定宽高,所以要做下处理。

import React, { PropsWithChildren, forwardRef } from "react";

type BaseIconProps = {
    className?: string;
    style?: React.CSSProperties;
    size?: string | string[];
    spin?: boolean;
};

export type IconProps = BaseIconProps &
    Omit<React.SVGAttributes<SVGElement>, keyof BaseIconProps>;

export const getSize = (size: IconProps["size"]) => {
    if (Array.isArray(size) && size.length === 2) {
        return size as string[];
    }

    const width = (size as string) || "1em";
    const height = (size as string) || "1em";

    return [width, height];
};

export const Icon = forwardRef<SVGSVGElement, PropsWithChildren<IconProps>>(
    (props, ref) => {
        const {
            style,
            className,
            spin,
            size = "1em",
            children,
            ...rest
        } = props;

        const [width, height] = getSize(size);

        return (
            <svg
                ref={ref}
                style={style}
                width={width}
                height={height}
                fill="currentColor"
                {...rest}>
                {children}
            </svg>
        );
    }
);

然后再处理下 className。

安装用到的包:

npm install --save classnames

const cn = cs(
    "icon",
    {
        "icon-spin": spin,
    },
    className
);

实现下 icon-spin 的样式:

安装 sass 包:

npm install --save-dev sass

添加 Icon/index.scss

@keyframes spin {
    0% {
        transform: rotate(0deg);
    }

    100% {
        transform: rotate(360deg);
    }
}

.icon {
    display: inline-block;
}

.icon-spin {
    animation: spin 1s linear infinite;
}

icon 设置为 inline-block,也就是行内元素但是可以设置宽高。

icon-spin 执行无限旋转动画。

在 Icon/index.tsx 里引入:

至此,Icon 组件就封装完了。

这就完了么?

没错,Icon 组件就这么简单。

然后创建 Icon/createIcon.tsx

import React, { forwardRef } from "react";
import { Icon, IconProps } from ".";

interface CreateIconOptions {
    content: React.ReactNode;
    iconProps?: IconProps;
    viewBox?: string;
}

export function createIcon(options: CreateIconOptions) {
    const { content, iconProps = {}, viewBox = "0 0 1024 1024" } = options;

    return forwardRef<SVGSVGElement, IconProps>((props, ref) => {
        return (
            <Icon ref={ref} viewBox={viewBox} {...iconProps} {...props}>
                {content}
            </Icon>
        );
    });
}

它是用来创建 Icon 组件的,接收 svg 的内容,也可以设置一些 IconProps、fill 颜色等。

接下来用它创建几个 Icon 组件试试:

Icon/icons/IconAdd.tsx

import { createIcon } from "../createIcon";

export const IconAdd = createIcon({
    content: (
        <>
            <path d="M853.333333 480H544V170.666667c0-17.066667-14.933333-32-32-32s-32 14.933333-32 32v309.333333H170.666667c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h309.333333V853.333333c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V544H853.333333c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32z"></path>
        </>
    ),
});

Icon/icons/IconEmail.tsx

import { createIcon } from "../createIcon";

export const IconEmail = createIcon({
    content: (
        <>
            <path d="M874.666667 181.333333H149.333333c-40.533333 0-74.666667 34.133333-74.666666 74.666667v512c0 40.533333 34.133333 74.666667 74.666666 74.666667h725.333334c40.533333 0 74.666667-34.133333 74.666666-74.666667V256c0-40.533333-34.133333-74.666667-74.666666-74.666667z m-725.333334 64h725.333334c6.4 0 10.666667 4.266667 10.666666 10.666667v25.6L512 516.266667l-373.333333-234.666667V256c0-6.4 4.266667-10.666667 10.666666-10.666667z m725.333334 533.333334H149.333333c-6.4 0-10.666667-4.266667-10.666666-10.666667V356.266667l356.266666 224c4.266667 4.266667 10.666667 4.266667 17.066667 4.266666s12.8-2.133333 17.066667-4.266666l356.266666-224V768c0 6.4-4.266667 10.666667-10.666666 10.666667z"></path>
        </>
    ),
});

在 App.tsx 里引入下试试:

import { IconAdd } from "./Icon/icons/IconAdd";
import { IconEmail } from "./Icon/icons/IconEmail";

function App() {
    return (
        <div style={{ padding: "50px" }}>
            <IconAdd></IconAdd>
            <IconEmail></IconEmail>
        </div>
    );
}

export default App;

跑下开发服务:

npm run start

可以看到,Icon 渲染出来了:

然后试下 props:

import { IconAdd } from "./Icon/icons/IconAdd";
import { IconEmail } from "./Icon/icons/IconEmail";

function App() {
    return (
        <div style={{ padding: "50px" }}>
            <IconAdd size="40px"></IconAdd>
            <IconEmail spin></IconEmail>
            <IconEmail style={{ color: "blue", fontSize: "50px" }}></IconEmail>
        </div>
    );
}

export default App;

没啥问题。

接下来支持下 iconfont 的图标:

创建 Icon/createFrontIconfont.tsx

import React from "react";
import { Icon, IconProps } from "./";

const loadedSet = new Set<string>();

export function createFromIconfont(scriptUrl: string) {
    if (
        typeof scriptUrl === "string" &&
        scriptUrl.length &&
        !loadedSet.has(scriptUrl)
    ) {
        const script = document.createElement("script");
        script.setAttribute("src", scriptUrl);
        script.setAttribute("data-namespace", scriptUrl);
        document.body.appendChild(script);

        loadedSet.add(scriptUrl);
    }

    const Iconfont = React.forwardRef<SVGSVGElement, IconProps>(
        (props, ref) => {
            const { type, ...rest } = props;

            return (
                <Icon {...rest} ref={ref}>
                    {type ? <use xlinkHref={`#${type}`} /> : null}
                </Icon>
            );
        }
    );

    return Iconfont;
}

createFromIconfont 会传入 scriptUrl,我们在 document.body 上添加 <script> 标签引入它。

当然,如果加载过的就不用再次加载了,所以用 Set 来记录下。

然后用的时候使用 <use xlinkHref="#type" > 引用。

antd 的就是这么做的:

我们测试下:

登录 iconfont.cn

选几个图标:

添加到购物车,创建个项目:

然后就可以看到在线 js 链接:

我们在项目里引入下试试:

import { createFromIconfont } from "./Icon/createFrontIconfont";
import { IconAdd } from "./Icon/icons/IconAdd";
import { IconEmail } from "./Icon/icons/IconEmail";

const IconFont = createFromIconfont(
    "//at.alicdn.com/t/c/font_4443338_a2wwqhorbk4.js"
);

function App() {
    return (
        <div>
            <div style={{ padding: "50px" }}>
                <IconFont type="icon-shouye-zhihui" size="40px"></IconFont>
                <IconFont
                    type="icon-gerenzhongxin-zhihui"
                    fill="blue"
                    size="40px"></IconFont>
            </div>
        </div>
    );
}

export default App;

引入成功!

至此,我们的 Icon 组件就开发完成了。

案例代码上传了小册仓库。

总结

这节我们实现了 Icon 组件。

支持 size、spin、className、style 等参数。

然后实现了 createIcon 方法,可以传入 svg 内容来生成具体的 Icon。

并且对 iconfont 做了支持,实现了 createIconFromIconfont,可以传入 scriptUrl,然后指定 type 来引用对应的 icon。

通过把 svg 的 fill 设置为 currentColor,把 width、height 设置为 1em, 实现了可以通过 color 和 font-size 来设置 Icon 大小和颜色的效果。

这就是我们每天在用的 Icon 组件的实现原理。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
12.深入理解Suspense和ErrorBoundary
Next
14.组件实战:Space间距组件