使用 pnpm+vite+ts+tailwind 开发的 React 组件库, 采用 monorepo 组织,文档站使用 Docusaurus 构建
文档站在线地址:https://dance.cosine.ren/
Github 地址:https://github.com/dancing-team/dance-ui
NPM 包:https://www.npmjs.com/package/@dance-ui/ui
前言:笔者参加了字节青训营,但由于实习,对组内贡献不多,故想要做一整套的实现文档作为补充生态,如有写的可以改进的,不完备的地方,希望各位能够指出。
首先聊聊 Button 的功能,也就是我们要实现什么
- type 为 default 默认按钮,primary 表示主要按钮,link 表示无边框按钮,unstyle 表示不带任何样式的按钮(方便自己定制)
- danger属性代表带有警告意味的按钮
- ghost 属性代表幽灵按钮,适用于有背景的情况下,会将北京改为透明并且按钮反色。
- size 属性 预设3种按钮大小
- loading 属性 表示按钮加载中,禁用点击事件
- disabled 属性 表示按钮禁用中
相关的配置可以从如下表格中看到
属性 | 说明 | 类型 | 是否可选 | 默认值 |
---|---|---|---|---|
type | 按钮类型 | 'default' | 'primary' | 'link' |
size | 按钮大小 | 'large' | 'middle' | 'small' |
onClick | 点击事件 | () => void | 是 | - |
dander | 是否为危险按钮 | boolean | 是 | - |
ghost | 是否为幽灵按钮 | boolean | 是 | - |
loading | 是否加载中 | boolean | 是 | - |
className | 组件额外的 CSS className | string | 是 | - |
style | 组件额外的 CSS style | CSSProperties | 是 | - |
children | 子组件 | ReactNode | 是 | - |
iconClassName | Loading 图标 CSS className | string | 是 | - |
loadingIconProps | Loading图标参数 | LoadingProps | 是 | - |
再来聊聊具体的设计与实现
接口设计
整体接口设计是参照上方图标进行设计的。
export type ButtonProps = {
/** 按钮类型 */
type?: 'default' | 'primary' | 'link' | 'unstyle'
/** 按钮大小 */
size?: 'large' | 'middle' | 'small'
/** 点击事件 */
onClick?: () => void
/** 是否为危险按钮(红色警告) */
danger?: boolean
/** 是否为幽灵按钮 */
ghost?: boolean
/** 是否禁用 */
disabled?: boolean
/** 是否加载中 */
loading?: boolean
/** 组件额外的 CSS className */
className?: string
/** 组件额外的 CSS style */
style?: CSSProperties
/** 子组件 */
children?: ReactNode
/** Loading图标 CSS className */
iconClassName?: string
/** Loading图标参数 */
loadingIconProps?: LoadingProps
}
样式设计
样式设计中值得称道的是类型的设计,整个样式设计结构为:
- ButtonClass
- sizeClass
- large
- middle
- small
- typeClass
- large
- middle
- small
- ghostClass
- large
- middle
- small
- dangerClass
- large
- middle
- small
当然啦,如果你想要写 warnClass 也不难,可以在这个 ButtonClass 类中增加一个 warnClass,然后直接加配置就可,所以这样来写扩展性相对来说是很不错的。
看看代码吧:
const ButtonClass = {
sizeClass: {
large: 'py-2 px-5',
middle: 'py-1 px-4',
small: 'px-1',
},
typeClass: {
default: 'border-black bg-white text-black enabled:hover:border-dd-primary enabled:hover:text-dd-primary',
primary: 'border-dd-primary bg-dd-primary text-white enabled:hover:opacity-80',
link: 'border-transparent enabled:hover:text-dd-primary',
},
ghostClass: {
default: 'border-white text-white enabled:hover:border-dd-primary enabled:hover:text-dd-primary',
primary: 'border-dd-primary bg-transparent text-dd-primary enabled:hover:opacity-80',
link: 'border-transparent text-white enabled:hover:text-dd-primary',
},
dangerClass: {
default: 'border-dd-danger text-dd-danger enabled:hover:opacity-80',
primary: 'border-dd-danger bg-dd-danger text-white enabled:hover:opacity-80',
link: 'border-transparent text-dd-danger enabled:hover:opacity-80',
},
}
说完了样式,让我们来看看组件主要设计
const Button = forwardRef(function ButtonInner(
{
type,
size,
className,
onClick,
disabled,
danger,
ghost,
loading,
style,
children,
iconClassName,
loadingIconProps,
}: ButtonProps,
ref: LegacyRef<HTMLButtonElement>,
) {
const { sizeClass, typeClass, dangerClass, ghostClass } = ButtonClass
const _disabled = disabled || loading
const _chooseClass = useMemo(() => {
if ((danger && ghost) || danger) return dangerClass
else if (ghost) return ghostClass
else return typeClass
}, [danger, dangerClass, ghost, ghostClass, typeClass])
return (
<button
ref={ref}
className={
type === 'unstyle'
? className
: classNames(
'box-border border transition focus:outline-none',
sizeClass[size ?? 'middle'],
_chooseClass[type ?? 'default'],
_disabled ? 'disabled:cursor-not-allowed disabled:opacity-60' : 'cursor-pointer',
className,
)
}
style={style}
onClick={_disabled ? undefined : onClick}
disabled={_disabled}>
<Loading show={loading} className={classNames('mr-2', iconClassName)} {...loadingIconProps} />
{children}
</button>
)
})
整体来说 core 还是前面说的接口设计,然后可以 className 可以使用 classnames 进行组合。
可以看到我们提供了一个 ref,来供用户操纵 Button 的 ref。
这边还需要设置一些初始值:
Button.defaultProps = {
type: 'default',
size: 'middle',
loading: false,
disabled: false,
}
那么这就是本节的全部内容了,下面我会把全部代码贴上。
import classNames from 'classnames'
import { CSSProperties, forwardRef, LegacyRef, ReactNode, useMemo } from 'react'
import Loading, { LoadingProps } from '../Loading'
export type ButtonProps = {
/** 按钮类型 */
type?: 'default' | 'primary' | 'link' | 'unstyle'
/** 按钮大小 */
size?: 'large' | 'middle' | 'small'
/** 点击事件 */
onClick?: () => void
/** 是否为危险按钮(红色警告) */
danger?: boolean
/** 是否为幽灵按钮 */
ghost?: boolean
/** 是否禁用 */
disabled?: boolean
/** 是否加载中 */
loading?: boolean
/** 组件额外的 CSS className */
className?: string
/** 组件额外的 CSS style */
style?: CSSProperties
/** 子组件 */
children?: ReactNode
/** Loading图标 CSS className */
iconClassName?: string
/** Loading图标参数 */
loadingIconProps?: LoadingProps
}
const ButtonClass = {
sizeClass: {
large: 'py-2 px-5',
middle: 'py-1 px-4',
small: 'px-1',
},
typeClass: {
default: 'border-black bg-white text-black enabled:hover:border-dd-primary enabled:hover:text-dd-primary',
primary: 'border-dd-primary bg-dd-primary text-white enabled:hover:opacity-80',
link: 'border-transparent enabled:hover:text-dd-primary',
},
ghostClass: {
default: 'border-white text-white enabled:hover:border-dd-primary enabled:hover:text-dd-primary',
primary: 'border-dd-primary bg-transparent text-dd-primary enabled:hover:opacity-80',
link: 'border-transparent text-white enabled:hover:text-dd-primary',
},
dangerClass: {
default: 'border-dd-danger text-dd-danger enabled:hover:opacity-80',
primary: 'border-dd-danger bg-dd-danger text-white enabled:hover:opacity-80',
link: 'border-transparent text-dd-danger enabled:hover:opacity-80',
},
}
const Button = forwardRef(function ButtonInner(
{
type,
size,
className,
onClick,
disabled,
danger,
ghost,
loading,
style,
children,
iconClassName,
loadingIconProps,
}: ButtonProps,
ref: LegacyRef<HTMLButtonElement>,
) {
const { sizeClass, typeClass, dangerClass, ghostClass } = ButtonClass
const _disabled = disabled || loading
const _chooseClass = useMemo(() => {
if ((danger && ghost) || danger) return dangerClass
else if (ghost) return ghostClass
else return typeClass
}, [danger, dangerClass, ghost, ghostClass, typeClass])
return (
<button
ref={ref}
className={
type === 'unstyle'
? className
: classNames(
'box-border border transition focus:outline-none',
sizeClass[size ?? 'middle'],
_chooseClass[type ?? 'default'],
_disabled ? 'disabled:cursor-not-allowed disabled:opacity-60' : 'cursor-pointer',
className,
)
}
style={style}
onClick={_disabled ? undefined : onClick}
disabled={_disabled}>
<Loading show={loading} className={classNames('mr-2', iconClassName)} {...loadingIconProps} />
{children}
</button>
)
})
Button.defaultProps = {
type: 'default',
size: 'middle',
loading: false,
disabled: false,
}
export default Button