开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情
button 可以说是 ant-design-vue 最基础最通用的组件,所以打算从这个组件开始一步一步熟悉这个框架的源码。当然,由于篇幅有限,个人能力也有不足之处,所以只会截取部分代码进行分析。源码可在 GitHub 中查看,详细的文档可在 ant-design-vue 官网查看。
第一部分 主体结构
- 在 button/button.tsx 文件中,即可查阅到 button 组件的核心源码。
export default defineComponent({
name: 'AButton',
inheritAttrs: false,
__ANT_BUTTON: true,
props: initDefaultProps(buttonProps(), { type: 'default' }),
slots: ['icon'],
setup(props, { slots, attrs, emit }) {
...
}
})
defineComponent 是类型推导的辅助函数。setup 是组合式 API 的入口,包含主要的处理逻辑,这里先省略。
- props 是组件重要的选项,所以先分析 initDefaultProps 函数。
const initDefaultProps = <T>(
types: T,
defaultProps: {
[K in keyof T]?: T[K] extends VueTypeValidableDef<infer U>
? U
: T[K] extends VueTypeDef<infer U>
? U
: T[K] extends { type: PropType<infer U> }
? U
: any;
},
): T => {
const propTypes: T = { ...types };
Object.keys(defaultProps).forEach(k => {
const prop = propTypes[k] as VueTypeValidableDef;
if (prop) {
if (prop.type || prop.default) {
prop.default = defaultProps[k];
} else if (prop.def) {
prop.def(defaultProps[k]);
} else {
propTypes[k] = { type: prop, default: defaultProps[k] };
}
} else {
throw new Error(`not have ${k} prop`);
}
});
return propTypes;
};
export default initDefaultProps;
initDefaultProps 函数用来初始化组件的 props。第一个参数 types 里面包含了所有的 props,第二个参数 defaultProps 里面包含默认 props。处理的时候先遍历 defaultProps 的参数,如果 types 里面没有该参数就会报错,如果 types 里面有该参数则再细分处理。types 里面的属性值有 type 或者 default,则将新的默认值保存到 default;types 里面的属性值有的 def,则将新的默认值放到 def 函数里面;其他情况则将属性值保存到 type 中,新的默认值放到 default 中。
第二部分 setup 函数主体
- useConfigInject 函数返回的配置借助了注入的 configProvider,默认值是 defaultConfigProvider。如果使用了 config-provider 组件则会使用该组件注入的配置。
const { prefixCls, autoInsertSpaceInButton, direction, size } = useConfigInject('btn', props);
使用解构赋值,可以提取出 useConfigInject 函数返回的字段
- 按钮包含 loading 状态,这里可以控制 loading 是否显示以及延迟执行的时间
const loadingOrDelay = computed(() =>
typeof props.loading === 'object' && props.loading.delay
? props.loading.delay || true
: !!props.loading,
);
watch(
loadingOrDelay,
val => {
clearTimeout(delayTimeoutRef.value); // 清除定时器
if (typeof loadingOrDelay.value === 'number') {
delayTimeoutRef.value = setTimeout(() => {
innerLoading.value = val;
}, loadingOrDelay.value);
} else {
innerLoading.value = val;
}
},
{
immediate: true,
},
);
onBeforeUnmount(() => {
delayTimeoutRef.value && clearTimeout(delayTimeoutRef.value); // 组件卸载后也要清除定时器
});
这部分代码核心是将 props.loading 转化成 innerLoading。props.loading 为对象的时候提取出 delay 的数值并构造一个定时器,为布尔值的时候直接提取出来。注意,在组件卸载前需要清除存在的定时器。
- 为了展示按钮不同的样式效果,这里使用了多个类名进行控制
const classes = computed(() => {
const { type, shape = 'default', ghost, block, danger } = props;
const pre = prefixCls.value;
const sizeClassNameMap = { large: 'lg', small: 'sm', middle: undefined };
const sizeFullname = size.value;
const sizeCls = sizeFullname ? sizeClassNameMap[sizeFullname] || '' : '';
return {
[`${pre}`]: true,
[`${pre}-${type}`]: type,
[`${pre}-${shape}`]: shape !== 'default' && shape,
[`${pre}-${sizeCls}`]: sizeCls,
[`${pre}-loading`]: innerLoading.value,
[`${pre}-background-ghost`]: ghost && !isUnborderedButtonType(type),
[`${pre}-two-chinese-chars`]: hasTwoCNChar.value && autoInsertSpace.value,
[`${pre}-block`]: block,
[`${pre}-dangerous`]: !!danger,
[`${pre}-rtl`]: direction.value === 'rtl',
};
});
类名生效后,对应 style 目录里面的样式文件,index.less 保存对应的样式,mixin.less 保存对应的样式方法。声明的样式变量保存在 components/style 目录下面。
第三部分 渲染函数分析
- 处理 loading 效果和两个汉字中间插入空格的效果
const insertSpace = (child: VNode, needInserted: boolean) => {
// 文字之间插入空格
const SPACE = needInserted ? ' ' : '';
if (child.type === Text) {
let text = (child.children as string).trim();
if (isTwoCNChar(text)) {
text = text.split('').join(SPACE);
}
return <span>{text}</span>;
}
return child;
};
const iconNode =
icon && !innerLoading.value ? (
icon
) : (
<LoadingIcon existIcon={!!icon} prefixCls={prefixCls.value} loading={!!innerLoading.value} />
);
const kids = children.map(child => insertSpace(child, isNeedInserted && autoInsertSpace.value));
如果有 icon 并且没有 innerLoading 则使用 icon,否则使用 LoadingIcon 组件。在 LoadingIcon 组件中,有 icon 占位的话直接替换成 loading 图标,没有的话需要使用内置组件 transition 进行过渡处理。
对扁平化处理后的默认插槽 children 进行遍历,如果是两个中文字符的话在中间插入空格。
- 展示组件最终的渲染效果
if (href !== undefined) {
return (
<a {...buttonProps} href={href} target={target} ref={buttonNodeRef}>
{iconNode}
{kids}
</a>
);
}
const buttonNode = (
<button {...buttonProps} ref={buttonNodeRef} type={htmlType}>
{iconNode}
{kids}
</button>
);
if (isUnborderedButtonType(type)) {
return buttonNode;
}
return (
<Wave ref="wave" disabled={!!innerLoading.value}>
{buttonNode}
</Wave>
);
最后渲染的时候分成三种情况,有 href 的时候直接使用 a 标签进行渲染,如果是 isUnborderedButtonType 类型的时候直接渲染 button,否则渲染 Wave 组件包裹的 button。 Wave 组件可以在点击按钮的时候增加四处扩散的波纹效果。
第四部分 小结
通过对源码的分析,可以很方便的分析组件 API 的设计理念。另外,可以分析出该组件重要的细节包括 loading 样式,注入配置,两个汉字中间加上空格,链接效果,点击扩散动画等。