前面一章,我们学习了往函数组件中传入参数,在自定义组件时,以获得更多的灵活性。这一章,我们还需要继续通过一个实践,来巩固学习。
本次实践主要呈现一下在商用环境之下,封装组件时的详细思考。因此,本章中会包含一些自定义组件中比较重要的技巧。
我们先来看一下完整的案例和代码
10import clsx from 'clsx'20import { twMerge } from 'tailwind-merge'30import { getImageUrl, UserInfo } from './utils'4050interface AvatarProps extends React.ImgHTMLAttributes<HTMLImageElement> {60person: UserInfo,70size: number | string,80shape?: 'circle' | 'square',90className?: string10}1112export default function Avatar(props: AvatarProps) {13const { person, size, shape = 'circle', className = '', ...others } = props1415const 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},21className22))2324return (25<img26{...others}27className={cls}28src={getImageUrl(person)}29alt={person.name}30width={size}31height={size}32/>33)34}35
由于我们的 Avatar 组件,是基于原生组件 img
封装的,因此,我们可以继承原生组件的属性,以确保 img
标签功能的完整性。
这里由于我们也不想花费过多的代码去实现原生组件的属性,因此,我们使用 ...other
来接收原生组件的属性。
首先是类型声明上继承
1interface AvatarProps extends React.ImgHTMLAttributes<HTMLImageElement> {2person: UserInfo,3size: number | string,4shape?: 'circle' | 'square',5className?: string6}
然后通过结构赋值,将原生组件的属性,赋值给 ...others
,并通过展开 others
传入参数原样传递给原生组件。
1export default function Avatar(props: AvatarProps) {2const { person, size, shape = 'circle', className = '', ...others } = props34// ...56return (7<img {...others} />8)9}
这样操作之后,我们就可以非常完整的把原生组件的属性,传递给 img
标签。
在开发中,如果我们约定某一些属性不是必传的,那么我们就可以借助 ES6 的语法,给这些属性设置一些默认值。
1function Avatar(props: AvatarProps) {2const { size = 100, shape = 'circle', className = '', ...others } = props34// ...5}
通常情况下,我们无法实现约定好所有的样式,因此,我们依然要支持传入 className
来覆盖默认的样式。但是当传入 className 之后,我们依然要尽量去保留组件内部我们约定好的样式。所以,这里就会存在一个类名合并的需求
在开发中,我们通常会使用三方工具库 twMerge
来合并类名
10import { twMerge } from 'tailwind-merge'2030function Avatar(props: AvatarProps) {40const { person, size, shape = 'circle', className = '', ...others } = props5060const cls = twMerge(70// 内部约定的样式80'border dark:border-0 border-gray-200 dark:inset-ring dark:inset-ring-white',90// 外部传入的样式10className11)1213...14}
clsx
有条件的合并类名在开发中,我们通常会使用三方工具库 clsx
来有条件的合并类名
例如,我们在这个案例中,约定了当传入的 shape
为 circle
时,会自动添加 rounded-full
类名,使头像变成圆形。但是这里我们会额外进行一次判断,来决定是否添加 rounded-full
类名
`border ${shape === 'circle' ? 'rounded-full' : 'rounded-xl'}`
这样的写法比较传统,但是当条件越来越多时,代码会变得非常难以维护。因此,我们使用 clsx
来有条件的合并类名,他的写法如下
10import clsx from 'clsx'2030clsx(40'border dark:border-0 border-gray-200 dark:inset-ring dark:inset-ring-white',50{60'rounded-full': shape === 'circle',70'rounded-xl': shape === 'square',80},90className10)
clsx
会自动判断传入的类名是否为真,如果为真,则合并到一起,如果为假,则不合并。
clsx
可以非常方便的实现有条件的合并类名,并且代码的可读性也得到了提升。但是,在它合并完成之后,依然可能存在一些相同属性的重复类名。因此,我们还需要结合使用 twMerge
来合并类名以避免类名的重复。
10import clsx from 'clsx'20import { twMerge } from 'tailwind-merge'3040function Avatar(props: AvatarProps) {50const { person, size, shape = 'circle', className = '', ...others } = props6070const cls = twMerge(clsx(80'border dark:border-0 border-gray-200 dark:inset-ring dark:inset-ring-white',90{10'rounded-full': shape === 'circle',11'rounded-xl': shape === 'square',12},13className14))1516// ...17}
最后我们来看一下完整代码和案例演示。本案例包含了类型声明、属性继承、默认值、类名合并、有条件的合并类名等技巧。这些技巧在实践中都是非常实用且使用频率非常高的,因此,我们可以自己动手实现一下,以加深印象。
10import clsx from 'clsx'20import { twMerge } from 'tailwind-merge'30import { getImageUrl, UserInfo } from './utils'4050interface AvatarProps extends React.ImgHTMLAttributes<HTMLImageElement> {60person: UserInfo,70size: number | string,80shape?: 'circle' | 'square',90className?: string10}1112export default function Avatar(props: AvatarProps) {13const { person, size, shape = 'circle', className = '', ...others } = props1415const 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},21className22))2324return (25<img26{...others}27className={cls}28src={getImageUrl(person)}29alt={person.name}30width={size}31height={size}32/>33)34}35