Avatar音译‘阿凡达’,释义头像或某事物的化身,支持文字、Icon、图片、远程图片,解析源码前先来看看所需参数。

Avatar作为独立组件时:

API参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export interface AvatarProps {
/** 指定头像形状:圆形,方形 */
shape?: 'circle' | 'square';
/** 指定头像大小:大,小,默认 */
size?: AvatarSize;
/** 字符类型距离左右两侧边界单位像素 */
gap?: number;
/** 图片资源路径或图片元素 */
src?: React.ReactNode;
/** 图片资源链接 */
srcSet?: string;
/** 图片是否拖拽 */
draggable?: boolean;
/** 头像的Icon图标 */
icon?: React.ReactNode;
style?: React.CSSProperties;
prefixCls?: string;
className?: string;
children?: React.ReactNode;
/** 图片加载失败的替换文字 */
alt?: string;
/** 图片加载失败的回调 */
onError?: () => boolean;
}

部分主要代码

这个组件采用函数式编程的写法,在这只列举部分主要的代码,一些动态css代码以省略号代替,完整源码请参考官方库。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
const InternalAvatar: React.ForwardRefRenderFunction<unknown, AvatarProps> = (props, ref) => {
/** 组内嵌套使用时 Group中Context的size */
const groupSize = React.useContext(SizeContext);
const [scale, setScale] = React.useState(1);
const [mounted, setMounted] = React.useState(false);
const [isImgExist, setIsImgExist] = React.useState(true);

const avatarNodeRef = React.useRef<HTMLElement>();
const avatarChildrenRef = React.useRef<HTMLElement>();
const avatarNodeMergeRef = composeRef(ref, avatarNodeRef);

const { getPrefixCls } = React.useContext(ConfigContext);

const setScaleParam = () => {
if (!avatarChildrenRef.current || !avatarNodeRef.current) {
return;
}
const childrenWidth = avatarChildrenRef.current.offsetWidth;
const nodeWidth = avatarNodeRef.current.offsetWidth;
if (childrenWidth !== 0 && nodeWidth !== 0) {
const { gap = 4 } = props;
if (gap * 2 < nodeWidth) {
setScale(nodeWidth - gap * 2 < childrenWidth ? (nodeWidth - gap * 2) / childrenWidth : 1);
}
}
};

React.useEffect(() => {
setMounted(true);
}, []);

React.useEffect(() => {
setIsImgExist(true);
setScale(1);
}, [props.src]);

React.useEffect(() => {
setScaleParam();
}, [props.gap]);

const handleImgLoadError = () => {
const { onError } = props;
const errorFlag = onError ? onError() : undefined;
if (errorFlag !== false) {
setIsImgExist(false);
}
};

const {
prefixCls: customizePrefixCls,
shape,
size: customSize,
src,
srcSet,
icon,
className,
alt,
draggable,
children,
...others
} = props;

const size = customSize === 'default' ? groupSize : customSize;

const screens = useBreakpoint();
// ...responsiveSizeStyle...
// ...devWarning...
// ...prefixCls...
// ...sizeCls...
const hasImageElement = React.isValidElement(src);
// ...classString...
// ...sizeStyle...

let childrenToRender;
if (typeof src === 'string' && isImgExist) {
childrenToRender = (
<img src={src} draggable={draggable} srcSet={srcSet} onError={handleImgLoadError} alt={alt} />
);
} else if (hasImageElement) {
childrenToRender = src;
} else if (icon) {
childrenToRender = icon;
} else if (mounted || scale !== 1) {
const transformString = `scale(${scale}) translateX(-50%)`;
const childrenStyle: React.CSSProperties = {
msTransform: transformString,
WebkitTransform: transformString,
transform: transformString,
};

const sizeChildrenStyle: React.CSSProperties =
typeof size === 'number'
? {
lineHeight: `${size}px`,
}
: {};

childrenToRender = (
<ResizeObserver onResize={setScaleParam}>
<span
className={`${prefixCls}-string`}
ref={(node: HTMLElement) => {
avatarChildrenRef.current = node;
}}
style={{ ...sizeChildrenStyle, ...childrenStyle }}
>
{children}
</span>
</ResizeObserver>
);
} else {
childrenToRender = (
<span
className={`${prefixCls}-string`}
style={{ opacity: 0 }}
ref={(node: HTMLElement) => {
avatarChildrenRef.current = node;
}}
>
{children}
</span>
);
}

delete others.onError;
delete others.gap;

return (
<span
{...others}
style={{ ...sizeStyle, ...responsiveSizeStyle, ...others.style }}
className={classString}
ref={avatarNodeMergeRef as any}
>
{childrenToRender}
</span>
);
};

const Avatar = React.forwardRef<unknown, AvatarProps>(InternalAvatar);
Avatar.displayName = 'Avatar';

Avatar.defaultProps = {
shape: 'circle' as AvatarProps['shape'],
size: 'default' as AvatarProps['size'],
};

export default Avatar;

① 使用React.forwardRef接收一个函数式组件InternalAvatar,两个参数props、ref,这个React API可以把ref转发到组件树的任意一个组件中,在组件树的任意组件中即可操作父组件传过来的ref。
② 使用React.isValidElement方法验证是否是react图片元素,通过ResizeObserver组件的api回调来计算缩放比例,实现响应式尺寸。
③ 当传入的参数不在约定规则之内时,渲染对应的children,并置为透明隐藏。

与Avatar.Group嵌套使用时:

API参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export interface GroupProps {
className?: string;
children?: React.ReactNode;
style?: React.CSSProperties;
prefixCls?: string;
/** 显示的最大头像个数 */
maxCount?: number;
/** 多余头像样式 */
maxStyle?: React.CSSProperties;
/** 多余头像气泡弹出位置 */
maxPopoverPlacement?: 'top' | 'bottom';
/** 自定义组内头像的大小 */
size?: AvatarSize;
}

Group中代码相对较少,这里全部列举出来,有两点需要分析:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const Group: React.FC<GroupProps> = props => {
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const { prefixCls: customizePrefixCls, className = '', maxCount, maxStyle, size } = props;

const prefixCls = getPrefixCls('avatar-group', customizePrefixCls);

const cls = classNames(
prefixCls,
{
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
);

const { children, maxPopoverPlacement = 'top' } = props;
const childrenWithProps = toArray(children).map((child, index) =>
cloneElement(child, {
key: `avatar-key-${index}`,
}),
);

const numOfChildren = childrenWithProps.length;
if (maxCount && maxCount < numOfChildren) {
const childrenShow = childrenWithProps.slice(0, maxCount);
const childrenHidden = childrenWithProps.slice(maxCount, numOfChildren);
childrenShow.push(
<Popover
key="avatar-popover-key"
content={childrenHidden}
trigger="hover"
placement={maxPopoverPlacement}
overlayClassName={`${prefixCls}-popover`}
>
<Avatar style={maxStyle}>{`+${numOfChildren - maxCount}`}</Avatar>
</Popover>,
);
return (
<SizeContextProvider size={size}>
<div className={cls} style={props.style}>
{childrenShow}
</div>
</SizeContextProvider>
);
}

return (
<SizeContextProvider size={size}>
<div className={cls} style={props.style}>
{childrenWithProps}
</div>
</SizeContextProvider>
);
};

export default Group;

① 利用props.children获取组件的子链,对子链(react元素)遍历,然后使用React.cloneElement()方法以子链元素为样本克隆一份,可以把新传入的props和旧的props进行合并。
② 父组件使用Context保存size配置,下面子组件Avatar可以使用Group中的size数据。