关于二次封装element-plus table的一些归纳总结(4)

在讲解实现原理之前,我们首先需要思考的就是实现思路,一个表格主要分成两部分,第一就是表头,第二就是表体。

因此,我们的最终实现也是按照这两个部分去实现的,我们将采用tsx语法来实现,因为tsx语法实现起来更简单一些。

定义表格组件的props

首先创建一个tableProps.ts,然后定义我们的传入table的props属性,分别如下所示:

ts 复制代码
export const tableProps = {
  data: {
    type: Array as PropType<Record<string, any>[]>,
    default: () => [],
  },
  align: {
    type: String,
    default: "center",
  },
  column: {
    type: Array as PropType<ElTableColumnProps[]>,
    default: () => [],
  },
};

这里涉及到了两个类型的定义,稍后我们会在类型当中讲到。从这段代码当中,我们也可以知道,我们的table的props属性包含了3个,即data,align,column,这也是我们在使用示例当中用到了的,其中data表示表体数据,由于数据是由使用用户决定的,通常来讲就是一个对象数组,因此,这里我们使用了Record内置类型来定义,默认值是一个空数组,然后align表示全局设置内容的排练方式,有left,right,center三种值,默认值是center,表示居中,column的配置就稍微复杂了一点,我们后续再根据类型定义来详细说明。

事实上,如果我们使用js代码,应该如下所示:

js 复制代码
export const tableProps = {
  data: {
    type: Array,
    default: () => [],
  },
  align: {
    type: String,
    default: "center",
  },
  column: {
    type: Array,
    default: () => [],
  },
};

笔记: vue props中定义方式,type表示数据类型,类型通常首字母大写(即String,Array,Object,Boolean等数据类型),然后vue内部提供了一个PropType用来定义具体的类型,as关键字表示断言,default即默认值。

表格列的封装

接下来我们来看表格列的封装,新建一个table-column.tsx,vue提供了一个defineComponent方法为我们定义一个组件,该方法传入一个对象参数,对象包含了props的定义,以及setup函数等,这个列的封装我们使用了tsx语法来实现。代码如下所示:

tsx 复制代码
import { defineComponent } from "vue";
// ...
const TableColumn = defineComponent({
  props: {
     // ...
  },
  setup(props, { slots }) {
     // ...
     return () => <></>;
  },
});

export default TableColumn

接下来,我们获取我们定义的props,然后传递给props属性,如下所示:

tsx 复制代码
import { defineComponent } from "vue";
// 
import { MergeTableProps, tableProps } from "./tableProps";
export type ColumnTypeProps = Pick<MergeTableProps,'column' | 'align'>;
// 
const columnProps = { column:tableProps.column,align:tableProps.align };

// ...
const TableColumn = defineComponent({
  props: {
      ...(columnProps as ColumnTypeProps),
  },
  setup(props, { slots }) {
     // ...
     return () => <></>;
  },
});

以上我们从tableProps里引入定义好的props,然后定义一个新的变量columnProps来接受,紧接着传递给defineComponent的props参数。注意这里涉及到了MergeTableProps类型,我们将在后文介绍。

接下来我们在setup函数中获取props的column列数组,然后对每一列添加渲染列组件,并用作返回值渲染。代码如下所示:

tsx 复制代码
// ...
const TableColumn = defineComponent({
  props: {
      ...(columnProps as ColumnTypeProps),
  },
  setup(props, { slots }) {
      // 渲染每一列
     const renderColumn = (columnDict: Record<string, any>, index: number) => {
         // ...
     }
     const columnsSlots = props?.column?.map(renderColumn);
     return () => <>{columnsSlots}</>;
  },
});

接下来就是针对一些列属性做规范化或者赋值,然后添加到列组件中,并返回这个列组件。代码如下所示:

tsx 复制代码
// ...
const TableColumn = defineComponent({
  props: {
      ...(columnProps as ColumnTypeProps),
  },
  setup(props, { slots }) {
      // 渲染每一列
     const renderColumn = (columnDict: Record<string, any>, index: number) => {
         // ...
         
         );
     }
     //...
  },
});

这里我们使用v-slots指令来解析属性,其中vSlots是一个对象,我们规范化或者赋值的属性都将存储到该对象当中,接下来我们获取列当中的配置属性render属性,以及slotName, headerSlot, children等属性,并根据这些属性来决定渲染的结果。首先当然是判断render是否是一个函数,如果是,则给vSlots赋值一个default属性,作为渲染的子组件。如果存在slotName属性,并且能够从slots上获取到自定义的slotName属性值,并且还是一个函数,则作为值赋值给default属性 ,如果children的长度大于0,则渲染children,如果存在headerSlot属性,并且slots[headerSlot]是一个函数,则赋值给header属性。代码如下所示:

tsx 复制代码
import { MergeTableProps, ScopeType, SlotsType, tableProps } from "./tableProps";
// ...
const TableColumn = defineComponent({
  props: {
      ...(columnProps as ColumnTypeProps),
  },
  setup(props, { slots }) {
      // 渲染每一列
     const renderColumn = (columnDict: Record<string, any>, index: number) => {
          // 获取配置属性
          const { render, slotName, headerSlot, children, ...rest } = columnDict;
          // 定义存储结果
          const vSlots:SlotsType = {};
          // default插槽和header插槽都应该是一个函数
          // 判断render属性是否是一个函数
         if (typeof render === "function") {
             // scope为函数上下文
            vSlots.default = (scope) => {
              //  如果存在prop属性则传递参数,否则不传
              if (rest.prop) {
                return render(scope.row[rest.prop], scope);
              }
              return render(scope);
            };
          }
         // 如果存在slotName属性 ,并且slots[slotName]是一个函数,则渲染,即自定义插槽
         if (slotName && typeof slots[slotName] === "function") {
            const defaultSlotName = slots[slotName] as ScopeType;
            vSlots.default = scope => defaultSlotName(scope);
         }
         // 如果存在headerSlot属性,并且存在slots[headerSlot],则渲染header,即自定义列插槽
         if (headerSlot && slots[headerSlot]) {
            const headerSlotName = slots[headerSlot] as ScopeType;
            vSlots.header = scope => headerSlotName(scope);
         }
         // 否则递归渲染children
         if (children?.length > 0) {
            vSlots.default = () => children.map(renderColumn);
         }
         // ...
     }
     const columnsSlots = props?.column?.map(renderColumn);
     return () => <>{columnsSlots}</>;
  },
});

以上涉及到了2个类型,即SlotsType和ScopeType,它们分别定义如下:

ts 复制代码
export type SlotsType = {
  default?: (scope: Record<string, any>) => any;
  header?: (scope: Record<string, any>) => any;
};

export type ScopeType = (scope: any) => {}

这里的ts类型定义是不严格的,因此这2个类型也比较好理解,以上就是列组件的实现,接下来我们来看表格组件的实现。

表格组件的最终实现

实际上封装实现了列组件,那么表格组件就好实现了,与列组件一样,我们也是采用tsx语法来实现,同样也会用到defineComponent函数。代码如下所示:

tsx 复制代码
import { defineComponent  } from "vue";
import TableColumn from "./table-column";
// ...
export default defineComponent({
  name: "ElementTable",
  props: {
    // ...
  },
  // 注册列组件
  components: { TableColumn },
  setup(props, { slots, attrs }) {
    return () => (
      <div class="element-table">
         // ...
      </div>
    );
  }
});

接下来,我们同样会从tableProps.ts中获取定义好的tableProps当做参数传递给defineComponent的props属性,然后这里会涉及到element table的一些方法,我们在mounted钩子函数里面去调用,然后在methods属性里面去定义,我们会给el-table绑定一个ref属性,然后通过this上下文去获取,并添加到this对象当中。在el-table中,我们也会使用v-slots指令,这个指令配置了2个参数append与empty属性,如果slots属性中存在对应的属性,则添加进去,最后我们将table-column组件渲染到el-table组件中去。代码如下所示:

tsx 复制代码
import { defineComponent  } from "vue";
import TableColumn from "./table-column";
import { MergeTableProps, MethodNameType, tableProps } from "./tableProps";
export default defineComponent({
  name: "ElementTable",
  props: {
    ...(tableProps as MergeTableProps),
  },
  components: { TableColumn },
  mounted() {
    // 调用方法注册el-table组件的一些方法
    this.injectTablePrimaryMethods();
  },
  methods: {
    // 注册方法 
    injectTablePrimaryMethods() {
      const _self = this as ThisType<unknown>;
      const elTableRef = _self['$refs']['elTableRef'];
      const tableMethodNameList: MethodNameType[] = [
        "clearSelection",
        "toggleRowSelection",
        "toggleAllSelection",
        "toggleRowExpansion",
        "setCurrentRow",
        "clearSort",
        "clearFilter",
        "doLayout",
        "sort",
      ];

      for (const name of tableMethodNameList) {
        if (!(_self[name])) {
          _self[name] = elTableRef?.[name];
        }
      }
    }
  },
  setup(props, { slots, attrs }) {
    return () => (
      <div class="element-table">
        <el-table
          data={props.data}
          ref="elTableRef"
          {...attrs}
          v-slots={{
            append: () => slots.append && slots.append(),
            empty: () => slots.empty && slots.empty(),
          }}
        >
        // 渲染table-column
          <table-column column={props.column} align={props.align} v-slots={slots}></table-column>
        </el-table>
      </div>
    );
  }
});

这里涉及到了ThisType类型,它是ts的内置类型,以及MethodNameType类型,它的类型定义如下:

ts 复制代码
import type ElTable from "element-plus/lib/components/table";
// InstanceType 为ts内置类型,表示获取实例类型,使用typeof用于将导入的el-table组件转化成类型
export type ElTableType =  InstanceType<typeof ElTable>;
export type MethodNameType = keyof ElTableType;

以上ts类型定义,我们也已经在注释中说明,没有什么可以细讲的,接下来就是我们的最后一步了。

将封装好的table组件用作插件注册到vue中

最后我们会定义一个install方法,然后添加到table中去当做属性,而在install方法内部,我们将使用vue中提供的component方法去注册这个组件,这是vue中实现插件的方式,代码如下所示:

ts 复制代码
import { App,createApp } from "vue";
import Table from './elementTable';

// install方法接收2个参数,即app应用实例,与配置对象
function install(app: ReturnType<typeof createApp>, options = {}) {
  // app.component注册组件
  app.component(Table.name, Table);
}
// 定义类型
export type ElementTable = typeof Table & {
  install: (app: App, options: Record<string, any>) => App;
};

Table.install = install;

// 导出table
export default Table as ElementTable;

export  { Table };

组件实现就到此为止了,接下来我们来看类型定义。

类型的定义

既然要使用tsx语法,那么我们就避免不了需要定义一些类型,首先创建一个table.d.ts文件,然后我们使用declare关键字创建一个namespace空间,名字就叫ElementTable,如下所示:

ts 复制代码
declare namespace ElementTable {
   // ... 在这里定义一些类型
}

第一个类型是ConditionalKeys,该类型接受2个参数,第一个参数代表一个接口,第二个参数代表一个条件类型,如字符串的组合等。这个类型的整体含义就是,要从第一个接口参数当中提取符合第二个条件类型参数的属性,有点类似Pick类型的作用。代码如下所示:

ts 复制代码
export type ConditionalKeys<T,U> = NonNullable<{
   [K in keyof T]: K extends U ? K : never
}[keyof T]>

这一个类型涉及到了ts的不少知识点,分别总结如下:

  1. NonNullable代表提取一个非null和undefined的类型,是ts的内置类型。
  2. keyof 关键字用于提取接口的属性。
  3. extends关键字代表条件判断。
  4. 读取接口属性值使用[]语法。
  5. in操作符用于读取类型属性。
  6. never类型表示从不,也是一个类型。

这个类型将用于我们需要从element table当中提取方法属性名的时候用到,后面会说到。

第二个类型则是EventKey类型,顾名思义,就是表示事件属性,代码如下所示:

ts 复制代码
export type EventKey = NonNullable<EventKeyVal[keyof EventKeyVal]>;

这个类型实际上就是一个any类型,因为EventKeyVal本身就是any类型。

第三个类型则是CamelEventKey,该类型表示驼峰化事件属性,代码如下所示:

ts 复制代码
 export type CamelEventKey<T extends string> = {
    [key in T]: key extends `on${infer A}-${infer B}`
      ? `on${A}${Capitalize<B>}`
      : key;
  };

用法如下所示:

ts 复制代码
type A = CamelEventKey<'on-click'> // 'onClick'

这个类型用到了内置类型Capitalizeinfer关键字,infer关键字我们可以理解为推断一个类型。接下来我们来看后续的类型定义,都比较好理解,代码如下:

ts 复制代码
export type eventJsx = CamelEventKey<
    CamelEventKey<EmitKeyMethod>[keyof CamelEventKey<EmitKeyMethod>]
>;
export type eventKeyName = eventJsx[keyof eventJsx];

export type eventMethodProps = {
   [key in eventKeyName]: (...args: any[]) => any;
};

export type KeyConstructor<Base extends object> = {
   [KeyProp in keyof Base]: PropType<Base[KeyProp]>;
};

eventJsx类型就是递归加泛型类型,后续的类型也都是读取属性,然后定义接口类型,比较好理解。接下来我们来看tableProps.ts里面的类型定义,代码如下:

ts 复制代码
import type ElTable from "element-plus/lib/components/table";
import type { ElTableColumn } from "element-plus/lib/components/table";
import { PropType } from "vue";

export type ElTableType = InstanceType<typeof ElTable>;
export type ElTableProps = ElTableType["$props"];

export type UserElTableColumnProps = {
  slotName?: string;
  headerSlot?: string;
  render?: (...arg: any[]) => any;
  children?: ElTableColumnProps[];
};

export type EmitKeyMethod = ElementTable.ConditionalKeys<
  ElTableType,
  `on${string}`
>;

export type ElTableColumnProps = InstanceType<typeof ElTableColumn>["$props"] &
  UserElTableColumnProps;

export type OmitTableProp = Required<
  Omit<ElTableProps, "data" | "class" | ElementTable.EventKey>
>;

export type KeyConstructor<Base extends object> = {
  [KeyProp in keyof Base]: PropType<Base[KeyProp]>;
};

export type TableValidProps = KeyConstructor<OmitTableProp>;

export type MergeTableProps = typeof tableProps &
  ElementTable.eventMethodProps & {
    otherProps: PropType<Record<string, any>>;
    summaryMethod: PropType<(...param: any[]) => string[]>;
  } & TableValidProps;

这里最难的应该就是MergeTableProps类型了,它其实是三个类型的联合类型,其它的都是基础的接口类型,或者读取接口类型。其中Required类型表示将所有属性变作必选类型,是ts的一个内置类型。

总结

我们将table组件的封装分成了2步,第一步封装列组件,然后将列组件注册到表格组件中,组合在一起得到我们最终的table组件,并且我们还添加了install方法,将组件封装成插件的形式。

如果觉得本文不错,望不吝啬点赞收藏,如有错误,敬请指正。

以上代码详情地址源码

相关推荐
PandaCave3 分钟前
vue工程运行、构建、引用环境参数学习记录
javascript·vue.js·学习
软件小伟5 分钟前
Vue3+element-plus 实现中英文切换(Vue-i18n组件的使用)
前端·javascript·vue.js
醉の虾27 分钟前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js
张小小大智慧35 分钟前
TypeScript 的发展与基本语法
前端·javascript·typescript
hummhumm1 小时前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
chusheng18401 小时前
Java项目-基于SpringBoot+vue的租房网站设计与实现
java·vue.js·spring boot·租房·租房网站
asleep7011 小时前
第8章利用CSS制作导航菜单
前端·css
hummhumm1 小时前
第 28 章 - Go语言 Web 开发入门
java·开发语言·前端·python·sql·golang·前端框架
游走于计算机中摆烂的1 小时前
启动前后端分离项目笔记
java·vue.js·笔记
幼儿园的小霸王2 小时前
通过socket设置版本更新提示
前端·vue.js·webpack·typescript·前端框架·anti-design-vue