React自定义Button

半兽人 发表于: 2025-03-27   最后更新时间: 2025-03-31 11:28:31  
{{totalSubscript}} 订阅, 28 游览

背景

以下用 React 写的一个按钮组件,文件名是 Button.tsx(因为用了 TypeScript)。它的目标是做一个“万能按钮”,可以根据需要调整样式和功能,方便在项目里多次使用。就像你去买衣服,这个按钮可以让你挑颜色、尺码,还能加点装饰。

import type { CSSProperties } from 'react'
import React from 'react'
import { type VariantProps, cva } from 'class-variance-authority'
import Spinner from '../spinner'
import classNames from '@/utils/classnames'

const buttonVariants = cva(
    'btn disabled:btn-disabled',
    {
        variants: {
            variant: {
                'primary': 'btn-primary',
                'warning': 'btn-warning',
                'secondary': 'btn-secondary',
                'secondary-accent': 'btn-secondary-accent',
                'ghost': 'btn-ghost',
                'ghost-accent': 'btn-ghost-accent',
                'tertiary': 'btn-tertiary',
            },
            size: {
                small: 'btn-small',
                medium: 'btn-medium',
                large: 'btn-large',
            },
        },
        defaultVariants: {
            variant: 'secondary',
            size: 'medium',
        },
    },
)

export type ButtonProps = {
    destructive?: boolean
    loading?: boolean
    styleCss?: CSSProperties
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
    ({ className, variant, size, destructive, loading, styleCss, children, ...props }, ref) => {
        return (
            <button
                type='button'
                className={classNames(
                    buttonVariants({ variant, size, className }),
                    destructive && 'btn-destructive',
                )}
                ref={ref}
                style={styleCss}
                {...props}
            >
                {children}
                {loading && <Spinner loading={loading} className='!text-white !h-3 !w-3 !border-2 !ml-1' />}
            </button>
        )
    },
)
Button.displayName = 'Button'

export default Button
export { Button, buttonVariants }

下面是代码的逐步拆解:


1. 导入部分

import type { CSSProperties } from 'react'
import React from 'react'
import { type VariantProps, cva } from 'class-variance-authority'
import Spinner from '../spinner'
import classNames from '@/utils/classnames'
  • 简单解释:这部分就像你去超市买原料,准备做个按钮“大餐”。你需要:
    • CSSProperties:从 React 借来的“调色盘”,让你能直接写 CSS 样式。
    • React:做按钮的主材料,没它啥也干不了。
    • cvaVariantProps:从 class-variance-authority 这个“样式搭配机”里拿来的工具,帮你快速组合按钮样式。
    • Spinner:一个“加载中”小动画,像是按钮的“工作指示灯”。
    • classNames:一个“标签粘贴器”,帮你把各种样式标签贴到按钮上。
  • 作用:这些是工具和原料,后面会用到。

2. 定义按钮样式:buttonVariants

const buttonVariants = cva(
  'btn disabled:btn-disabled',
  {
    variants: {
      variant: {
        'primary': 'btn-primary',
        'warning': 'btn-warning',
        'secondary': 'btn-secondary',
        'secondary-accent': 'btn-secondary-accent',
        'ghost': 'btn-ghost',
        'ghost-accent': 'btn-ghost-accent',
        'tertiary': 'btn-tertiary',
      },
      size: {
        small: 'btn-small',
        medium: 'btn-medium',
        large: 'btn-large',
      },
    },
    defaultVariants: {
      variant: 'secondary',
      size: 'medium',
    },
  },
)
  • 简单解释:这就像你设计了一台“按钮样式机”。你告诉它:
    • 基础样式是 btn,如果按钮被禁用(disabled),就加个 btn-disabled(比如变灰)。
    • 有两种“调味料”:
      • variant:按钮的“颜色和风格”,比如 primary(主色)、warning(警告色)、ghost(透明风格)。
      • size:按钮的“大小”,有小号、中号、大号。
    • 如果你不选样式,默认是 secondary(次要风格)和 medium(中号)。
  • 作用:用 cva 这个工具生成一个函数,帮你根据输入的 variantsize 输出对应的 CSS 类名。
  • 例子:输入 variant: 'primary', size: 'large',它会输出 btn btn-primary btn-large

3. 定义按钮的属性:ButtonProps

export type ButtonProps = {
  destructive?: boolean
  loading?: boolean
  styleCss?: CSSProperties
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>
  • 简单解释:这就像你给按钮写了个“使用说明书”,告诉大家它能接受哪些“定制要求”:
    • destructive:可选,设为 true 就变成“危险按钮”(比如红色)。
    • loading:可选,设为 true 就显示“加载中”动画。
    • styleCss:可选,直接写 CSS 样式(比如 { color: 'red' })。
    • React.ButtonHTMLAttributes<HTMLButtonElement>:继承了 HTML <button> 的所有属性,比如 onClick(点击事件)、disabled(禁用状态)。
    • VariantProps<typeof buttonVariants>:从 buttonVariants 里拿来的 variantsize 选项。
  • 作用:明确这个按钮能接受哪些参数,方便 TypeScript 检查类型,也让开发者知道怎么用。

4. 核心组件:Button

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, destructive, loading, styleCss, children, ...props }, ref) => {
    return (
      <button
        type='button'
        className={classNames(
          buttonVariants({ variant, size, className }),
          destructive && 'btn-destructive',
        )}
        ref={ref}
        style={styleCss}
        {...props}
      >
        {children}
        {loading && <Spinner loading={loading} className='!text-white !h-3 !w-3 !border-2 !ml-1' />}
      </button>
    )
  },
)
Button.displayName = 'Button'
  • 简单解释:这是按钮的“生产线”,它把所有原料和要求组合起来,生产出一个成品按钮。
    • 输入:你传进来一堆参数(classNamevariantsize 等)和 ref(遥控器)。
    • 加工
      • buttonVariants 生成基础样式(比如 btn btn-primary)。
      • classNames 拼接额外样式:
        • 如果有 className,加进去。
        • 如果 destructivetrue,加个 btn-destructive
      • styleCss 作为内联样式。
      • 把其他属性(...props)贴到 <button> 上,比如 onClick
    • 内容
      • children:按钮里的文字或图标(比如“点击我”)。
      • 如果 loadingtrue,加个小 Spinner(加载动画)。
    • 输出:一个完整的 <button> 元素。
  • 为什么用 forwardRef:让外部能直接控制这个按钮(比如让它聚焦)。
  • displayName:给组件取个名字,方便调试时认出来。

5. 导出

export default Button
export { Button, buttonVariants }
  • 简单解释:这就像你把成品装箱,贴上标签,准备卖出去。
    • export default Button:默认卖的是 Button 组件。
    • export { Button, buttonVariants }:也提供单独的“零件”,让别人可以用 buttonVariants 自己改样式。
  • 作用:让其他文件能引入和使用这个组件。

代码怎么工作?

假设你在另一个文件里这么用:

<Button variant="primary" size="large" loading={true} onClick={() => alert('点了!')}>
  点击我
</Button>
  • 过程
    1. buttonVariants({ variant: 'primary', size: 'large' }) 输出 btn btn-primary btn-large
    2. classNames 拼接成 btn btn-primary btn-large(因为没 destructive,所以不加 btn-destructive)。
    3. 生成一个 <button>,类名是 btn btn-primary btn-large,里面有文字“点击我”和一个 Spinner
    4. 点击时弹出“点了!”的提示。
  • 结果:一个大号、主色调、带加载动画的按钮。

总结

这个 Button 组件是一个“可定制的按钮工厂”:

  • 样式:通过 variantsize 调整外观。
  • 功能:支持加载动画(loading)、危险样式(destructive)、自定义 CSS(styleCss)。
  • 灵活性:继承了 HTML 按钮的所有功能,还能用 ref 控制。

它就像一个“按钮模具”,你告诉它想要啥样,它就给你做出来,省得每次都从头写按钮的样式和逻辑。希望这个解释清楚了!如果还有疑问,比如想知道某部分具体怎么用,随时问我!

更新于 2025-03-31
在线,2小时前登录

查看React更多相关的文章或提一个关于React的问题,也可以与我们一起分享文章