Ant Design Vue 3.0 的那些正经事儿

5,604 阅读8分钟

说在前面,这不仅是一篇 3.0 的功能介绍,更是阐述背后变更的原因,以及设计的逻辑,如果你正在开发自己的组件库,期望能够帮助到您 。 ​

没错,3.0 她来了,3.0 是一个吉利的数字,她比2大1,但又比4小1,就是那么神奇,不是嘛!(⊙o⊙)… ​

v2 版本似乎发布了没有多久,为什么直接调到了 v3,主要原因是: ​

  • 她有较多的更新,同时有一些是无法向下兼容的,例如,使用 dayjs 替换 momentjs
  • v2 版本主要是我们为了兼容 Vue 3 开发的一个版本,他没有太多的新特性,少数的破坏性更新也只是为了更好的兼容 Vue 3 的,对于一些新的功能和交互只能推迟到 v3 版本来实现了。
  • 3 真的是一个吉利的数字,他刚好和 vue 3 一致,不会让一些人误解,未来的 4 5 6......,也只会出现“我都使用 4 了,你们还在用3” 的说法,不得不说,这的确可以减少一些答疑成本(太机智了)
  • 团队开始全职维护该组件库,节奏会比以往要快很多,前方加速,请系好安全带(更新快了,你说我不稳定,更新慢了,你说我不维护,太 TM 难了)

说回正经事儿

3.0 都更新了啥?我想从源码层面、功能层面、易用性层面、性能层面分别讲解。 ​

源码层面

使用 TS + Composition Api 进行重构,目前只有极个别的组件还在使用 Option Api,我们会逐步重构,但这些组件不会有破环性更新,所以不用担心未来的升级成本。搭配 Volar 你会得到更好的类型提示。不得不说 Vue 3 在 TS 方面有了很大的提升,但依然还有一些不足,对组件库来说体感较强的是 Vue 3 源码类型复杂度较高,泛型组件不友好,组件属性类型复用不友好,这三个问题分开来看都有一些方案去解决,但三者结合起来,我们依然没有找到一个非常好的平衡点去规范它,当然,我们在 TS 方面也并不深入,如果你比较擅长,也非常欢迎加入我们,一起为 Vue 3 生态,为开源贡献一份自己的力量。 ​

目前我们对于组件属性的类型定义方案是把 props 单独抽出来定义成一个函数,举个例子:

import type { ExtractPropTypesPropType } from 'vue';

export type ButtonTypes = 'primary' | 'ghost' | 'dashed' | 'link' | 'text';

export const buttonProps = () => ({
 type: { typeString as PropType<ButtonTypes> }
})

export type ButtonProps = Partial<ExtractPropTypes<ReturnType<typeof buttonProps>>>;
                                                              
export default defineComponent({
 name'AButton',
 propsbuttonProps(),
})

通过这种方式,我们同时导出 buttonProps ButtonProps , 前者一般是给其他组件或用户二次封装组件使用,后者一般是用户写业务代码使用,至于前者为何是函数返回,这里涉及到一个引用类型默认值的问题,就是那个Vue 2 时代经典的面试题,为何定义 data 要用函数返回是一样的道理。通过这种方式算是达到了类型复用的目的。如果你有更好的建议,欢迎通过 PR 的方式讨论,你可以从一个简单的组件开始。 ​

功能层面

简单一句话就是,同步了 antd 4.x 的功能:

1、自定义时间库,使用 dayjs 作为默认时间库,提供 momentjs、date-fns 快速切换功能

2、Tree、TreeSelect 支持虚拟滚动,Table 支持表格合计

3、更好的暗黑主题

4、更好的无障碍辅助

5、支持 RTL,除了少量未重构组件尚不支持,正式版之前添加

6、CSS Variables 正式版之前添加 ​

更多细节功能不再展开叙述。

易用性层面

antd 经历了默认好看,到现在一直在追求的默认好用,但好用包含了功能强大、API 简单,这两个本身就是矛盾的存在,应该说我们一直探索的是他们之间的平衡点,是如何在扩展更多功能的时候,保持 API 的简单易用。 ​

本次主要更改了 TreeTreeSelectTableCard 的自定义渲染时的API,我们以Table举例,

<template>
  <a-table :dataSource="dataSource" :columns="columns" />
</template>
<script>
  export default {
    setup() {
      return {
        dataSource: [
          {
            key: '1',
            name: '王二蛋',
          },
        ],

        columns: [
          {
            title: '姓名',
            dataIndex: 'name',
          },
        ],
      };
    },
  };
</script>

渲染结果如下:

image.png

很简单,对不对,但王二蛋是一位坐拥亿万资产的富二代,也是我们的VIP客户,老板期望对该类客户进行特别标示,名字要加粗加红加V加...,怎么玩,在 React 里面,我们可以直接:

name<div class="jia_cu jia_hong jia_v">王二蛋</div>

当然在 Vue 中,你依然可以使用同样的方式进行配置,前提是使用 vue-jsx 语法或手写 render 函数,但 template 语法会让我们得到更好的性能,比 react 还要好的性能。那么在 template 中怎么配置,在 1.x 和 2.x 版本中我们约定了一个规则是在 columns 中通过 slots 指定一个"任意"名称的插槽进行配置:

<template>
  <a-table :dataSource="dataSource" :columns="columns">
   <template v-slot:customName="{record, text}">
     <template v-if="record.name === '王二蛋'">
       <div class="jia_cu jia_hong jia_v">王二蛋</div>
      </template>
      <template v-else>{{text}}</template>
    </template>
  </a-table>
</template>
<script>
  export default {
    setup() {
      return {
        dataSource: [
          {
            key'1',
            name'王二蛋',
          },
        ],

        columns: [
          {
            title'姓名',
            dataIndex'name',
            slots: { customRender'customName' // 名称任取}
          },
        ],
      };
    },
  };
</script>

似乎看着没有什么问题,的确这种方式陪伴了我们好多年,似乎大家也都已经习惯了这种配置方式,但是在权衡了很久之后,我们依然决定打破这个舒适圈,这种方式的 API,他有两个弊端: ​

1、配置膨胀,如果你的每一列都需要自定义渲染,你需要在 columns 中都要配置上 slots,也要相应的在 template 提供对应的插槽。 ​

2、才是最主要的问题,他有一个很大的风险就是,我们无法限制用户自定义的插槽名称,那么如果未来组件内部需要扩展插槽,就会有冲突的风险,这是一个不可控的风险。 ​

先插一句,防杠精,其实我们是提供了 a-table-column 方式去构建配置的,这种方式并不需要去配置插槽,但我们并没有主推这种方式:

原因一是个人感觉 columns 要比 a-table-column 少写很多字符,也更直观些;

原因二是 columns 的形式,相较于 a-table-column 性能略高,其实 a-table-column 内部依然会转化为 columns,转化的过程目前有两种方式,第一种是父组件 table,遍历子组件 a-table-column,生成 columns;第二种是子组件 onMounted 之后,将子组件信息告诉父组件,这里还有个恶心的点是,构建子组件在父组件的位置索引,Vue 是不承诺渲染"过程顺序"的,只保证"结果顺序",这个索引的构建方法我就不展开了;除此之外,上述两种方法其实都用了非Vue文档API,都是有一定的风险的。总之,因为大部分用户选择了 columns,我们期望将 columns 的使用难度降低,让table更加易用。 ​

所以 3.0,我们废弃了 column.slots 的配置,而是提供了统一的自定义出口 v-slot:bodyCell,使用新的 API 优化后的代码如下:

<template>
  <a-table :dataSource="dataSource" :columns="columns">
  	<template v-slot:bodyCell="{record}">
    	<template v-if="record.name === '王二蛋'">
      	<div class="jia_cu jia_hong jia_v">王二蛋</div>
      </template>
		</template>
  </a-table>
</template>
<script>
  export default {
    setup() {
      return {
        dataSource: [
          {
            key: '1',
            name: '王二蛋',
          },
        ],

        columns: [
          {
            title: '姓名',
            dataIndex: 'name',
          },
        ],
      };
    },
  };
</script>

插槽中可以不需要 v-else,组件内部会自动 fallback 到默认值(王二蛋) ,正如我前文所说,当我们新增插槽的时候,会有和你自定义插槽冲突的风险,希望你没有起名叫 bodyCell 的插槽,除此之外,headerCell customFilterDropdown customFilterIcon都是新增的插槽,我想应该大概可能不会那么巧就碰上了吧。 ​

同样的逻辑,我们优化了 Tree、TreeSelect 自定义 title 的逻辑,不再需要在 treeData 数据源中通过 slots 配置自定义插槽,而是由 v-slot:title 统一接管。Card 自定义tab,不再需要在 tabList 数据源中通过 slots 配置自定义插槽,而是通过 v-slot:customTab接管。

性能层面

1、FormItem 使用 provide/inject 代替 cloneElement,减少 render 次数,提升表单性能

2、废弃 TreeNode、TreeSelectNode 使用 treeData 属性替代

3、在 2.x 版本中,Select、AutoComplete 支持了虚拟滚动,在 3.0 中,我们又新增了 Tree、TreeSelect 组件支持虚拟滚动。可以轻松应对大数据渲染。

4、其它很多用户无感知的组件内部性能优化

为什么 Table 还不支持虚拟滚动?

两个原因:破坏性更新 和 成本 问题

如果要支持完善的虚拟滚动,会有很大的成本,注意,这里说的是完善的虚拟滚动,不是简单一个列表,我们要支持固定列、行列合并、展开、子表格等等一系列问题,开发成本完全不亚于一套组件库了,这也是为什么 MUI(原名 material ui 7万+ star 的开源项目) 专门招聘了全职人员维护虚拟表格组件。

虽然你在 antd react 版本文档中有一个虚拟滚动的示例,但那完全是借助第三方组件自定义的一个简单虚拟滚动示例,如果要添加固定列、选择、拖拽、展开等功能,成本非常高,你可以尝试在 antd react 版本的 issue 下搜索 table 虚拟关键词查看相关问题。

对于Table,我们也会推出独立的产品去解决大数据展示问题,初版应该不会让大家等太久,如果顺利,最近一两个月会和大家见面,敬请期待! 可以关注下官方公众号,我们会及时通过公众号发布通知。

image.png

这部分展开来说,有很多干货,我也会在 10.23 早早聊Vue专场会议(免费的,尤大也来)上分享给大家,大家可以关注下

image.png