封装React AntD的table表格组件

封装组件是为了能在开发过程中高度复用功能和样式相似的组件,以便我们只关注于业务逻辑层的处理,提高开发效率,提高逼格,降低代码重复率,降低劳动时间,减少加班的可能。

本次组件的封装采用了函数式组件即无状态组件的方式来提高页面渲染性能,由于无状态组件在数据变更后不会主动触发页面的重新渲染,所以本次的封装也用到了React Hooks。下面简要介绍一下函数式组件和React Hooks。

函数式组件是被精简成一个render方法的函数来实现的,由于是无状态组件,所以无状态组件就不会再有组件实例化的过程,无实例化过程也就不需要分配多余的内存,从而性能得到一定的提升。但函数式组件是没有this和ref的,如果强行给函数式组件添加ref会报一个'Function components cannot have string refs. We recommend using useRef() instead.'的错误。函数式组件也没有生命周期,没有所谓的componentWillMount、componentDidMount、componentWillReceiveProps、shouldComponentUpdate等方法。

React Hooks是react16.8引入的特性,他允许你在不写class的情况下操作state和react的其他特性。Hook就是JavaScript函数,但是使用它们会有两个额外的规则:

只能在函数最外层调用Hook,不能在循环、条件判断或者子函数中调用;

只能在React的函数组件中调用Hook,不能在其他JavaScript函数中调用(自定义Hook除外)。

React Hooks中用的比较频繁的方法:

useState

useEffect

useContext

useReducer

useMemo

useRef

useImperativeHandle

由于以上方法的具体介绍及使用的篇幅过大,故请自行查阅API或资料,这里不再展开描述。

另外,本次封装一部分采用了JSX来创建虚拟dom节点,一部分采用了createElement方法来创建虚拟dom节点,createElement方法接收三个参数:

第一个参数:必填,可以是一个html标签名称字符串如span、div、p等,也可以是一个react组件;

第二个参数:选填,创建的标签或组件的属性,用对象方式表示;

第三个参数:选填,创建的标签或组件的子节点,可以是一个字符串或一个组件,也可以是一个包含了字符串或组件的数组,还可以是一个采用createElement创建的虚拟dom节点。

createElement方法可能用的人不会很多,因为现在有了类似于html的JavaScript语法糖JSX,使用和理解起来也较为直观和方便,符合我们对html结构的认知。但其实JSX被babel编译后的呈现方式就是使用createElement方法创建的虚拟dom,至于为何使用createElement方法,私心觉得可以提升编译打包效率。另外本次封装组件时有些地方也使用了JSX,是觉得在那些地方使用JSX更舒服,而有些地方使用createElement方法私心也觉得更符合js的编写习惯,如果你觉得在一个组件中既有JSX又有createElement会很乱的话,你可以统一使用一种即可。

本次封装所使用到的方法的介绍基本完毕,以下是组件封装的具体实现部分。

先贴一张最后实现的效果图:

1、所封装的antd table组件table.js

import React, { createElement, useState, useImperativeHandle } from 'react'

import PropTypes from 'prop-types'

import { Link } from "react-router-dom"

import { Table } from 'antd';

import { timestampToTime, currency } from '@/utils'

const h = createElement;

const TableComp = ({columns, dataSource, hasCheck, cRef, getCheckboxProps}) => {

  const empty = '-',

    [selectedRowKeys, setSelectedRowKeys ] = useState([]),

    [selectedRows, setSelectedRows] = useState([]),

    render = {

      Default: v => v,

      Enum: (v, {Enum}) => {

        if(!Enum[v]) return empty;

        return Enum[v]

      },

      Format: (v, {format}, row) => format(v, row),

      Currency: v => currency(v),

      Date: v => timestampToTime(v, 'second'),

      Link: (v, {url}) => <Link to={url}>{v}</Link>,

      Action: (v, {value}, row) => {

        const result = value.filter(n => {

          const {filter = () => true} = n

          return filter(row)

        })

        return result.map(t => <span className="table-link" onClick={() => t.click(row)} key={t.label}>{t.label}</span>)

      },

    }

  columns = columns.map(n => {

    const { type = 'Default' } = n;

    return {...n, render: (v, row) => (!v || v.length < 1) && type != 'Action' ? empty : render[type](v, n, row)}

  })

  //父组件获取selectedRowKeys的方法-cRef就是父组件传过来的ref

  useImperativeHandle(cRef, () => ({

    //getSelectedRowKeys就是暴露给父组件的方法

    getSelectedRowKeys: () => selectedRowKeys,

    getSelectedRows: () => selectedRows

  }));

  const onSelectChange = (selectedRowKeys, selectedRows) => {

    setSelectedRowKeys(selectedRowKeys);

    setSelectedRows(selectedRows);

  }

  const rowSelection = {

    selectedRowKeys,

    onChange: onSelectChange,

    getCheckboxProps: record => getCheckboxProps(record)

  };

  return hasCheck ? h(Table, {columns, dataSource, rowSelection}) : h(Table, {columns, dataSource})

}

TableComp.propTypes = {

  columns: PropTypes.array.isRequired,    //表格头部

  dataSource: PropTypes.array.isRequired, //表格数据

  hasCheck: PropTypes.bool,              //表格行是否可选择

  cRef: PropTypes.object,                //父组件传过来的获取组件实例对象或者是DOM对象

  getCheckboxProps: PropTypes.func,      //选择框的默认属性配置

}

export default TableComp

2、时间戳格式化timestampToTime.js

export const timestampToTime = (timestamp, type) => {

  let date = new Date(timestamp); //时间戳为10位需*1000,时间戳为13位的话不需乘1000

  let Y = date.getFullYear() + '-';

  let M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-';

  let D = date.getDate() < 10 ? '0' + date.getDate() + ' ' : date.getDate() + '';

  let h = date.getHours() < 10 ? '0' + date.getHours() + ':' : date.getHours() + ':';

  let m = date.getMinutes() < 10 ? '0' + date.getMinutes() + ':' : date.getMinutes() + ':';

  let s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds();

  if (type == 'second') {

    return Y + M + D + ' ' + h + m + s;

  } else {

    return Y + M + D

  }

}

3、金额千分位currency.js

export const currency = v => {

  let [n, d = []] = v.toString().split('.');

  return [n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')].concat(d).join('.');

};

简单介绍下封装的table组件,本组件基本涵盖了大多数情况下我们所用到的表格组件的情况,包括:操作按钮的权限控制、某一列的跳转、某一列的字段映射以及金额千分位、时间戳格式化和某一列数组数据的循环展示、表格是否可选择等等。如还有其他需求,可自行添加即可。

4、table组件的使用方法:

import React, { useRef } from 'react'

import { Button } from 'antd'

import Table from './table'

const TableDemo = () => {

  const permission = ["handle", "pass", "refuse", "reApply", 'export'], Enum = {

    CREATED: '代办理',

    PASS: '待审批',

    REJECT: '驳回',

    REFUSE: '拒绝',

  };

  const statisticFormat = val => val.map((t, idx) => <span key={idx} style={{marginRight: '5px'}}>{t.total}</span>)

  const columns = [

    { title: '姓名', dataIndex: 'name', key: 'name', type: 'Link', url: 'https://www.baidu.com' },

    { title: '年龄', dataIndex: 'age', key: 'age' },

    { title: '状态', dataIndex: 'status', key: 'status', type: 'Enum', Enum, },

    { title: '预警统计', dataIndex: 'statistic', key: 'statistic', type: 'Format', format: statisticFormat },

    { title: '存款', dataIndex: 'money', key: 'money', type: 'Currency' },

    { title: '日期', dataIndex: 'date', key: 'date', type: 'Date'},

    { title: '操作', dataIndex: 'action', key: 'action', type: 'Action', value: [

      {label: "查看", click: data => {console.log(data)}},

      {label: "办理", click: data => {}, filter: ({status}) => status == 'CREATED' && permission.some(n => n == 'handle')},

      {label: "通过", click: data => {}, filter: ({status}) => status == 'PASS' && permission.some(n => n == 'pass')},

      {label: "驳回", click: data => {}, filter: ({status}) => status == 'REJECT' && permission.some(n => n == 'reject')},

      {label: "拒绝", click: data => {}, filter: ({status}) => status == 'CREATED' && permission.some(n => n == 'refuse')},

      {label: "重新付款", click: data => {}, filter: ({status}) => status == 'REAPPLY' && permission.some(n => n == 'reApply')},

    ]},

  ]

  const dataSource = [

    {key: 1, name: '小坏', age: 20, status: 'CREATED', date: 1596791666000, statistic: [{level: 3, total: 5}, {level: 2, total: 7}, {level: 1, total: 20}, {level: 0, total: 0}], money: 200000000000},

    {key: 2, name: 'tnnyang', age: 18, status: 'PASS', date: 1596791666000, statistic: [],  money: 2568912357893},

    {key: 3, name: 'xiaohuai', status: 'REJECT', statistic: [], money: 6235871},

    {key: 4, name: '陈公子', age: 21, status: 'REAPPLY', date: 1596791666000, statistic: []},

  ]

  const config = {

    columns,

    dataSource,

    hasCheck: true,  //是否显示表格第一列的checkbox复选框

    getCheckboxProps: record => {return {disabled: record.status == 'REJECT'}}  //table表格rowSelection的禁用

  }

  //点击获取通过checkbox复选框选中的表格

  const childRef = useRef();

  const getTableChecked = () => {

    const selectedRowKeys = childRef.current.getSelectedRowKeys(), selectedRows = childRef.current.getSelectedRows();

    console.log(selectedRowKeys)

    console.log(selectedRows)

  }

  return <div>

    <Table {...config} cRef={childRef} />

    <Button type="primary" onClick={getTableChecked}>获取选择的列表项</Button>

  </div>

}

export default TableDemo

最后再贴一下本次封装所用到的各个包的版本:

react: 16.8.6,

react-dom: 16.8.6,

react-router-dom: 5.0.0,

antd: 4.3.5,

@babel/core: 7.4.4,

babel-loader: 8.0.5

其实最主要的是react和antd的版本,其中antd4和antd3在table组件上的差异还是很大的,在其他组件上的差异也是很大的。

------------------------------------ 分割线 -----------------------------------

有大佬在我另一篇博文封装react antd的form表单组件的评论区提出关于合并表格的单元格功能等,我一想,合并单元格也是我们在使用表格组件时可能会经常遇到的需求,那么就在之前封装的基础上加上合并单元格的实现吧。

在看了antd的表格组件的实例后觉得合并单元格其实也很简单,是基于单元格的值和索引来实现的,那么在我们之前封装的基础上再套一层合并单元格的函数就可以了。

带有合并单元格功能的table封装组件的完整代码如下:

import React, { createElement, useState, useImperativeHandle } from 'react'

import PropTypes from 'prop-types'

import { Link } from "react-router-dom"

import { Table } from 'antd';

import { timestampToTime, currency } from '@/utils'

const h = createElement;

const TableComp = ({columns, dataSource, hasCheck, cRef, getCheckboxProps, bordered}) => {

  const empty = '-',

    [selectedRowKeys, setSelectedRowKeys ] = useState([]),

    [selectedRows, setSelectedRows] = useState([]),

    render = {

      Default: v => v,

      Enum: (v, {Enum}) => {

        if(!Enum[v]) return empty

        return Enum[v]

      },

      Format: (v, {format}, row) => format(v, row),

      Currency: v => currency(v),

      Date: v => timestampToTime(v, 'second'),

      Link: (v, {url}) => <Link to={url}>{v}</Link>,

      Action: (v, {value}, row) => {

        const result = value.filter(n => {

          const {filter = () => true} = n

          return filter(row)

        })

        return result.map(t => <span className="table-link" onClick={() => t.click(row)} key={t.label}>{t.label}</span>)

      },

    }

  columns = columns.map(n => {

    const { type = 'Default', merge } = n;

    return {...n, render: (v, row, index) => (!v || v.length < 1) && type != 'Action' ? empty : merge && typeof merge == 'function' ? merge(render[type](v, n, row), index) : render[type](v, n, row)}

  })

  //父组件获取selectedRowKeys的方法-cRef就是父组件传过来的ref

  useImperativeHandle(cRef, () => ({

    //getSelectedRowKeys就是暴露给父组件的方法

    getSelectedRowKeys: () => selectedRowKeys,

    getSelectedRows: () => selectedRows

  }));

  const onSelectChange = (selectedRowKeys, selectedRows) => {

    setSelectedRowKeys(selectedRowKeys);

    setSelectedRows(selectedRows);

  }

  const rowSelection = {

    selectedRowKeys,

    onChange: onSelectChange,

    getCheckboxProps: record => getCheckboxProps(record)

  };

  return hasCheck ? h(Table, {columns, dataSource, rowSelection, bordered}) : h(Table, {columns, dataSource, bordered})

}

Components.propTypes = {

  columns: PropTypes.array.isRequired,    //表格头部

  dataSource: PropTypes.array.isRequired,  //表格数据

  onChange: PropTypes.func,                //表格翻页函数

  hasCheck: PropTypes.bool,                //表格行是否可选择

  cRef: PropTypes.object,                  //父组件传过来的获取组件实例对象或者是DOM对象

  getCheckboxProps: PropTypes.func,        //选择框的默认属性配置

}

export default TableComp

使用方法完整代码:

import React, { useRef } from 'react'

import { Button } from 'antd'

import Table from './mergeTable'

import './demo.css'

const TableDemo = () => {

  const permission = ["handle", "pass", "refuse", "reApply", 'export'], Enum = {

    CREATED: '代办理',

    PASS: '待审批',

    REJECT: '驳回',

    REFUSE: '拒绝',

  };

  const statisticFormat = val => val.map((t, idx) => <span key={idx} style={{marginRight: '5px'}}>{t.total}</span>)

  const columns = [

    { title: '姓名', dataIndex: 'name', key: 'name', type: 'Link', url: 'https://www.baidu.com' },

    { title: '身份证号/年龄', dataIndex: 'ID', key: 'ID', colSpan: 2, },

    { title: '年龄', dataIndex: 'age', key: 'age', colSpan: 0, },

    { title: '状态', dataIndex: 'status', key: 'status', type: 'Enum', Enum, merge: (v, index) => {

      const obj = {

        children: v,

        props: {},

      };

      if (index != 2) {

        return v;

      }

      //从第二行开始合并,合并两列 - index从0开始,colSpan代表合并行。

      if (index === 2) {

        obj.props.colSpan = 2;

      }

      return obj;

    }},

    { title: '规则', dataIndex: 'rule', key: 'rule', merge: (v, index) => {

      const obj = {

        children: v,

        props: {},

      };

      //从第二行开始合并,合并两列 - index从0开始,colSpan代表合并行。

      if (index === 2) {

        obj.props.colSpan = 0;

      }

      return obj;

    }},

    { title: '预警统计', dataIndex: 'statistic', key: 'statistic', type: 'Format', format: statisticFormat },

    { title: '存款', dataIndex: 'money', key: 'money', type: 'Currency', merge: (v, index) => {

      const obj = {

        children: v,

        props: {},

      };

      //从第二行开始合并,合并两行 - index从0开始,rowSpan代表合并列,rowSpan = 2代表合并两列,即将存款列的第二行和第三行合并

      if (index === 1) {

        obj.props.rowSpan = 2;

      }

      //colSpan或者rowSpan设值为0时,设置的表格不会渲染,即表格数据不会展示出来。这段代码的意思是第三行的数据不展示并将第三行合并到第二行

      if (index === 2) {

        obj.props.rowSpan = 0;

      }

      return obj;

    }},

    { title: '日期', dataIndex: 'date', key: 'date', type: 'Date'},

    { title: '操作', dataIndex: 'action', key: 'action', type: 'Action', value: [

      {label: "查看", click: data => {console.log(data)}},

      {label: "办理", click: data => {}, filter: ({status}) => status == 'CREATED' && permission.some(n => n == 'handle')},

      {label: "通过", click: data => {}, filter: ({status}) => status == 'PASS' && permission.some(n => n == 'pass')},

      {label: "驳回", click: data => {}, filter: ({status}) => status == 'REJECT' && permission.some(n => n == 'reject')},

      {label: "拒绝", click: data => {}, filter: ({status}) => status == 'CREATED' && permission.some(n => n == 'refuse')},

      {label: "重新付款", click: data => {}, filter: ({status}) => status == 'REAPPLY' && permission.some(n => n == 'reApply')},

    ]},

  ]

  const dataSource = [

    {key: 1, name: '小坏', ID: 'abc', age: 20, status: 'CREATED', rule: '标准', date: 1596791666000, statistic: [{level: 3, total: 5}, {level: 2, total: 7}, {level: 1, total: 20}, {level: 0, total: 0}], money: 200000000000},

    {key: 2, name: 'tnnyang', ID: 'def', age: 18, status: 'PASS', rule: '个性化', date: 1596791666000, statistic: [],  money: 2568912357893},

    {key: 3, name: 'xiaohuai', ID: 'ghi', status: 'REJECT', rule: '标准', statistic: [], money: 6235871},

    {key: 4, name: '陈公子', ID: 'jkl', age: 21, status: 'REAPPLY', rule: '个性化', date: 1596791666000, statistic: []},

  ]

  const config = {

    columns,

    dataSource,

    getCheckboxProps: record => {return {disabled: record.status == 'REJECT'}},  //table表格rowSelection的禁用

    bordered: true,

  }

  //点击获取通过checkbox复选框选中的表格

  const childRef = useRef();

  const getTableChecked = () => {

    const selectedRowKeys = childRef.current.getSelectedRowKeys(), selectedRows = childRef.current.getSelectedRows();

    console.log(selectedRowKeys)

    console.log(selectedRows)

  }

  return <div style={{margin: '20px'}}>

    <Table {...config} cRef={childRef} />

    <Button type="primary" onClick={getTableChecked}>获取选择的列表项</Button>

  </div>

}

export default TableDemo


转自:https://www.cnblogs.com/tnnyang/p/13491868.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容