Table of Contents

1、概述

前面一章,我们学习了往函数组件中传入参数,在自定义组件时,以获得更多的灵活性。这一章,我们还需要继续通过一个实践,来巩固学习。

本次实践主要呈现一下在商用环境之下,封装组件时的详细思考。因此,本章中会包含一些自定义组件中比较重要的技巧。

我们先来看一下完整的案例和代码

预览
avatar.tsx
app.tsx
utils.ts
1
import clsx from 'clsx'
2
import { twMerge } from 'tailwind-merge'
3
import { getImageUrl, UserInfo } from './utils'
4
5
interface AvatarProps extends React.ImgHTMLAttributes<HTMLImageElement> {
6
person: UserInfo,
7
size: number | string,
8
shape?: 'circle' | 'square',
9
className?: string
10
}
11
12
export default function Avatar(props: AvatarProps) {
13
const { person, size, shape = 'circle', className = '', ...others } = props
14
15
const cls = twMerge(clsx(
16
'border dark:border-0 border-gray-200 dark:inset-ring dark:inset-ring-white',
17
{
18
'rounded-full': shape === 'circle',
19
'rounded-xl': shape === 'square',
20
},
21
className
22
))
23
24
return (
25
<img
26
{...others}
27
className={cls}
28
src={getImageUrl(person)}
29
alt={person.name}
30
width={size}
31
height={size}
32
/>
33
)
34
}
35

2、继承原生组件的属性

由于我们的 Avatar 组件,是基于原生组件 img 封装的,因此,我们可以继承原生组件的属性,以确保 img 标签功能的完整性。

这里由于我们也不想花费过多的代码去实现原生组件的属性,因此,我们使用 ...other 来接收原生组件的属性。

首先是类型声明上继承

code.ts
1
interface AvatarProps extends React.ImgHTMLAttributes<HTMLImageElement> {
2
person: UserInfo,
3
size: number | string,
4
shape?: 'circle' | 'square',
5
className?: string
6
}

然后通过结构赋值,将原生组件的属性,赋值给 ...others,并通过展开 others 传入参数原样传递给原生组件。

avatar.tsx
1
export default function Avatar(props: AvatarProps) {
2
const { person, size, shape = 'circle', className = '', ...others } = props
3
4
// ...
5
6
return (
7
<img {...others} />
8
)
9
}

这样操作之后,我们就可以非常完整的把原生组件的属性,传递给 img 标签。

3、设置默认值

在开发中,如果我们约定某一些属性不是必传的,那么我们就可以借助 ES6 的语法,给这些属性设置一些默认值。

avatar.tsx
1
function Avatar(props: AvatarProps) {
2
const { size = 100, shape = 'circle', className = '', ...others } = props
3
4
// ...
5
}

4、className 的合并

通常情况下,我们无法实现约定好所有的样式,因此,我们依然要支持传入 className 来覆盖默认的样式。但是当传入 className 之后,我们依然要尽量去保留组件内部我们约定好的样式。所以,这里就会存在一个类名合并的需求

在开发中,我们通常会使用三方工具库 twMerge 来合并类名

avatar.tsx
1
import { twMerge } from 'tailwind-merge'
2
3
function Avatar(props: AvatarProps) {
4
const { person, size, shape = 'circle', className = '', ...others } = props
5
6
const cls = twMerge(
7
// 内部约定的样式
8
'border dark:border-0 border-gray-200 dark:inset-ring dark:inset-ring-white',
9
// 外部传入的样式
10
className
11
)
12
13
...
14
}

5、使用 clsx 有条件的合并类名

在开发中,我们通常会使用三方工具库 clsx 来有条件的合并类名

例如,我们在这个案例中,约定了当传入的 shapecircle 时,会自动添加 rounded-full 类名,使头像变成圆形。但是这里我们会额外进行一次判断,来决定是否添加 rounded-full 类名

code.ts
`border ${shape === 'circle' ? 'rounded-full' : 'rounded-xl'}`

这样的写法比较传统,但是当条件越来越多时,代码会变得非常难以维护。因此,我们使用 clsx 来有条件的合并类名,他的写法如下

code.ts
1
import clsx from 'clsx'
2
3
clsx(
4
'border dark:border-0 border-gray-200 dark:inset-ring dark:inset-ring-white',
5
{
6
'rounded-full': shape === 'circle',
7
'rounded-xl': shape === 'square',
8
},
9
className
10
)

clsx 会自动判断传入的类名是否为真,如果为真,则合并到一起,如果为假,则不合并。

clsx 可以非常方便的实现有条件的合并类名,并且代码的可读性也得到了提升。但是,在它合并完成之后,依然可能存在一些相同属性的重复类名。因此,我们还需要结合使用 twMerge 来合并类名以避免类名的重复。

avatar.tsx
1
import clsx from 'clsx'
2
import { twMerge } from 'tailwind-merge'
3
4
function Avatar(props: AvatarProps) {
5
const { person, size, shape = 'circle', className = '', ...others } = props
6
7
const cls = twMerge(clsx(
8
'border dark:border-0 border-gray-200 dark:inset-ring dark:inset-ring-white',
9
{
10
'rounded-full': shape === 'circle',
11
'rounded-xl': shape === 'square',
12
},
13
className
14
))
15
16
// ...
17
}

6、总结

最后我们来看一下完整代码和案例演示。本案例包含了类型声明、属性继承、默认值、类名合并、有条件的合并类名等技巧。这些技巧在实践中都是非常实用且使用频率非常高的,因此,我们可以自己动手实现一下,以加深印象。

预览
avatar.tsx
app.tsx
utils.ts
1
import clsx from 'clsx'
2
import { twMerge } from 'tailwind-merge'
3
import { getImageUrl, UserInfo } from './utils'
4
5
interface AvatarProps extends React.ImgHTMLAttributes<HTMLImageElement> {
6
person: UserInfo,
7
size: number | string,
8
shape?: 'circle' | 'square',
9
className?: string
10
}
11
12
export default function Avatar(props: AvatarProps) {
13
const { person, size, shape = 'circle', className = '', ...others } = props
14
15
const cls = twMerge(clsx(
16
'border dark:border-0 border-gray-200 dark:inset-ring dark:inset-ring-white',
17
{
18
'rounded-full': shape === 'circle',
19
'rounded-xl': shape === 'square',
20
},
21
className
22
))
23
24
return (
25
<img
26
{...others}
27
className={cls}
28
src={getImageUrl(person)}
29
alt={person.name}
30
width={size}
31
height={size}
32
/>
33
)
34
}
35
专栏首页
到顶
专栏目录