组件库技术栈(React + TS + TailWind)
组件库团队文档地址 https://github.com/dancing-team/dance-ui/ 欢迎各位老爷 Star
前言:笔者参加了字节青训营,但由于实习,对组内贡献不多,故想要做一整套的实现文档作为补充生态,如有写的可以改进的,不完备的地方,希望各位能够指出。
首先聊聊 Tag 的功能,也就是我们要实现什么
本篇文章我们将来实现一个简易版的 Tag,它包含了如下功能:
- 默认标签直接使用
import { Tag } from '@dance-ui/ui'
<Tag>标签1</Tag>
- 给 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>
- 设置可关闭的 Tag
<Tag color="pink" closable={true}>
可关闭标签
</Tag>
<Tag color="green" closable={true}>
可关闭标签
</Tag>
相关的配置可以从如下的表格中看到
属性 | 说明 | 类型 | 是否可选 | 默认值 |
children | 填入的子元素 | ReactNode | 是 | - |
onClose | 点击关闭按钮时的回调函数 | () => void | 是 | - |
closable | 是否有关闭按钮 | boolean | 是 | false |
color | 背景颜色 | string | 是 | - |
再来聊聊具体的设计与实现
接口设计
首先,我们设计这个组件,需要暴露给使用者一些属性与配置。
最简单的需要考虑到tag包裹的子元素,也就是我们说的 children ;
再上一层呢,我们需要让用户有自己调配颜色的能力,也就是我们说的 color;
然后呢,我们还需要考虑到一个场景,就是用户可以添加和删减 tag 的,我们可以暴露一个 closable 属性,让用户去操作;
那用户删除的时候想要同时做一些事情怎么办呢,这个时候就可以给用户暴露一个回调函数,让用户删除的时候调用。
综上,我们暴露的接口长这样:
export type TagProps = {
/* 子元素 */
children?: string
/* 关闭调用函数 */
onClose?: () => void
/* 是否可以关闭 */
closable?: boolean
/* 用户传入的颜色 */
color?: string
}
样式设计
组内选用的是 TailWind,这个主要是出于几个方面的考量:
- 纯 css 的优点是直接,缺点是多,体积大。
- 预编译css语言(less sass stylus) 优点主要就是变量和嵌套。缺点主要有几个:a. 没写好的话会导致selector过长,导致渲染问题和性能问题 b.全局样式冲突
- css module 优点就是解决全局样式冲突 缺点是hash太长了,也会有很多重复的规则,类型校验基本等于没有,而且需要打包工具+编译工具配合使用
- css in js 主要是通过js runtime插入样式,实现主题化比较容易,也解决了全局样式冲突。并且产物没有任何css代码,利于组件库做分发,类型检查也可以。缺点就是不强制具名,排查问题及其痛苦,此外有runtime运行时开销,频繁css变动会导致可预知的性能劣化
- 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