如何封装一个通用的Table 组件以支持任意扩展

lxf2023-05-09 00:40:53

哇,这篇文章设想了好久,一直都没有机会写出来,废话不多说,让我们一路打怪升级吧!

01:一个简单的需求

当有一天产品想让我们实现一个简单的需求

如何封装一个通用的Table 组件以支持任意扩展

const dataSource = [
    {
        key: '1',
        name: '胡彦斌',
        age: 32,
        address: '西湖区湖底公园1号',
    },
    //...
];
const columns = [
    {
        title: '姓名',
        dataIndex: 'name',
        key: 'name',
    },
    {
        title: '年龄',
        dataIndex: 'age',
        key: 'age',
    },
    {
        title: '住址',
        dataIndex: 'address',
        key: 'address',
    },
];

const BaseTable:React.FC = ()=>{
    return <Table  bordered dataSource={dataSource} columns={columns} />;
};
export default BaseTable;

代码非常简单,我们非常轻易地实现了该效果。

02:想要更多的

后来 随着数据的增加, 产品和 UI想让我们把展示的效果弄的好看一点,实现以下的效果。

  • 希望姓名一栏我们添加上点击事件,用户可以去查看该用户的详情
  • 希望爱好添加上颜色做个区分度
  • 希望普通显示的文字的字体大小是 16px等等
  • 希望所有的数字都向右靠齐
  • 希望文字太长的话可以自动...然后可以hover展示
  • 希望...

如何封装一个通用的Table 组件以支持任意扩展

为演示效果,我们调三个公共的需求进行一下封装。 数据代码如下:

const dataSource:Array<DataType> = [
    {
        key: '1',
        name: '张三',
        age: 20,
        address: '银河系太阳系中国北京市天安门西大街 01 号天子壹号院 18号楼 3 单元 201',
        tags:["篮球","洗脚","上网吧"],
        wages:8000,
    },
    {
        key: '2',
        name: '李四',
        age: 18,
        address: '银河系太阳系中国北京市天安门西大街北京市中南海西大街 01 号天子壹号院 18号楼 3 单元 201',
        tags:["乒乓球","漂流","酒吧"],
        wages:13000,
    },
];

配置代码如下:

const columns: ColumnsType<DataType> = [
    {
        title: '姓名',
        dataIndex: 'name',
        key: 'name',
        render: (text:string) => <a>{text}</a>,
    },
    {
        title: '年龄',
        dataIndex: 'age',
        key: 'age',
        render: (text:string) => <span style={{}}>{text}</span>,
    },
    {
        title: '爱好',
        dataIndex: 'tags',
        key: 'tags',
        render: (tags:string[]) => (
            <>
                {tags.map((tag) => {
                    let color = colorMap.get(tag)
                    return (
                        <Tag color={color} key={tag}>
                            {tag.toUpperCase()}
                        </Tag>
                    );
                })}
            </>
        ),
    },
    {
        title: '工资',
        dataIndex: 'wages',
        key: 'wages',
        align:"center",
        render: (text:number) => <span style={{textAlign:"right",width:"100%",display:"block"}}>{text}</span>,
    },
    {
        title: '住址',
        dataIndex: 'address',
        key: 'address',
    },
];

为了实现以上的效果,我们在每一个配置里面都添加了render函数.

03:如果有很多页面都需要用到表格的这些效果呢

应该如何实现呢?

我们当然可以复制粘贴N遍,这种做法非常的不优雅,一旦其中的一个效果需要改一下,我们就需要改 N遍,复制的越多,后期维护就越麻烦,这不就是给自己挖坑吗?

我们可以封装一个底层的表格,来实现那些相同的功能,N 个页面只需要调用就好了。

那么应该如何封装呢?

我们可以把所有的公共的效果都抽取到一个render函数里面,然后根据它传入的不同的参数来识别返回它想要的是哪种效果。

我们可以给每一种效果都起一个名字

  • 默认的效果就叫 default
  • 遍历数据的 就叫 array
  • 数字要向右对齐展示的 就叫number
  • 其他不适合底层表格封装的这里就不处理了

我们需要先把 TS 的类型定义出来。

type renderKey = 'default'|'array'|'number'

interface eColumnType<RecordType> extends ColumnType<RecordType>{
    type?:renderKey
}

type eColumnsType<RecordType=unknown> = (ColumnGroupType<RecordType> | eColumnType<RecordType>)[];

我们直接模仿antd 它自己定义的类型,添加上我们需要的type,把它定义为可以空,不会影响原生的使用方式,毕竟我们只是一种扩展。

然后我们把之前散落在各个页面的表格中的render 收集起来。

//处理表格的配置,通过不同的type来实现不同的效果
function disposeColumns<T>(columns:eColumnsType<T>):eColumnsType<T>{
    return columns.map((item:eColumnType<T>)=>{
       switch (item.type){
           case "default":
               item.render=(text:string) => <span style={{fontSize:16}}>{text}</span>;
               break;
           case "array":
               item.render=(tags:string[]) => (
                   <>
                       {tags.map((tag) => {
                           let color = colorMap.get(tag)
                           return (
                               <Tag color={color} key={tag}>
                                   {tag.toUpperCase()}
                               </Tag>
                           );
                       })}
                   </>
               )
               break;
           case "number":
               item.render = (text:number) => <span style={{textAlign:"right",width:"100%",display:"block"}}>{text}</span>
               break;
           default:
       }
        return item;
    })
}

通过判断type来返回不同的效果。 下面是我们的配置代码,看的出来是不是精简了很多。

const columns: eColumnsType<DataType> = [
    {
        title: '姓名',
        dataIndex: 'name',
        key: 'name',
        //像这种点击事件,需要和上层数据直接交互绑定的,就不放在底层了的表格中处理了。
        render: (text:string) => <a>{text}</a>,
    },
    {
        title: '年龄',
        dataIndex: 'age',
        key: 'age',
        type:"default",
    },
    {
        title: '爱好',
        dataIndex: 'tags',
        key: 'tags',
        type:"array",
    },
    {
        title: '工资',
        dataIndex: 'wages',
        key: 'wages',
        align:"center",
        type:"number",
    },
    {
        title: '住址',
        dataIndex: 'address',
        key: 'address',
    },
];

可以看到我们的配置代码精简了很多,如果有 很多页面都需要类似的效果,那么只需要修改配置,添加上它想要的效果,就可以了。

实际的渲染出来的效果是一样的,这里就不再贴图了。

04:switch 坏代码的味道

虽然我们把很多页面中不同的效果都抽取到一个底层的表格中实现了,

但是这里还是有问题,我们把很多的逻辑都放在了switch里面,

一旦我们的后期需要添加新的效果,就会影响其他的逻辑,毕竟他们都在一个函数里面。

思考一下这里的代码:

function disposeColumns<T>(columns:eColumnsType<T>):eColumnsType<T>{
    return columns.map((item:eColumnType<T>)=>{
       switch (item.type){
           case "default":
              //...
               break;
           case "array":
               //...
               break;
           case "number":
              //...
               break;
           default:
       }
        return item;
    })
}

所有的switch判断语句中,它们想要的效果都是相同的,渲染一个结果出来。

每一个的渲染效果它其实都是独立的,和其他的渲染效果没有交集。

不变的就是他们这些函数的目标都是统一的,返回一个渲染的效果。

变化的时每一个渲染的效果都不相同。

所以我们可以基于此,使用 策略模式 来进行封装。

06:使用策略模式封装

既然要使用策略模式,那么我们封装的方法就要进一步抽取出来了。

我们把 default ,array ,number 它们三个都抽取封装成独立的函数。


//default:普通默认的效果
function renderDefaultView(text:string){
    return (text:string) => <span style={{fontSize:16}}>{text}</span>;
}

//array:处理数组的渲染
function renderArrayView(tags:Array<string>){
    return (tags:string[]) => (
        <>
            {tags.map((tag) => {
                let color = colorMap.get(tag)
                return (
                    <Tag color={color} key={tag}>
                        {tag.toUpperCase()}
                    </Tag>
                );
            })}
        </>
    )
}

//number:处理数字向右对齐的效果
function renderNumberView(text:number){
    return (text:number) => <span style={{textAlign:"right",width:"100%",display:"block"}}>{text}</span>
}

然后我们定义字典来方便的调用他们

const renderMap= new Map<renderKey,Function>([
    ["default",renderDefaultView],
    ["array",renderArrayView],
    ["number",renderNumberView],
])

最后我们在 disposeColumns中使用它们

//处理表格的配置,通过不同的type来实现不同的效果
function disposeColumns<T>(columns:eColumnsType<T>):eColumnsType<T>{
    return columns.map((item:eColumnType<T>)=>{
        if(item.type){
            item.render = renderMap.get(item.type)!();
        }
        return item;
    })
}

最后我们重新梳理一下代码:

  • 建立一个文件夹renderComponents,把default ,array ,number 都放到里面
  • renderComponents中添加一个配置文件,每一次添加新的渲染效果,就在配置文件里面添加一下配置,这样也不会影响封装组件那边的代码,变化的和不变分割开来。
  • 我们的baseTable只需要关注固定逻辑就行。
单独的类型文件type
import {ColumnType} from "antd/es/table";
import {ColumnGroupType} from "antd/es/table/interface";
import {TableProps} from "antd/lib/table/Table";
import {renderKey} from './renderComponents';

//为ColumnType 添加扩展
export interface eColumnType<RecordType> extends ColumnType<RecordType>{
    type?:renderKey
}

export type eColumnsType<RecordType=unknown> = (ColumnGroupType<RecordType> | eColumnType<RecordType>)[];

//扩展 TableProps
export interface eTableProps<RecordType> extends TableProps<RecordType>{
    columns?: eColumnsType<RecordType>;
}
renderComponents 的代码
import React from "react";
import { renderKey} from "../type";
import renderDefaultView from './renderDefaultView';
import renderArrayView from './renderArrayView';
import renderNumberView from './renderNumberView';

export type renderKey = 'default'|'array'|'number';

/**
 * 渲染效果的配置文件
 */
export const renderMap= new Map<renderKey,Function>([
    ["default",renderDefaultView],
    ["array",renderArrayView],
    ["number",renderNumberView],
])
封装好的 BaseTable

我们已经框架搭建好了,以后如果想要添加不同的渲染效果,只需要去renderComponents文件下面添加就好,不需要改动BaseTable的代码,实现了变化的分离。

import React from "react";
import {Table} from "antd";
import {eColumnsType,eColumnType,eTableProps,} from './type';
import {renderMap} from './renderComponents';

//处理表格的配置,通过不同的type来实现不同的效果
function disposeColumns<T>(columns:eColumnsType<T>):eColumnsType<T>{
    return columns.map((item:eColumnType<T>)=>{
        if(item.type){
            item.render = renderMap.get(item.type)!();
        }
        return item;
    })
}

const BaseTable:React.FC<eTableProps<any>> = (props)=>{
    let {columns,...otherProps} = props;
    columns =  columns && disposeColumns(columns);
    return <Table columns={columns} {...otherProps} />;
};

export default BaseTable;

06:写在最后

我们使用策略模式封装的这个底层表格,支持了任意的扩展,随着业务的增加,

所有的效果都可以基于此来进行封装,写的越多,我们积累的资源就会越多,

就可以任意的组合,扩展。

antd table的另外一个效果,表头上点击搜索展示一个弹框,同样也可以使用这种思路来进行封装。而不需要都写在一个组件里面。

希望这篇文章对大家有所启发...

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!