摘要:下的表格狂想曲前言歡迎大家閱讀從零開(kāi)始的組件開(kāi)發(fā)之路系列第一篇,表格篇。北京小李中的每一個(gè)元素是一列的配置,也是一個(gè)對(duì)象,至少應(yīng)該包括如下幾部分表頭該列使用行中的哪個(gè)進(jìn)行顯示易用性與通用性的平衡易用性與通用性互相制衡,但并不是絕對(duì)矛盾。
React 下的表格狂想曲 0. 前言
歡迎大家閱讀「從零開(kāi)始的 React 組件開(kāi)發(fā)之路」系列第一篇,表格篇。本系列的特色是從 需求分析、API 設(shè)計(jì)和代碼設(shè)計(jì) 三個(gè)遞進(jìn)的過(guò)程中,由簡(jiǎn)到繁地開(kāi)發(fā)一個(gè) React 組件,并在講解過(guò)程中穿插一些 React 組件開(kāi)發(fā)的技巧和心得。
為什么從表格開(kāi)始呢?在企業(yè)系統(tǒng)中,表格是最常見(jiàn)但功能需求最豐富的組件之一,同時(shí)也是基于 React 數(shù)據(jù)驅(qū)動(dòng)的思想受益最多的組件之一,十分具有代表性。這篇文章也是近期南京谷歌開(kāi)發(fā)者大會(huì)前端專(zhuān)場(chǎng)的分享總結(jié)。UXCore table 組件 Demo 也可以和本文行文思路相契合,可做參考。
1. 一個(gè)簡(jiǎn)單 React 表格的構(gòu)造 1.1 需求分析有表頭,每行的展示方式相同,只是數(shù)據(jù)上有所不同
每一列可能有不同的對(duì)齊方式,可能有不同的展示類(lèi)型,比如金額,比如手機(jī)號(hào)碼等
1.2 API 設(shè)計(jì)因?yàn)槊恳涣械恼故绢?lèi)型不同,因此列配置應(yīng)該作為一個(gè) Prop,由于有多列應(yīng)該是一個(gè)數(shù)組
數(shù)據(jù)源應(yīng)該作為基礎(chǔ)配置之一,應(yīng)該作為一個(gè) prop,由于有多行也應(yīng)該是一個(gè)數(shù)組
現(xiàn)在的樣子:
基本思路是通過(guò)遍歷列配置來(lái)生成每一行
data 中的每一個(gè)元素應(yīng)該是一行的數(shù)據(jù),是一個(gè) hash 對(duì)象。
{ city: "北京", name: "小李" }
columns 中的每一個(gè)元素是一列的配置,也是一個(gè) hash 對(duì)象,至少應(yīng)該包括如下幾部分:
{ title: "表頭", dataKey: "city", // 該列使用行中的哪個(gè) key 進(jìn)行顯示 }
易用性與通用性的平衡
易用性與通用性互相制衡,但并不是絕對(duì)矛盾。
何為易用?使用盡量少的配置來(lái)完成最典型的場(chǎng)景。
何為通用?提供盡量多的定制接口已適應(yīng)各種不同場(chǎng)景。
在 API 設(shè)計(jì)上盡量開(kāi)放保證通用性
在默認(rèn)值上提煉最典型的場(chǎng)景提高易用性。
從易用性角度出發(fā)
{ align: "left", // 默認(rèn)左對(duì)齊 type: "money/action", // 提供 "money", "card", "cnmobile" 等常用格式化形式 delimiter: ",", // 格式化時(shí)的分隔符,默認(rèn)是空格 actions: { // 表格中常見(jiàn)的操作列,不以數(shù)據(jù)進(jìn)行渲染,只包含動(dòng)作,hash 對(duì)象使配置最簡(jiǎn)化 "編輯": function() {doEdit();} }, }
從通用性角度出發(fā)
{
actions: [ // 相對(duì)繁瑣,但定制能力更強(qiáng)
{
title: "編輯",
callback: function() {doEdit();},
render: function(rowData) {
// 根據(jù)當(dāng)前行數(shù)據(jù),決定是否渲染,及渲染成定制的樣子
}
}
],
render: function(cellData, rowData) {
// 根據(jù)當(dāng)前行數(shù)據(jù),完全由用戶(hù)決定如何渲染
return {`${rowData.city} - ${rowData.name}`}
}
}
提供定制化渲染的兩種方式:
渲染函數(shù) (更推薦)
{ render: function(rowData) { return}, }
渲染組件
{ renderComp:, // 內(nèi)部接收 rowData 作為參數(shù) }
推薦渲染函數(shù)的原因:
函數(shù)在做屬性比較時(shí),更簡(jiǎn)單
約定更少,渲染組件的方式需要配合 Table 預(yù)留比如 rowData 一類(lèi)的接口,不夠靈活。
1.3 代碼設(shè)計(jì)圖:Table 的分層設(shè)計(jì)
2. 加入更多的內(nèi)置功能圖:最初的 Table 結(jié)構(gòu),詳細(xì)的分層為后續(xù)的功能擴(kuò)展做好準(zhǔn)備。
2.1 需求分析目前的表格可以滿(mǎn)足我們的最簡(jiǎn)單常用的場(chǎng)景,但仍然有很多經(jīng)常需要使用的功能沒(méi)有支持,如列排序,分頁(yè),搜索過(guò)濾、常用動(dòng)作條、行選擇和行篩選等。
列排序:升序/降序/默認(rèn)順序 Head/Cell 相關(guān)
分頁(yè):當(dāng)表格需要展示的條數(shù)很多時(shí),分頁(yè)展示固定的條數(shù) Table/Pagination 相關(guān),這里假設(shè)已有 Pagination 組件
搜索過(guò)濾:Table 相關(guān)
常用操作:Table 相關(guān)
行選擇:選中某些行,Row/Cell 相關(guān)
行篩選:手動(dòng)展示或者隱藏一些行,不屬于任何一列,因此是 Table 級(jí)
2.2 API 設(shè)計(jì)根據(jù)上面對(duì)于功能的需求分析,我們很容易定位 API 的位置,完成相應(yīng)的擴(kuò)展。
// table 配置,需求對(duì)應(yīng)的模塊對(duì)應(yīng)了他的配置在整個(gè)配置中的位置 { columns: [ // HEAD/ROW 相關(guān) { order: true, // 是否展示排序按鈕 hidden: false, // 是否隱藏,行篩選需要 } ], onOrder: function (activeColumn, order) { // 排序時(shí)的回調(diào) doOrder(activeColumn, order) }, actionBar: { // 常用操作條 "打印": function() {doPrint()}, }, showSeach: true, // 是否顯示搜索過(guò)濾,為什么不直接用下面的,這里也是設(shè)計(jì)上的一個(gè)優(yōu)化點(diǎn) onSearch: function(keyword) { doSearch(keyword) }, // 搜索時(shí)的回調(diào) showPager: true, // 是否顯示分頁(yè) onPagerChange: function(current, pageSize) {}, // 分頁(yè)改變時(shí)的回調(diào) rowSelection: { // 行選擇相關(guān) onSelect: function(isSelected, currentRow, selectedRows) { doSelect() } } } // data 結(jié)構(gòu) { data: [{ city: "xxx", name: "xxx", __selected__: true, // 行選擇相關(guān),用以標(biāo)記該行是否被選中,用前后的 __ 來(lái)做特殊標(biāo)記,另一方面也盡可能避免與用戶(hù)的字段重復(fù) }], currentPage: 1, // 當(dāng)前頁(yè)數(shù) totalCount: 50, // 總條數(shù) }2.3 代碼設(shè)計(jì) 結(jié)構(gòu)圖
內(nèi)部數(shù)據(jù)的處理圖:擴(kuò)展后的 Table 結(jié)構(gòu)
何時(shí)該用 state?何時(shí)該用 props?目前組件的數(shù)據(jù)流向還比較簡(jiǎn)單,我們似乎可以全部通過(guò) props 來(lái)控制狀態(tài),制作一個(gè) stateless 的組件。
UI=fn(state, props), 人們常說(shuō) React 組件是一個(gè)狀態(tài)機(jī),但我們應(yīng)該清楚的是他是由 state 和 props 構(gòu)成的雙狀態(tài)機(jī);
props 和 state 的改變都會(huì)觸發(fā)組件的重新渲染,那么我們使用它們的時(shí)機(jī)分別是什么呢?由于 state 是組件自身維護(hù)的,并不與他的父級(jí)組件進(jìn)行溝通,進(jìn)而也無(wú)法與他的兄弟組件進(jìn)行溝通,因此我們應(yīng)該盡量只在頁(yè)面的根節(jié)點(diǎn)組件或者復(fù)雜組件的根節(jié)點(diǎn)組件使用 state,而在其他情況下盡量只使用 props,這可以增強(qiáng)整個(gè) React 項(xiàng)目的可預(yù)知性和可控性。
但凡事不是絕對(duì)的,全都使用 Props 固然可以使組件可維護(hù)性變強(qiáng),但全部交給用戶(hù)來(lái)操作會(huì)使用戶(hù)的使用成本大大提高,利用 state,我們可以讓組件自己維護(hù)一些狀態(tài),從而減輕用戶(hù)使用的負(fù)擔(dān)。
我們舉個(gè)簡(jiǎn)單的例子
{/* 受控模式 */} {/* 非受控模式 */}
value 配置時(shí),input 的值由 value 控制,value 沒(méi)有配置時(shí),input 的值由自己控制,如果把 看做一個(gè)組件,那么此時(shí)可以認(rèn)為 input 此時(shí)有一個(gè) state 是 value。顯然,無(wú) value 狀態(tài)下的配置更少,降低了使用的成本,我們?cè)谧鼋M件時(shí)也可以參考這種模式。
例如在我們希望為用戶(hù)提供 行選擇 的功能時(shí),用戶(hù)通常是不希望自己去控制行的變化的,而只是關(guān)心行的變化時(shí)應(yīng)該拿取的數(shù)據(jù),此時(shí)我們就可以將 data 這個(gè) prop 變成 state。有一點(diǎn)需要注意的是,用戶(hù)的 prop
class Table extends React.Component { constructor(props) { super(props); this.data = deepcopy(props.data); this.state = { data: this.data, }; } /** * 在 data 發(fā)生改變時(shí),更改對(duì)應(yīng)的 state 值。 */ componentWillReceiveProps(nextProps, nextState) { if (!deepEqual(nextProps.data, this.data) { this.data = deepcopy(nextProps.data); this.setState({ data: this.data, }); } } }
這里涉及的一個(gè)很重要的點(diǎn),就是如何處理一個(gè)復(fù)雜類(lèi)型數(shù)據(jù)的 prop 作為 state。因?yàn)?JS 對(duì)象傳地址的特性,如果我們直接對(duì)比 nextProps.data 和 this.props.data 有些情況下會(huì)永遠(yuǎn)相等(當(dāng)用戶(hù)直接修改 data 的情況下),所以我們需要對(duì)這個(gè) prop 做一個(gè)備份。
生命周期的使用時(shí)機(jī)圖:React 的生命周期
constructor: 盡量簡(jiǎn)潔,只做最基本的 state 初始化
willMount: 一些內(nèi)部使用變量的初始化
render: 觸發(fā)非常頻繁,盡量只做渲染相關(guān)的事情。
didMount: 一些不影響初始化的操作應(yīng)該在這里完成,比如根據(jù)瀏覽器不同進(jìn)行操作,獲取數(shù)據(jù),監(jiān)聽(tīng) document 事件等(server render)。
willUnmount: 銷(xiāo)毀操作,銷(xiāo)毀計(jì)時(shí)器,銷(xiāo)毀自己的事件監(jiān)聽(tīng)等。
willReceiveProps: 當(dāng)有 prop 做 state 時(shí),監(jiān)聽(tīng) prop 的變化去改變 state,在這個(gè)生命周期里 setState 不會(huì)觸發(fā)兩次渲染。
shouldComponentUpdate: 手動(dòng)判斷組件是否應(yīng)該更新,避免因?yàn)轫?yè)面更新造成的無(wú)謂更新,組件的重要優(yōu)化點(diǎn)之一。
willUpdate: 在 state 變化后如果需要修改一些變量,可以在這里執(zhí)行。
didUpdate: 與 didMount 類(lèi)似,進(jìn)行一些不影響到 render 的操作,update 相關(guān)的生命周期里最好不要做 setState 操作,否則容易造成死循環(huán)。
父子級(jí)組件間的通信父級(jí)向子級(jí)通信不用多說(shuō),使用 prop 進(jìn)行傳遞,那么子級(jí)向父級(jí)通信呢?有人會(huì)說(shuō),靠回調(diào)啊~ onChange等等,本質(zhì)上是沒(méi)有錯(cuò)誤的,但當(dāng)組件比較復(fù)雜,存在多級(jí)結(jié)構(gòu)時(shí),如果每一級(jí)都去處理他的子級(jí)的回調(diào)的話(huà),不僅寫(xiě)起來(lái)非常麻煩,而且很多時(shí)候是沒(méi)有意義的。
我們采取的辦法是,只在頂級(jí)組件也就是 Table 這一層控制所有的 state,其他的各個(gè)子層都是完全由 prop 來(lái)控制,這樣一來(lái),我們只需要 Table 去操作數(shù)據(jù),那么我們逐級(jí)向下傳遞一個(gè)屬于 Table 的回調(diào)函數(shù),完成所有子級(jí)都只向 Table 做“匯報(bào)”,進(jìn)行跨級(jí)通信。
3. 自行獲取數(shù)據(jù) 3.1 需求分析圖:父子級(jí)間的通信
作為一個(gè)盡可能為用戶(hù)提高效率的組件,除了手動(dòng)傳入 data 外,我們也應(yīng)該有自行獲取數(shù)據(jù)的能力,用戶(hù)只需要配置 url 和相應(yīng)的參數(shù)就可以完成表格的配置,為此我們可能需要以下參數(shù):
數(shù)據(jù)源,返回的數(shù)據(jù)格式應(yīng)和我們之前定義的 data 數(shù)據(jù)結(jié)構(gòu)一致。 (易用)
隨請(qǐng)求一起發(fā)出去的參數(shù)。(通用)
在發(fā)請(qǐng)求前的回調(diào),可以在這里調(diào)整發(fā)送的參數(shù)。(通用)
請(qǐng)求回來(lái)后的回調(diào),可以在這里調(diào)整數(shù)據(jù)結(jié)構(gòu)以滿(mǎn)足對(duì) data 的要求。(通用)
同時(shí)要考慮到內(nèi)置功能的適配。(易用)
3.2 API 設(shè)計(jì)// table 配置,需求對(duì)應(yīng)的模塊對(duì)應(yīng)了他的配置在整個(gè)配置中的位置 { url: "http://fetchurl.com/data", // 數(shù)據(jù)源,只支持 json 和 jsonp fetchParams: { // 額外的一些參數(shù) token: "xxxabxc_sa" }, beforeFetch: function(data, from) { // data 為要發(fā)送的參數(shù),from 參數(shù)用來(lái)區(qū)分發(fā)起 fetch 的來(lái)源(分頁(yè),排序,搜索還是其他位置) return data; // 返回值為真正發(fā)送的參數(shù) }, afterFetch: function(result) { // result 為請(qǐng)求回來(lái)的數(shù)據(jù) return process(result); // 返回值為真正交給 table 進(jìn)行展示的數(shù)據(jù)。 }, }3.3 代碼設(shè)計(jì)
基于前面良好的通信模式,url 的擴(kuò)展變得非常簡(jiǎn)單,只需要在所有的回調(diào)中加入是否配置 url 的判斷即可。
class Table extends React.Component { constructor(props) { super(props); this.data = deepcopy(props.data); this.fetchParams = deepcopy(props.fetchParams); this.state = { data: this.data, }; } /** * 獲取數(shù)據(jù)的方法 */ fetchData(props, from) { props = props || this.props; const otherParams = process(this.state); ajax(props.url, this.fetchParams, otherParams, from); } /** * 搜索時(shí)的回調(diào) */ handleSearch(key) { if (this.props.url) { this.setState({ searchKey: key, }, () => { this.fetchData(); }); } else { this.props.onSearch(key); } } componentDidMount() { if (this.props.url) { this.fetchData(); } } componentWillReceiveProps(nextProps, nextState) { let newState = {}; if (!deepEqual(nextProps.data, this.data) { this.data = deepcopy(nextProps.data); newState["data"] = this.data; } if (!deepEqual(nextProps.fetchParams, this.fetchParams)) { this.fetchParams = deepcopy(nextProps.fetchParams); this.fetchData(); } if (nextProps.url !== this.props.url) { this.fetchData(nextProps); } if (Object.keys(newState) !== 0) { this.setState(newState); } } }4. 行內(nèi)編輯 4.1 需求分析
通過(guò)雙擊或者點(diǎn)擊編輯按鈕,實(shí)現(xiàn)行內(nèi)可編輯狀態(tài)的切換。如果只是變成普通的文本框那就太 low 了,有追求的我們希望每個(gè)列根據(jù)數(shù)據(jù)類(lèi)型可以有不同的編輯形式。既然是可編輯的,那么關(guān)于表單的一套東西都適用,他要可以驗(yàn)證,可以重置,也可以聯(lián)動(dòng)。
4.2 API 設(shè)計(jì)// table 配置,需求對(duì)應(yīng)的模塊對(duì)應(yīng)了他的配置在整個(gè)配置中的位置,顯然行內(nèi)編輯是和列相關(guān)的 { columns: [ // HEAD/ROW 相關(guān) { dataKey: "cityName", // 展示時(shí)操作的變量 editKey: "cityValue", // 編輯時(shí)操作的變量 customField: SelectField, // 編輯狀態(tài)的類(lèi)型 config: {}, // 編輯狀態(tài)的一些配置 renderChildren: function() { return [ {id: "bj", name: "北京"}, {id: "hz", name: "杭州"}].map((item) => { return }); }, rules: function(value) { // 校驗(yàn)相關(guān) return true; } } ], onChange: function(result) { doSth(result); // result 包括 {data: 表格的所有數(shù)據(jù), changedData: 變動(dòng)行的數(shù)據(jù), dataKey: xxx, editKey: xxx, pass: 正在編輯的域是否通過(guò)校驗(yàn)} } }
// data 結(jié)構(gòu) { data: [{ cityName: "xxx", cityValue: "yyy", name: "xxx", __selected__: true, __mode__: "edit", // 用來(lái)區(qū)分當(dāng)前行的狀態(tài) }], currentPage: 1, // 當(dāng)前頁(yè)數(shù) totalCount: 50, // 總條數(shù) }4.3 代碼設(shè)計(jì)
圖:行內(nèi)編輯模式下的表格架構(gòu)
所有的 CellField 繼承一個(gè)基類(lèi) Field,這個(gè)基類(lèi)提供通用的與 Table 通信,校驗(yàn)的方式,具體的 Field 只負(fù)責(zé)交互部分的實(shí)現(xiàn)。
下面是這部分設(shè)計(jì)的具體代碼實(shí)現(xiàn),礙于篇幅,不在文章中直接貼出。
https://github.com/uxcore/uxc...
https://github.com/uxcore/uxc...
5. 總結(jié)這篇文章以復(fù)雜表格組件的開(kāi)發(fā)為切入點(diǎn),討論了以下內(nèi)容:
組件設(shè)計(jì)的通用流程
組件分層架構(gòu)與 API 的對(duì)應(yīng)設(shè)計(jì)
組件設(shè)計(jì)中易用性與通用性的權(quán)衡
State 和 Props 的正確使用
生命周期的實(shí)戰(zhàn)應(yīng)用
父子級(jí)間組件通信
礙于整體篇幅,有一些和這個(gè)組件相關(guān)的點(diǎn)未詳細(xì)討論,我們會(huì)在本系列的后續(xù)文章中詳細(xì)說(shuō)明。
數(shù)據(jù)的 不可變性(immutability)
shouldComponentUpdate 和 pure render
樹(shù)形表格 和 數(shù)據(jù)的遞歸處理
在目前架構(gòu)上進(jìn)行折疊面板的擴(kuò)展
最后慣例地來(lái)宣傳一下團(tuán)隊(duì)開(kāi)源的 React PC 組件庫(kù) UXCore ,上面提到的點(diǎn),在我們的組件開(kāi)發(fā)工具中都有體現(xiàn),歡迎大家一起討論,也歡迎在我們的 SegmentFault 專(zhuān)題下進(jìn)行提問(wèn)討論。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/86391.html
某熊的技術(shù)之路指北 ? 當(dāng)我們站在技術(shù)之路的原點(diǎn),未來(lái)可能充滿(mǎn)了迷茫,也存在著很多不同的可能;我們可能成為 Web/(大)前端/終端工程師、服務(wù)端架構(gòu)工程師、測(cè)試/運(yùn)維/安全工程師等質(zhì)量保障、可用性保障相關(guān)的工程師、大數(shù)據(jù)/云計(jì)算/虛擬化工程師、算法工程師、產(chǎn)品經(jīng)理等等某個(gè)或者某幾個(gè)角色。某熊的技術(shù)之路系列文章/書(shū)籍/視頻/代碼即是筆者蹣跚行進(jìn)于這條路上的點(diǎn)滴印記,包含了筆者作為程序員的技術(shù)視野、...
摘要:架構(gòu)都是演變出來(lái)的,沒(méi)有最好的架構(gòu),只有最合適的架構(gòu)最近,滴滴出行平臺(tái)產(chǎn)品中心技術(shù)負(fù)責(zé)人李賢輝接受了的采訪(fǎng),闡述了滴滴的客戶(hù)端架構(gòu)模式與演變過(guò)程。李賢輝也是移動(dòng)開(kāi)發(fā)精英俱樂(lè)部中的一員,所以本期重點(diǎn)推薦了這篇文章。 「架構(gòu)都是演變出來(lái)的,沒(méi)有最好的架構(gòu),只有最合適的架構(gòu)!」最近,滴滴出行平臺(tái)產(chǎn)品中心 iOS 技術(shù)負(fù)責(zé)人李賢輝接受了 infoQ 的采訪(fǎng),闡述了滴滴的 iOS 客戶(hù)端架構(gòu)模式...
閱讀 720·2021-11-24 10:30
閱讀 1254·2021-09-24 09:48
閱讀 3074·2021-09-24 09:47
閱讀 3588·2019-08-29 17:11
閱讀 2875·2019-08-29 15:38
閱讀 2270·2019-08-29 11:03
閱讀 3594·2019-08-26 12:15
閱讀 1008·2019-08-26 10:45