React 实现 Table 的思考
Table 是最常用展示数据的方式之一,可是一个产品中往往很多非常类似的 Table,但是我们碰到的情况往往是 Table A 要排序,Table B 不需要排序,等等这种看起来非常类似,但是又不完全相同的表格。这种情况下,到底要不要抽取一个公共的 Table 组件呢?对于这个问题,我们团队也纠结了很久,先后开发了多个版本的 Table 组件,在最近的一个项目中,产出了第三版 Table 组件,能够较好的解决灵活性和公共逻辑抽取的问题。本文将会详细的讲述这种 Table 组件解决方案产出的过程和一些思考。 Table 的常见实现首先我们看到的是不使用任何组件实现一个业务表格的代码: import React,{ Component } from 'react'; const columnOpts = [ { key: 'a',name: 'col-a' },{ key: 'b',name: 'col-b' },]; function SomeTable(props) { const { data } = props; return ( <div className="some-table"> <ul className="table-header"> { columnOpts.map((opt,colIndex) => ( <li key={`col-${colIndex}`}>{opt.name}</li> )) } </ul> <ul className="table-body"> { data.map((entry,rowIndex) => ( <li key={`row-${rowIndex}`}> { columnOpts.map((opt,colIndex) => ( <span key={`col-${colIndex}`}>{entry[opt.key]}</span> )) } </li> )) } </ul> </div> ); } 这种实现方法带来的问题是:
抽象过程组件是对数据和方法的一种封装,在封装之前,我们总结了一下表格型的展示的特点:
基于以上特点,我们希望 Table 组件能够满足以下条件:
至此,我们首先想到 Table 组件应该长成这样的: const columnOpts = [ { key: 'a',name: 'col-a',onRenderTd: () => {} },name: 'col-b',onRenderTh: () => {},]; <Table data={data} columnOpts={columnOpts} /> 其中 到这里我们发现对于稍微复杂一点的 table, <Table data={data}> <Column dataKey="a" name="col-a" td={onRenderTd} /> <Column dataKey="b" name="col-b" td={onRenderTd} th={onRenderTh} /> </Table> 这样大家就可以像写HTML一样把一个简单的表格给搭建出来了。 优化有了 Table 的雏形,再联系下写表格的常见需求,我们给 Column 添加了 import React,{ PropTypes,Component } from 'react'; const propTypes = { name: PropTypes.string,dataKey: PropTypes.string.isRequired,align: PropTypes.oneOf(['left','center','right']),width: PropTypes.oneOfType([PropTypes.number,PropTypes.string]),th: PropTypes.oneOfType([PropTypes.element,PropTypes.func]),td: PropTypes.oneOfType([ PropTypes.element,PropTypes.func,PropTypes.oneOf([ 'int','float','percent','changeRate' ]) ]),}; const defaultProps = { align: 'left',}; function Column() { return null; } Column.propTypes = propTypes; Column.defaultProps = defaultProps; export default Column; 代码中可以发现
下面我们看一下 Table 的实现: const getDisplayName = (el) => { return el && el.type && (el.type.displayName || el.type.name); }; const renderChangeRate = (changeRate) => { ... }; const renderThs = (columns) => { return columns.map((col,index) => { const { name,dataKey,th } = col.props; const props = { name,colIndex: index }; let content; let className; if (React.isValidElement(th)) { content = React.cloneElement(th,props); className = getDisplayName(th); } else if (_.isFunction(th)) { content = th(props); } else { content = name || ''; } return ( <th key={`th-${index}`} style={getStyle(col.props)} className={`table-th col-${index} col-${dataKey} ${className || ''}`} > {content} </th> ); }); }; const renderTds = (data,entry,columns,rowIndex) => { return columns.map((col,index) => { const { dataKey,td } = col.props; const value = getValueOfTd(entry,dataKey); const props = { data,rowData: entry,tdValue: value,rowIndex,colIndex: index }; let content; let className; if (React.isValidElement(td)) { content = React.cloneElement(td,props); className = getDisplayName(td); } else if (td === 'changeRate') { content = renderChangeRate(value || ''); } else if (_.isFunction(td)) { content = td(props); } else { content = formatIndex(parseValueOfTd(value),td); } return ( <td key={`td-${index}`} style={getStyle(col.props)} className={`table-td col-${index} col-${dataKey} ${className || ''}`} > {content} </td> ); }); }; const renderRows = (data,columns) => { if (!data || !data.length) {return null;} return data.map((entry,index) => { return ( <tr className="table-tbody-tr" key={`tr-${index}`}> {renderTds(data,index)} </tr> ); }); }; function Table(props) { const { children,data,className } = props; const columns = findChildrenByType(children,Column); return ( <div className={`table-container ${className || ''}`}> <table className="base-table"> {hasNames(columns) && ( <thead> <tr className="table-thead-tr"> {renderThs(columns)} </tr> </thead> )} <tbody>{renderRows(data,columns)}</tbody> </table> </div> ); } 代码说明了一切,就不再详细说了。当然,在业务组件里,还可以加上公共的错误处理逻辑。 单元格示例前面提到我们的 class SortableTh extends Component { static displayName = 'SortableTh'; static propTypes = { ...,initialOrder: PropTypes.oneOf(['asc','desc']),order: PropTypes.oneOf(['asc','desc','none']).isRequired,onChange: PropTypes.func.isRequired,}; static defaultProps = { order: 'none',initialOrder: 'desc',}; onClick = () => { const { onChange,initialOrder,order,dataKey } = this.props; if (dataKey) { let nextOrder = 'none'; if (order === 'none') { nextOrder = initialOrder; } else if (order === 'desc') { nextOrder = 'asc'; } else if (order === 'asc') { nextOrder = 'desc'; } onChange({ orderBy: dataKey,order: nextOrder }); } }; render() { const { name,hasRate,rateType } = this.props; return ( <div className="sortable-th" onClick={this.onClick}> <span>{name}</span> <SortIcon order={order} /> </div> ); } } 通过这个例子可以看到, 总结总结一些自己的感想:
最终,我把这次 Table 组件的经验抽离出来,开源到 https://github.com/recharts/react-smart-table,希望开发者们可以参考。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |