likes
comments
collection
share

【从0-1实现一个组件库(2)】实现一个 Button

作者站长头像
站长
· 阅读数 14

使用 pnpm+vite+ts+tailwind 开发的 React 组件库, 采用 monorepo 组织,文档站使用 Docusaurus 构建

文档站在线地址:dance.cosine.ren/

Github 地址:github.com/dancing-tea…

NPM 包:www.npmjs.com/package/@da…

前言:笔者参加了字节青训营,但由于实习,对组内贡献不多,故想要做一整套的实现文档作为补充生态,如有写的可以改进的,不完备的地方,希望各位能够指出。

首先聊聊 Button 的功能,也就是我们要实现什么

  1. type 为 default 默认按钮,primary 表示主要按钮,link 表示无边框按钮,unstyle 表示不带任何样式的按钮(方便自己定制)

【从0-1实现一个组件库(2)】实现一个 Button

  1. danger属性代表带有警告意味的按钮

【从0-1实现一个组件库(2)】实现一个 Button

  1. ghost 属性代表幽灵按钮,适用于有背景的情况下,会将北京改为透明并且按钮反色。

【从0-1实现一个组件库(2)】实现一个 Button

  1. size 属性 预设3种按钮大小

【从0-1实现一个组件库(2)】实现一个 Button

  1. loading 属性 表示按钮加载中,禁用点击事件

【从0-1实现一个组件库(2)】实现一个 Button

  1. disabled 属性 表示按钮禁用中

【从0-1实现一个组件库(2)】实现一个 Button

相关的配置可以从如下表格中看到

属性说明类型是否可选默认值
type按钮类型'default''primary''link''unstyle'default
size按钮大小'large''middle''small'middle
onClick点击事件() => void-
dander是否为危险按钮boolean-
ghost是否为幽灵按钮boolean-
loading是否加载中boolean-
className组件额外的 CSS classNamestring-
style组件额外的 CSS styleCSSProperties-
children子组件ReactNode-
iconClassNameLoading 图标 CSS classNamestring-
loadingIconPropsLoading图标参数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
转载自:https://juejin.cn/post/7205092873326247973
评论
请登录