ant-design-vue 源码解析之 space

491 阅读3分钟

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

space 组件用的很少,看过源码之后,发现很多功能点都有但结构不复杂,适合作为入门学习的组件,所以选择 space 组件作为第二个学习的组件进行分析。当然,由于篇幅有限,个人能力也有不足之处,所以只会截取部分代码进行分析。源码可在 GitHub 中查看,详细的文档可在 ant-design-vue 官网查看。

第一部分 主体结构

  1. 整体结构分析
const Space = defineComponent({  // 进行类型推导的辅助函数
  name: 'ASpace',   // 组件名称
  props: spaceProps(),  
  slots: ['split'], // 插槽
  setup(props, { slots }) { // composition API 的入口
   ...
  },
});

export default withInstall(Space); // 导出可以使用的组件

可以看出,主体结构中,比较复杂的就是 props 和 withInstall,下面会详细分析这两部分。

  1. 传入的 props
export const spaceProps = () => ({
  prefixCls: String,
  size: {
    type: [String, Number, Array] as PropType<SpaceSize | [SpaceSize, SpaceSize]>,
  },
  direction: PropTypes.oneOf(tuple('horizontal', 'vertical')).def('horizontal'),
  align: PropTypes.oneOf(tuple('start', 'end', 'center', 'baseline')),
  wrap: { type: Boolean, default: undefined },
});

spaceProps 包含 prefixCls,size,direction,align,wrap 五个 props。

  1. 作为插件来使用
export const withInstall = <T>(comp: T) => {
  const c = comp as any;
  c.install = function (app: App) {
    app.component(c.displayName || c.name, comp);
  };

  return comp as T & Plugin;
};

withInstall 方法将插件的定义方式封装成一个函数,这样方便代码的复用

第二部分 setup 函数主体

  1. 主要的函数
const { prefixCls, space, direction: directionConfig } = useConfigInject('space', props);   // 使用注入的配置
const supportFlexGap = useFlexGapSupport(); // 判断是否支持 flex 的 row-gap
const size = computed(() => props.size ?? space.value?.size ?? 'small'); // 获取到 size
const horizontalSize = ref<number>();
const verticalSize = ref<number>();
watch(
  size,
  () => {
    [horizontalSize.value, verticalSize.value] = (
      (Array.isArray(size.value) ? size.value : [size.value, size.value]) as [
        SpaceSize,
        SpaceSize,
      ]
    ).map(item => getNumberSize(item)); // 根据 size 字符串获取到对应的数值,并保存到 horizontalSize 和 verticalSize
  },
  { immediate: true },
);

useConfigInject 函数在前面的文章已经分析过,这里不再赘述,可以看 ant design vue 源码解析之 button

useFlexGapSupport 函数是通过 detectFlexGapSupported 函数来做判断的,该函数首先判断 canUseDocElement 是否为 true,接着创建一个 div,使用了 display:flex;flex-direction:column;row-gap; 样式,里面再包含两个 div,最后通过 scrollHeight 是否为1来得出 flexGapSupported 的结果。

由于 size 包含多种结果,所以需要分多种情况进行处理。如果是数组直接进行处理,不是数组拼凑成包含两个元素的数组。接着遍历里面的值,对于 small, middle, large 都转化成数值,其他数值的情况直接保留。最后保存到 horizontalSize 和 verticalSize 中。

第三部分 渲染函数分析

  1. cn 计算属性分析
const mergedAlign = computed(() =>
  props.align === undefined && props.direction === 'horizontal' ? 'center' : props.align,
);
const cn = computed(() => {
  return classNames(prefixCls.value, `${prefixCls.value}-${props.direction}`, {
    // classNames 把多种类型的类名合并
    [`${prefixCls.value}-rtl`]: directionConfig.value === 'rtl',
    [`${prefixCls.value}-align-${mergedAlign.value}`]: mergedAlign.value,
  });
});

classNames 函数可以把字符串,数组和对象形式的类名进行整合。另外包含一个 mergedAlign 计算属性,当 props.align 未定义且 props.direction 是 horizontal 的时候返回 center,否则返回 props.align

  1. style 计算属性分析
const style = computed(() => {
    const gapStyle: CSSProperties = {};
    if (supportFlexGap.value) {
      gapStyle.columnGap = `${horizontalSize.value}px`;
      gapStyle.rowGap = `${verticalSize.value}px`;
    }
    return {
      ...gapStyle,
      ...(props.wrap && { flexWrap: 'wrap', marginBottom: `${-verticalSize.value}px` }),
    } as CSSProperties;
});

style 默认包含的 css 属性有 flexWrap,marginBottom。如果 supportFlexGap.value 为 true,增加 css 属性 columnGap 和 rowGap。

  1. 渲染函数分析
<div class={cn.value} style={style.value}>
  {items.map((child, index) => {
    let itemStyle: CSSProperties = {};
    if (!supportFlexGap.value) {
      if (direction === 'vertical') {
        if (index < latestIndex) {
          itemStyle = { marginBottom: `${horizontalSizeVal / (split ? 2 : 1)}px` };
        }
      } else {
        itemStyle = {
          ...(index < latestIndex && {
            [marginDirection.value]: `${horizontalSizeVal / (split ? 2 : 1)}px`,
          }),
          ...(wrap && { paddingBottom: `${verticalSize.value}px` }),
        };
      }
    }

    return (
      <>
        <div class={itemClassName} style={itemStyle}>
          {child}
        </div>
        {index < latestIndex && split && (
          <span class={`${itemClassName}-split`} style={itemStyle}>
            {split}
          </span>
        )}
      </>
    );
  })}
</div>;

space 组件有一个 split 插槽,可以对包含的多个节点中间插入内容,但是官方的文档中并没有提及这个插槽。

遍历默认插槽中的内容后,考虑到 supportFlexGap.value 的值可能为 false,还额外补充了 padding 或者 margin 来增加间距。

第四部分 小结

space 组件整体结构简单但组件的基本要素都具备了,适合用来入门学习。另外,withInstall 方法用来统一导出组件,split 插槽可以在间距里面插入内容,这两点是意外收获,一个是较好的复用方法,另一个是隐藏的插槽。