Published on

TableRender/ProTable源码解读

Authors
  • avatar
    Name
    noodles
    每个人的花期不同,不必在乎别人比你提前拥有

--- 写在前面 ---
在刚开始工作的两年中,做过很多不同业务相似的页面场景.比如在中后台系统中,一个通常的业务场景就是数据查询页面-筛选项 + 表格.之前列过一些回顾自己工 作经历中相对没有做好的点,这个就是其中的一个.在面对相似的场景的时候没有考虑到一些更通用的解法,似乎只想着写完没有更多考虑做好.这篇文章是中后台系统 中效率工具思考文章的系列作品开始,后面会继续展示动态Form方案/配置化报表实现等.
--- 正文开始 ---

本文主要对TableRenderProTable两种 表格的使用和源码进行解读,通过源码解读希望可以了解到配置化表格相关实现的细节和思考点,以用来启发相应业务场景的实现思考.

TableRender

TableRender使用

    // 搜索查询schema定义
    const schema = {
        type: 'object',
        labelWidth: 70,
        properties: {
            title: {
                title: '标题',
                type: 'string'
            },
            created_at: {
                title: '创建时间',
                type: 'string',
                format: 'date'
            }
        }
    };
    // 表格展示列定义
    const columns = [
        {
            title: '标题',
            dataIndex: 'title',
        },
        {
            title: '创建时间',
            key: 'since',
            dataIndex: 'created_at',
            valueType: 'date',
        }
    ];
    // 查询列表接口定义 查询/排序参数
    const api = (params, sorter) => {
    return {
      data: dataSource,
      total: dataSource.length
    };
  };
    <TableRender
      search={{ schema }}
      request={api}
      columns={columns}
      title='最简表格'
      ref={tableRef}
      toolbarRender={ 
        <>
          <Button>查看日志</Button>
          <Button>导出数据</Button>
          <Button type='primary'>
            <PlusOutlined />
            新增
          </Button>
        </>
      }
    />

从上面的TableRender的使用代码可以看出,TableRender的使用相对简单:

  • 定义搜索框的schema实现搜索配置
  • request查询接口
  • 表格展示columns
    TableRender还对数据格式化逻辑/工具栏(支持刷新/列配置),在具体的使用上TableRender依赖Antd,在表格操作上它只封装了基本的展示功能做基础的封装并 不支持表格的选中等操作.

TableRender源码解读

表格的渲染实现其实相对简单,在对两个库源码解读中,主要关注

  • 搜索与表格api联动
  • 表格配置与数据展示实现
  • 内部关键逻辑封装
TableRender的实现如下图所示: TableRender

搜索与表格api联动

搜索form基于form-render实现,有两种方式会触发api的加载.

查询触发逻辑

    // 监听表单值的变化 触发查询
    if (mode === 'simple') {
    watch = {
      '#': _debounce((value) => {
        form.submit();
        const callBack: any = _watch?.['#'];
        if (isFunction(callBack)) {
          callBack(value);
        }
      }, 300),
      ..._watch,
    }
  }
  // form-render的handleSearch回调 点击查询按钮查询
  const handleSearch = (data: any) => {
    if (typeof onSearch === 'function') {
      onSearch(data);
    }
    refresh({ ...data, sorter });
  };

参数获取逻辑

    const getTableData = (_api: any) => {
      setState({ loading: true });
      // 在容器里面通过form实例获取表单值做查询
      let _params = {
        ...form.getValues(true),
        ...customSearch,
        ...extraSearch,
        ..._pagination,
      };
      /// 省略若干代码
      // 这里在设置全局store 比如loading态 dataSource等
      setState({
        loading: false,
        dataSource: data || rows,
        ...extraData,
        pagination: {
            ..._pagination,
            total,
            pageSize: pageSize || _pageSize,
        },
      });
    }

表格配置与数据展示

表格数据获取

    // 订阅全局的dataSource
    const dataSource = useTableStore((store) => store.dataSource);

表格格式展示

    const proColumns = useMemo(() => {
        // getProColumns会解析columns配置,里面内置了对数据类型的封装逻辑从而实现格式化展示
        const proColumns = getProColumns(columns);
        if (columnsSetting && columnsSetting.length > 0) {
            return setColumns(columnsSetting, proColumns)
        }
        return proColumns;
    }, [columns, columnsSetting]);

内部关键逻辑封装

内部实现了一些简单的请求周期的方法封装,比如onSearch/afterSearch,可以将一些内部状态透传给外部

ProTable

ProTable在功能上更像是TableRender的加强版.在功能上它增加了更多常用的表格展示类型,配置上更加丰富且灵活.在实现结构上,ProTable跟TableRender的实现类似, ProTable在封装上更加集中,在封装的action中增加了数据轮训、请求取消、debounce等能力,融合了更多日常表单使用的习惯

ProTable使用

构建dataSource


    const tableListDataSource: TableListItem[] = [];
    for (let i = 0; i < 50; i += 1) {
    tableListDataSource.push({
        key: i,
        containers: Math.floor(Math.random() * 20),
        creator: 'aaa',
        createdAt: Date.now() - Math.floor(Math.random() * 100000),
    });
    }

表格columns配置与查询配置

    const columns: ProColumns<TableListItem>[] = [
        {
            title: '容器数量',
            width: 120,
            dataIndex: 'containers',
            align: 'right',
            search: false,
            sorter: (a, b) => a.containers - b.containers,
        },
        {
            title: '创建者',
            width: 120,
            dataIndex: 'creator',
            valueType: 'string',
        
        },
        {
            title: '创建时间',
            width: 140,
            key: 'since',
            dataIndex: 'createdAt',
            valueType: 'date',
            sorter: (a, b) => a.createdAt - b.createdAt,
            renderFormItem: () => {
            return <RangePicker />;
            },
        },
    ];

表格展示

    // 省略若干配置
    <ProTable<TableListItem>
      columns={columns}
      // 通过actionRef获取内部暴露的方法
      actionRef={actionRef}
      cardBordered
      // 支持选中表格题目配置
      rowSelection={{
        selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
        defaultSelectedRowKeys: [],
      }}
      // 支持传入dataSource或者request模式
      dataSource={tableListDataSource}
    />

ProTable源码解读

搜索与表格api联动

    // 顶部筛选区域根据传入的columns配置生成 表单值变化的时候触发onFormSearchSubmit
    // onFormSearchSubmit会修改formSearch
    <FormRender<T, U>
        columns={propsColumns}
        onFormSearchSubmit={(values) => {
          onFormSearchSubmit(values);
        }}
        ghost={ghost}
        onReset={props.onReset}
        onSubmit={props.onSubmit}
    />
    // formSearch作为内部封装的action接口effects入参 当effects发生变化的时候,会触发api请求fetchData
    const action = useFetchData(fetchData, defaultData, {
    pageInfo: propsPagination === false ? false : fetchPagination,
    loading: props.loading,
    dataSource: props.dataSource,
    onDataSourceChange: props.onDataSourceChange,
    onLoad,
    onLoadingChange,
    onRequestError,
    postData,
    revalidateOnFocus,
    manual: formSearch === undefined,
    polling,
    effects: [
      stringify(params),
      stringify(formSearch),
      stringify(proFilter),
      stringify(proSort),
    ],
    debounceTime: props.debounceTime,
    onPageInfoChange: (pageInfo) => {
      if (!propsPagination || !fetchData) return;

      // 总是触发一下 onChange 和  onShowSizeChange
      // 目前只有 List 和 Table 支持分页, List 有分页的时候打断 Table 的分页
      propsPagination?.onChange?.(pageInfo.current, pageInfo.pageSize);
      propsPagination?.onShowSizeChange?.(pageInfo.current, pageInfo.pageSize);
    },
  });

表格配置与数据展示

    // 表格渲染会根据传入的columns配置生成columns 
    // 从封装的action中获取展示数据源
    const getTableProps = () => ({
      ...rest,
      size,
      // 表格选择配置
      rowSelection: rowSelection === false ? undefined : rowSelection,
      // 根据传入的配置预处理成columns 做数据格式化展示
      columns: columns.map((item) =>
        item.isExtraColumns ? item.extraColumn : item,
      ),
      loading: action.loading,
      // action.dataSource 数据源 展示表格数据
      dataSource: editableUtils.newLineRecord
        ? editableDataSource(action.dataSource)
        : action.dataSource,
      pagination,
      onChange: (
        changePagination: TablePaginationConfig,
        filters: Record<string, (React.Key | boolean)[] | null>,
        sorter: any,
        extra: TableCurrentDataSource<T>,
      ) => {
        // 触发表格排序/条目选择的查询 触发外层api请求
      },
    });

内部关键逻辑封装

在封装的useFetchData hook中封装了请求的轮训、取消和debounce能力,通过将action直接暴露给用户也给用户控制数据请求的能力.

一点思考&总结

  • 对比TableRender与ProTable,ProTable相对来说对功能的封装更加全面,将一些日常表格需要处理的行为都内置了.复杂度下不确定会不会带来更多的 性能问题,这点需要在实际的使用中考量.TableRender对功能的封装相对少,满足基本的表单查询展示功能
  • 在做业务逻辑抽象封装的时候,有两个似乎对立面的词: 小而精&大而美.实际上在使用封装的能力时候,我们总会认为它不够小而精或者大而美.这其中一个原因 是在做技术选型的时候没有很好识别当前业务的现状(当然这点很难,业务是会发展的),也有对业务的场景能力没有做好划分的原因.有克制的能力封装也许是在对 业务逻辑抽象时需要考虑的一个因素
  • 在业务抽象时,要提供类似生命周期或者关键逻辑的接口,比如像webpack基于tapable实现的插件机制就给用户通过plugin订阅内部事件的能力
  • 在对比TableRender与ProTable的时候,想到了低代码的实现.通过完整的低代码周期实现的页面成为low code的话,用这种ProTable实现的方式可以 称为low code + little code.也许有时候这种low code + little code就能满足团队的诉求.

相关链接

ProTable
TableRender