ant design vue 源码解析之 button

645 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情

button 可以说是 ant-design-vue 最基础最通用的组件,所以打算从这个组件开始一步一步熟悉这个框架的源码。当然,由于篇幅有限,个人能力也有不足之处,所以只会截取部分代码进行分析。源码可在 GitHub 中查看,详细的文档可在 ant-design-vue 官网查看。

第一部分 主体结构

  1. 在 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 的入口,包含主要的处理逻辑,这里先省略。

  1. 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 函数主体

  1. useConfigInject 函数返回的配置借助了注入的 configProvider,默认值是 defaultConfigProvider。如果使用了 config-provider 组件则会使用该组件注入的配置。
const { prefixCls, autoInsertSpaceInButton, direction, size } = useConfigInject('btn', props);

使用解构赋值,可以提取出 useConfigInject 函数返回的字段

  1. 按钮包含 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 的数值并构造一个定时器,为布尔值的时候直接提取出来。注意,在组件卸载前需要清除存在的定时器。

  1. 为了展示按钮不同的样式效果,这里使用了多个类名进行控制
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 目录下面。

第三部分 渲染函数分析

  1. 处理 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 进行遍历,如果是两个中文字符的话在中间插入空格。

  1. 展示组件最终的渲染效果
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 样式,注入配置,两个汉字中间加上空格,链接效果,点击扩散动画等。