【前端基建】如何让组件库便于开发使用

今天我们来聊聊一个组件库怎么让开发用起来“爽”。

组件库是前端开发中必不可少的一个环节,不同的开发团队也会针对自己公司的业务场景来封装对应的组件库。接下来笔者会列举一些一个让开发用的比较爽的组件库(以React为例)要注意的点:

props设计

首先要注意的就是props的设计,一定要易于理解与直观,起名尽量简单与精确:

1
2
3
4
5
6
7
8
9
10
11
// good:
interface DialogProps{
open: boolean // control the state of the dialog
setOpen
}

// bad:
interface DialogProps{
isOpen: boolean
setIsOpen
}

避免二次透传样式

在组件开发之初应于UX团队高度协作定下每个组件的固定设计规范,并根据此规范来开发样式,避免因为样式与实际的规范不同导致的样式透传。样式的透传不仅会给开发带来额外的心智负担,更有可能会带来意料之外的bug。

1
2
3
4
5
// good:
<Dialog />

// bad:
<Dialog className={"bg-foreground-100"}/>

还有一种思路就是通过variance来实现,就拿button举例,可以在开发之初与UX团队定义好有哪些类型的button,提前做好封装,这里以class-variance-authority为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const button = cva(["font-semibold", "border", "rounded"], {
variants: {
intent: {
primary: [
"bg-blue-500",
"text-white",
"border-transparent",
"hover:bg-blue-600",
],
// **or**
// primary: "bg-blue-500 text-white border-transparent hover:bg-blue-600",
secondary: [
"bg-white",
"text-gray-800",
"border-gray-400",
"hover:bg-gray-100",
],
},
size: {
small: ["text-sm", "py-1", "px-2"],
medium: ["text-base", "py-2", "px-4"],
},
},
compoundVariants: [
{
intent: "primary",
size: "medium",
class: "uppercase",
// **or** if you're a React.js user, `className` may feel more consistent:
// className: "uppercase"
},
],
defaultVariants: {
intent: "primary",
size: "medium",
},
});

这样开发在使用的时候直接传不同的variance即可,无需再额外传递样式:

1
<Button variants={{intent: "secondary", size: "small"}}

避免隐式逻辑

假设有一个对象是这样的:

1
2
3
4
interface Item{
name: string
// ...other props
}

有一个组件是这么写的:

1
2
3
const Card = (props: { item: Item}) => {
return <div>{name}</div>
}

这种情况下其实就出现了隐式逻辑,这个组件的目的是把一个对象的名字给展示出来,但是要求的props是Item这个对象本身,然后取这个对象的name这个键的值。开发并不知道这个名字是怎么渲染出来的,只知道把item传进去就能渲染。如果之后有一个地方的一个对象name这个键并不是想用来渲染的,就要花时间排查逻辑,甚至对组件做修改。像这种场景就应该直接在props里面显式地暴露name出来。

避免过度封装

不要想着把所有的场景全都封装在一个大而全的组件。一个理想的组件只要能满足80%以上的业务需求就行了,剩下的20%只能是每个个案单独处理。这里介绍一下原子化的业务组件的优化范式:

1
2
3
4
5
6
7
8
9
// bad:
<Dialog header={} headerClassName={} body={} bodyClassName={} footer={} footerClassName={} open={} setOpen={} />

// good:
<Dialog>
<DialogHeader/>
<DialogBody/>
<DialogFooter/>
</Dialog>

易于持续集成

最理想情况下这个基础组件库应该什么第三方依赖都没有,但是受限于开发时间等等因素,这是不可能的。这时候应该尽量选择粒度更细的无头组件库进行二次封装,例如Radix UI,而非完整的设计系统组件库,例如Ant Design。

易于维护

组件库的代码应该尽量直观简洁,因为这是整个团队每个人之后都有可能要维护的东西。编码风格需要照顾到团队的平均技术水平,避免过分炫技的代码。在组件的代码中应该保留关键注释与发布日志,方便团队在后期维护的时候遇到这种问题:“这段逻辑为啥要改成这样啊?”

轻量化

代码量应该尽量少,保证80%的场景覆盖的基础能力即可。引用的三方库也应该尽可能少,打包出来的产物需要尽量小。

文档完善,例子多

所谓文档不是把组件的每个props列出来是干嘛的就可以,更应该注重与业务的实际关联。每个组件其实都有1-3个非常典型的应用场景,文档里面应当有完善的使用示例,最好就是实际的业务代码。比如对于像RadioInput这种组件,经常出现的场景肯定就是表单了,那么文档就可以画一个用到这两个组件的与业务相关联的表单来表达使用方式与对应代码,那下次开发遇到类似的需求可以直接复制粘贴代码使用。

类型定义完善

每个props都应该有完善的类型定义,避免使用anyunknown等不清晰的类型。同时应该把每个组件的类型定义单独暴露出来,方便开发使用做二次封装。

假设一个菜单的每一项是这样的:

1
2
3
4
interface MenuItem{
key: string
type: 'content' | 'divider'
}

那用户使用的时候就可以这样写一个钩子,可以把MenuItem这个定义引进来,这样写起来就舒服很多:

1
2
3
4
5
6
7
8
const useMenuOptions = (): MenuItem[] =>{
return [
{
key: 'test',
type: 'content'
}
]
}

高性能

组件库是整个项目的基础,所以性能一定要高。在发布前应该有完善的性能测试,包括动画是否卡顿,是否存在重复渲染,内存占用大小等。

不打架

比如一个组件库里面同时有Drawer和Dialog组件,那么这个时候就需要注意两者的z-index了,避免同时使用的时候出现预期之外的覆盖。

风格统一

假设在某个组件里面有这样的一个类型定义:

1
type item = [string, string] // 元组第0项是key,第1项是value

那么剩下所有组件类似的地方都应该遵循“元组第0项是key,第1项是value”的规范,如果某个组件反过来了,很容易造成bug并对开发带来心智负担。

暴露Ref

许多组件都是基于原生HTML元素的封装,比如input,那这个时候就可以把input的Ref给暴露出来,方便开发直接访问HTML元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={className}
ref={ref}
{...props}
/>
)
}
)