Table 是最經常使用展現數據的方式之一,但是一個產品中每每不少很是相似的 Table,可是咱們碰到的狀況每每是 Table A 要排序,Table B 不須要排序,等等這種看起來很是相似,可是又不徹底相同的表格。這種狀況下,到底要不要抽取一個公共的 Table 組件呢?對於這個問題,咱們團隊也糾結了好久,前後開發了多個版本的 Table 組件,在最近的一個項目中,產出了第三版 Table 組件,可以較好的解決靈活性和公共邏輯抽取的問題。本文將會詳細的講述這種 Table 組件解決方案產出的過程和一些思考。html
首先咱們看到的是不使用任何組件實現一個業務表格的代碼:前端
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> ); }
這種實現方法帶來的問題是:react
每次寫表格須要寫不少佈局類的樣式git
重複代碼不少,並且項目成員之間很難達到統一,A 可能喜歡用表格來佈局,B 可能喜歡用 ul 來佈局github
類似可是不徹底相同的表格很難複用數組
組件是對數據和方法的一種封裝,在封裝以前,咱們總結了一下表格型的展現的特色:前端工程師
輸入數據源較統一,通常爲對象數組echarts
thead 中的單元格大部分只是展現一些名稱,也有一些個性化的內容,如帶有排序 icon 的單元格函數
tbody 中的部分單元格只是簡單的讀取一些值,不少單元格的都有本身的邏輯,可是在一個產品中一般不少相似的單元格佈局
列是有順序的,更適合以列爲單位來添加布局樣式
基於以上特色,咱們但願 Table 組件可以知足如下條件:
接收一個 對象數組 和 全部列的配置 爲參數,自動建立基礎的表格內容
thead 和 tbody 中的單元格都可以定製化,以知足不一樣的需求
至此,咱們首先想到 Table 組件應該長成這樣的:
const columnOpts = [ { key: 'a', name: 'col-a', onRenderTd: () => {} }, { key: 'b', name: 'col-b', onRenderTh: () => {}, onRenderTd: () => {} }, ]; <Table data={data} columnOpts={columnOpts} />
其中 onRenderTd
和 onRenderTh
分別是渲染 td 和 th 時的回調函數。
到這裏咱們發現對於稍微複雜一點的 table,columnOpts
將會是一個很是大的配置數組,咱們有沒有辦法不使用數組來維護這些配置呢?這裏咱們想到的一個辦法是建立一個 Column
的組件,讓你們能夠這麼來寫這個 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 添加了 width
和 align
屬性。加這兩個屬性的緣由很容易想到,由於咱們在寫表格相關業務時,樣式裏面寫的最多的就是單元格的寬度和對齊方式。咱們來看一下 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;
代碼中能夠發現 th
能夠接收兩種格式,一種是 function
,一種是 ReactElement
。這裏提供 ReactElement
類型的 th
主要讓你們可以設置一些額外的 props
,後面咱們會給出一個例子。
td
的類型就更復雜了,不只可以接收 function
和 ReactElement
這兩種類型,還有 int
, float
, percent
, changeRate
這三種類型是最經常使用的數據類型,這樣方便咱們能夠在 Table 裏面根據類型對數據作格式化,省去了項目成員中不少重複的代碼。
下面咱們看一下 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, dataKey, 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, dataKey, 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), dataKey, 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, entry, columns, 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> ); }
代碼說明了一切,就再也不詳細說了。固然,在業務組件裏,還能夠加上公共的錯誤處理邏輯。
前面提到咱們的 td
和 th
還能夠接收 ReactElement
格式的 props
,你們可能還有會有點疑惑,下面咱們看一個 SortableTh
的例子:
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, order, hasRate, rateType } = this.props; return ( <div className="sortable-th" onClick={this.onClick}> <span>{name}</span> <SortIcon order={order} /> </div> ); } }
經過這個例子能夠看到,th
和 td
接收 ReactElement
類型的 props
可以讓外部很好的控制單元格的內容,每一個單元格不僅是接收 data
數據的封閉單元。
總結一些本身的感想:
前端工程師也須要往前走一步,瞭解用戶習慣。在寫這個組件以前,我一直是用 ul 來寫表格的,用 ul 寫的表格調整樣式比較便利,後來發現用戶不少時候喜歡把整個表格裏面的內容 copy 下來用於存檔。然而,ul 寫的表格 copy 後粘貼在 excel 中,整行的內容都在一個單元格里面,用 table 寫的表格則可以幾乎保持本來的格式,因此咱們此次用了原生的 table 來寫表格。
業務代碼中組件抽取的粒度一直是一個比較糾結的問題。粒度太粗,項目成員之間須要寫不少重複的代碼。粒度太細,後續可擴展性又很低,因此只能是你們根據業務特色來評估了。像 Table 這樣的組件很是通用,並且後續確定有新的類型冒出來,因此粒度不宜太細。固然,咱們這樣寫 Table 組件後,你們能夠抽取經常使用的一些 XXXTh
和 XXXTd
。
最終,我把此次 Table 組件的經驗抽離出來,開源到 https://github.com/recharts/react-smart-table,但願開發者們能夠參考。