【从0-1实现组件系列(1)】实现一个 Tag 组件

lxf2023-04-14 09:13:01

我正在参加「AdminJS·启航计划」~

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

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

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

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

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

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

本篇文章我们将来实现一个简易版的 Tag,它包含了如下功能:

  1. 默认标签直接使用
import { Tag } from '@dance-ui/ui'

<Tag>标签1</Tag>

【从0-1实现组件系列(1)】实现一个 Tag 组件

  1. 给 Tag 增加颜色
import { Tag } from '@dance-ui/ui'

<Tag color="red">标签2</Tag>
<Tag color="blue">标签3</Tag>
<Tag color="green">标签4</Tag>
<Tag color="pink">标签5</Tag>

【从0-1实现组件系列(1)】实现一个 Tag 组件

  1. 设置可关闭的 Tag
<Tag color="pink" closable={true}>
    可关闭标签
  </Tag>
<Tag color="green" closable={true}>
    可关闭标签
</Tag>

【从0-1实现组件系列(1)】实现一个 Tag 组件

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

属性说明类型是否可选默认值
children填入的子元素ReactNode-
onClose点击关闭按钮时的回调函数() => void-
closable是否有关闭按钮booleanfalse
color背景颜色string-

再来聊聊具体的设计与实现

接口设计

首先,我们设计这个组件,需要暴露给使用者一些属性与配置。

最简单的需要考虑到tag包裹的子元素,也就是我们说的 children ;

再上一层呢,我们需要让用户有自己调配颜色的能力,也就是我们说的 color;

然后呢,我们还需要考虑到一个场景,就是用户可以添加和删减 tag 的,我们可以暴露一个 closable 属性,让用户去操作;

那用户删除的时候想要同时做一些事情怎么办呢,这个时候就可以给用户暴露一个回调函数,让用户删除的时候调用。

综上,我们暴露的接口长这样:

export type TagProps = {
  /* 子元素 */
  children?: string
  /* 关闭调用函数 */
  onClose?: () => void
  /* 是否可以关闭 */
  closable?: boolean
  /* 用户传入的颜色 */
  color?: string
}

样式设计

组内选用的是 TailWind,这个主要是出于几个方面的考量:

  1. 纯 css 的优点是直接,缺点是多,体积大。
  2. 预编译css语言(less sass stylus) 优点主要就是变量和嵌套。缺点主要有几个:a. 没写好的话会导致selector过长,导致渲染问题和性能问题 b.全局样式冲突
  3. css module 优点就是解决全局样式冲突 缺点是hash太长了,也会有很多重复的规则,类型校验基本等于没有,而且需要打包工具+编译工具配合使用
  4. css in js 主要是通过js runtime插入样式,实现主题化比较容易,也解决了全局样式冲突。并且产物没有任何css代码,利于组件库做分发,类型检查也可以。缺点就是不强制具名,排查问题及其痛苦,此外有runtime运行时开销,频繁css变动会导致可预知的性能劣化
  5. atomic css 原子化css,每条规则对应一个类名,可以直接通过拼接类名做到样式应用,如 tailwindcss。从定义就可以看出,编译产物是和项目大小无关的,会趋于一个固定值,也不会有全局样式冲突,缺点是tree shaking强制依赖静态分析,字符串拼接直接gg;此外有一些学习成本,比如需要明确自己不会在同一个 class 上写同属性的多个类名(例如 p-8 p-16)

由于 tailwind 有些学习成本,所以我会建议你想要写样式的时候直接查文档,边查边写也不慢。

在 css 上,我们使用 defaultStyle 和 colorStyle,把设置颜色样式和未设置颜色的样式做一个区分;

额外提一嘴,两者并不是 if else 的关系。

// 先来看看默认状态的 css
const defaultStyle =
  `inline-block rounded border border-solid
   border-slate-200 py-0 px-2 text-xs
   whitespace-nowrap bg-[#fafafa] h-6 leading-6`

defaultStyle 把基本的样式给勾勒出来了,接下来就是如果用户设置了 color 属性,我们就会增加 colorStyle 这么一个样式。

const colorStyle = 'border-transparent text-white'

在其中,我们使用 classnames 把两者给勾连起来。

className={classnames(defaultStyle, color ? colorStyle : '')} style={{ backgroundColor: color }}

代码编写

聊完了基本的“借口设计”和样式设计,就可以来看看主体代码了。

const Tag: React.FC<TagProps> = function TagInner({ children, onClose, closable, color }: React.PropsWithChildren<TagProps>) {
  const tag: LegacyRef<HTMLDivElement> | undefined = createRef()
  const handleClose: () => void = () => {
    if (onClose) onClose()
    if (tag.current?.style) tag.current.style.display = 'none'
  }
  return (
    <div className={classnames(defaultStyle, color ? colorStyle : '')} style={{ backgroundColor: color }} ref={tag}>
      {children}
      {closable && (
        <span className="text-black-50 ml-2 cursor-pointer" onClick={handleClose}>
          x
        </span>
      )}
    </div>
  )
}

可以看到我们提供了一个 ref,来供用户操纵 Tag 的 ref。

这边还需要设置一个初始值:

Tag.defaultProps = {
  /* 是否可以关闭 */
  closable: false,
}

那么这就是本节的全部内容了,下面我会把全部代码贴上。

import classnames from 'classnames'
import React, { createRef, LegacyRef } from 'react'

/**
 * 标签组件
 * @param {closable} boolean 是否可关闭
 * @param {onClose} func 标签关闭时的回调
 * @param {color} string 标签的颜色,不设置则为默认颜色
 */

export type TagProps = {
  /* 子元素 */
  children?: string
  /* 关闭 */
  onClose?: () => void
  /* 是否可以关闭 */
  closable?: boolean
  /* 用户传入的颜色 */
  color?: string
}

const defaultStyle =
  'inline-block rounded border border-solid border-slate-200 py-0 px-2 text-xs whitespace-nowrap bg-[#fafafa] h-6 leading-6'
const colorStyle = 'border-transparent text-white'

const Tag: React.FC<TagProps> = function TagInner({ children, onClose, closable, color }: React.PropsWithChildren<TagProps>) {
  const tag: LegacyRef<HTMLDivElement> | undefined = createRef()
  const handleClose: () => void = () => {
    if (onClose) onClose()
    if (tag.current?.style) tag.current.style.display = 'none'
  }
  return (
    <div className={classnames(defaultStyle, color ? colorStyle : '')} style={{ backgroundColor: color }} ref={tag}>
      {children}
      {closable && (
        <span className="text-black-50 ml-2 cursor-pointer" onClick={handleClose}>
          x
        </span>
      )}
    </div>
  )
}
Tag.defaultProps = {
  /* 是否可以关闭 */
  closable: false,
}

export default Tag