背景
以下用 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
:做按钮的主材料,没它啥也干不了。cva
和VariantProps
:从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
这个工具生成一个函数,帮你根据输入的variant
和size
输出对应的 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
里拿来的variant
和size
选项。
- 作用:明确这个按钮能接受哪些参数,方便 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'
- 简单解释:这是按钮的“生产线”,它把所有原料和要求组合起来,生产出一个成品按钮。
- 输入:你传进来一堆参数(
className
、variant
、size
等)和ref
(遥控器)。 - 加工:
- 用
buttonVariants
生成基础样式(比如btn btn-primary
)。 - 用
classNames
拼接额外样式:- 如果有
className
,加进去。 - 如果
destructive
是true
,加个btn-destructive
。
- 如果有
- 加
styleCss
作为内联样式。 - 把其他属性(
...props
)贴到<button>
上,比如onClick
。
- 用
- 内容:
children
:按钮里的文字或图标(比如“点击我”)。- 如果
loading
是true
,加个小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>
- 过程:
buttonVariants({ variant: 'primary', size: 'large' })
输出btn btn-primary btn-large
。classNames
拼接成btn btn-primary btn-large
(因为没destructive
,所以不加btn-destructive
)。- 生成一个
<button>
,类名是btn btn-primary btn-large
,里面有文字“点击我”和一个Spinner
。 - 点击时弹出“点了!”的提示。
- 结果:一个大号、主色调、带加载动画的按钮。
总结
这个 Button
组件是一个“可定制的按钮工厂”:
- 样式:通过
variant
和size
调整外观。 - 功能:支持加载动画(
loading
)、危险样式(destructive
)、自定义 CSS(styleCss
)。 - 灵活性:继承了 HTML 按钮的所有功能,还能用
ref
控制。
它就像一个“按钮模具”,你告诉它想要啥样,它就给你做出来,省得每次都从头写按钮的样式和逻辑。希望这个解释清楚了!如果还有疑问,比如想知道某部分具体怎么用,随时问我!