從宏觀的角度來說,其實只有良好的抽象才能真正提升一個團隊的開發效率,前端
而囿於不一樣產品所面臨的不一樣業務需求,當咱們抽絲剝繭般地將一個個前端工程抽象到最後一層,react
那麼剩下的其實就只有按鈕、輸入框、日曆、對話框、圖標等這些毫無業務含義的 UI 組件了。數據庫
選擇或開發一套適合本身團隊使用的 UI 組件庫應該是每個前端團隊在底層架構達成共識後下一件就要去作的事情,redux
那麼咱們就以今天爲始,分別從如下幾個方面來一塊兒探討如何構建一套優秀的 UI 組件庫。後端
在 React 界,優秀且開源的 UI 組件庫有不少,國外的如 material-ui,國內的如 ant-design,api
都是通過衆多使用者檢驗,組件豐富且代碼質量過硬的組件庫。數組
因此當你決定本身再造一套 UI 組件庫以前,不妨先嚐試下這些在 UI 組件庫界口碑良好的標品,再決定是否要親自進入這個看似簡單但實則困難重重的領域。數據結構
在這裏,咱們並不會去比較任何組件庫之間的區別或優劣,但卻能夠從產品層面給出幾個開發自有組件庫的判斷依據,僅供參考。架構
在選擇了本身造輪子這樣一條路以後,下一個擺在面前的艱難的選擇就是,框架
要造一個規範的組件庫仍是一個自由的組件庫?
規範的組件庫能夠從根本上保證產品視覺、交互風格的一致性,
也能夠很大程度上下降業務開發的複雜度,從而提高團隊總體的開發效率。
但在遇到一些看似類似實則不一樣的業務需求時,規範的組件庫每每會走入一個可怕的死循環,
那就是 A 需求須要使用 A 組件,可是現有的 A 組件不能徹底支持 A 需求。
這時擺在工程師面前的就只有兩條路,從零開始把 A 需求開發一遍或者侵入 A 組件代碼去支持 A 需求。
方法一費時費力,會極大地增長本次項目的開發成本,方法二會致使 A 組件代碼膨脹且邏輯複雜,極大地增長組件庫後期的維護成本。
在屢次陷入上面所描述的這個困境以後,最近一次內部組件庫重構時,咱們選擇了擁抱自由,
這其中既有業務方面的考慮,也有 React 在組件自由組合方面的自然優點,讓咱們來看一個例子。
// traditional select <div className={dropdownClass}> <div className={`${baseClassName}-control ${disabledClass}`} onMouseDown={this.handleMouseDown.bind(this)} onTouchEnd={this.handleMouseDown.bind(this)} > {value} <span className={`${baseClassName}-arrow`} /> </div> {menu} </div>
這是一個很是傳統的 Select 組件,觸發 Select 的部分爲 Select 的值及一個箭頭,咱們來看下面的一個業務場景:
這裏的選擇器再也不是 value 加一個箭頭,而是一個自定義元素,
點擊後展開下拉列表。雖然它的交互和 Select 如出一轍,
但這時候咱們就不能再用當前的這個 Select 去實現它了。
// Customizeable Select <div {...filterProps} className={classes} onClick={::this.handleInnerClick}> { children || <span> <span className={`${prefixCls}-container`}> {label ? <span className={`${prefixCls}-container-label`}>{label}</span> : null} <span className={`${prefixCls}-container-value`} style={valueStyle}> {currentValue !== '' ? currentValue : selectPlaceholder} </span> </span> <Icon className={iconClasses} name="angle-down" /> </span> } {this.renderPopup()} </div>
在傳統的 value 和箭頭以外,更自由的 Select 添加了 label 及 children 支持,分別能夠對應有名稱的 Select
或相似上面這種自定義的選擇器。
一樣的還有 Select 的孿生兄弟 Dropdown。
// Customizeable Dropdown <div {...filterProps} className={classes}> { data.map((r, i) => { return ( <ItemComponent data={r} key={i} datas={data} className={itemClasses} onClick={onSelect.bind(null, r, i)} onMouseOver={onMouseOver.bind(null, r, i)} /> ); }) } </div> // Using Dropdown const demoData = [{ text: 'Robb Stark', age: 36 }] SelectItem(props) { const { data, ...other } = props; return (<div {...filterProps}> <div>{data.text}</div> <div>is {data.age} years old.</div> </div>); }
這是一個常見的下拉列表的組件,是否容許用戶傳入 ItemComponent 其實就是一個規範與自由之間的博弈。
而在選擇了擁抱自由以後,組件的使用者終於不用再被組件所定義好的 DOM 結構所束縛,能夠自由地組織自定義下拉元素。
是的,相較於傳統的規範組件,自由的組件須要使用者在業務項目中多寫一些代碼,
但若是咱們往深處多看一層,這些特殊的下拉元素本就是屬於某個業務所獨有的,
將其放在業務代碼層偏偏是一種更合適的分層方法。
而另外一方面,咱們在這裏所定義的自由,毫不僅僅是多暴露幾個渲染函數那麼簡單,
這裏的自由,指的是組件 DOM 結構的自由,由於一旦某個組件定死了本身的 DOM 結構,
外部除了重寫樣式去強行覆蓋外沒有任何其餘可行的方式去改變它。
雖然咱們上面提到了許多自由的好處,但不少時候咱們仍是會被一個問題所挑戰,
那就是自由的組件在大部分時候真的很難用,由於調用起來很麻煩。
這個問題實際上是有解的,那就是默認值。
咱們能夠在組件內部內置許多經常使用的組成元素,當用戶不指定組成元素時,
使用默認組成元素來渲染,這樣就能夠在規範與自由之間達到一個良好的平衡。
固然,這裏也有一個貼心小提示,那就是若是你真得但願在規範與自由之間達到一個良好的平衡,必定要提早作好組件庫工做量增長三分之一的準備。
或者你也能夠選擇針對不一樣的使用場景,作兩套不一樣的解決方案,
例如前端開源 UI 框架界的翹楚 antDesign,其底層依賴的 react-component 其實也是很是解耦的設計,
幾乎看不到任何固定的 DOM 結構,而是使用自定義組件或 children prop 將 DOM 結構的決定權交給使用者。
// react-component/dropdown return ( <Trigger {...otherProps} prefixCls={prefixCls} ref="trigger" popupClassName={overlayClassName} popupStyle={overlayStyle} builtinPlacements={placements} action={trigger} showAction={showAction} hideAction={hideAction} popupPlacement={placement} popupAlign={align} popupTransitionName={transitionName} popupAnimation={animation} popupVisible={this.state.visible} afterPopupVisibleChange={this.afterVisibleChange} popup={this.getMenuElement()} onPopupVisibleChange={this.onVisibleChange} getPopupContainer={getPopupContainer} > {children} </Trigger> );
若是你問一個工程師在某個場景下,耦合好仍是解耦好?
我想他可能都不會問你是什麼場景,就脫口而出:固然解耦好,耦合的代碼根本無法維護!
但事實上,在傳統的組件庫設計中,咱們一直都默認組件是能夠和數據源(通常組件都會有 data 這個 prop)相耦合的,
這樣就致使了咱們在給某個組件賦值以前,要先寫一個數據處理方法,
將後端返回回來的數據處理成組件要求的數據結構,再傳給組件進行渲染。
這時,若是後端返回的或組件要求的數據結構再變態一些(如數組嵌套),
這個數據處理方法有可能會寫得很是複雜,
並且也會帶來許多的 edge case 致使組件在取某個特定的 attribute 時直接報錯。
那麼如何將組件與數據源解耦呢?答案就是不要在組件代碼(不管是視圖層仍是控制層)中出現 http://data.xxx,
而是在回調時將整個對象都拋給調用者供其按需使用。
這樣咱們的組件就能夠無縫適配於各類各樣的後端接口,大大下降使用者或組件在數據處理過程當中犯錯誤的可能。
承接前文,其實這樣的數據處理方式是和前面所提到的自由的設計思想一脈相承的,
正是由於咱們賦予了使用者自由定製 DOM 結構的能力,因此咱們同時也能夠賦予他們在數據處理上的自由。
講到這裏,支持規範組件的人可能已經有些崩潰了,由於聽起來自由組件既不強制 DOM 結構,
也不處理數據,代碼都要咱們在外面寫,那麼爲何還要用這個組件呢?
咱們以 Select(選擇器)組件爲例來回答這個問題。
是的,自由的 Select 須要使用者自定義下拉元素,
還須要在回調中本身處理使用 data 的哪一個 attribute 來完成下一步的業務邏輯,
但 Select 組件真的什麼都沒有作嗎?
其實並非,Select 組件規範了選擇這個交互方式,處理了何時顯示或隱藏下拉列表,
添加了下拉列表元素的 hover 和 click 事件,並控制了絕對定位的下拉列表的彈出位置。
這些通用的交互邏輯,纔是 Select 組件的核心,
至於多變的渲染和數據處理邏輯,打包開放給用戶反而更利於他們在多變的業務場景中更加方便地使用 Select 組件。
講完了組件與數據源之間的解耦,咱們再來說一下組件各個 props 之間解耦的必要性。
假設一個需求:按照中國、美國、英國、日本、加拿大的順序分別顯示 5 個當地時間,當地時間需由服務端獲取,且顯示格式不一樣。
這時咱們能夠設計一個時間組組件,能夠接收五個國家的時間數據做爲其 data prop,
而展現一個當地時間至少須要英文惟一標識符(region)、中文顯示名(name)、
當前時間(time)、顯示格式(format)四個屬性,由此咱們能夠設計時間組組件的 data 屬性爲:
data: [ { region: 'china' name: '中國', time: 1481718888, format: 'MMMM Do YYYY, h:mm:ss a', }, ... ]
看起來很完美,但事實真的是這樣嗎?
我相信若是你把這份數據結構拿給後端同事看時,他必定會馬上指出一個問題,
那就是後端數據庫中是不會保存 name 及 format 字段的,由於這是由具體產品定義的展現邏輯,
而接口只負責告訴你這個地區是哪裏(region)以及這個地區的當前時間是多少(time)。
事情到這裏也許還不算那麼糟糕,由於你能夠在調用組件以前,
把異步獲取到的數據再從新格式化一遍,補上缺失的字段。
但這時一個更棘手的問題來了,那就是接口返回的數組數據通常是不保證順序的,
你還須要按照產品的要求,在補充完缺失的字段後,對這個數組進行一次重排,以保證每一次渲染出來的地區都在一樣的位置。
換一種方式,若是咱們這樣去設計時間組組件的 props 呢?
{ data: { china: { time: 1481718888, }, ... }, timeList: [ { region: 'china', name: '中國', format: 'MMMM Do YYYY, h:mm:ss a', }, ... ], ... }
當咱們將須要異步獲取的 props 抽離以後,這個組件就變得很是 data & api friendly 了,
咱們經過配置 timeList 就能夠完美地控制時間組的渲染規則及渲染順序
而且不再須要去對接口返回的數據進行補全或定製了。
甚至咱們還能夠經過設置默認值的方式,讓組件先同步渲染出來,
在異步的數據請求完成後,重繪數值部分,給予用戶更好的視覺體驗。
除了分離非必須耦合的 props 以外,
細心的朋友可能還會發現上面的 data prop 的數據結構從數組變爲了對象,
這又是爲何呢?讓咱們來看下一小節。
設計思想能夠是自由的,數據處理也能夠是自由的,
但一個成熟的 UI 組件庫做爲一個獨立的前端項目,
在代碼層面必需要創建起本身的規範,拋開老生常談的 JavaScript 或 Sass 層面的代碼規範不表,
咱們從 CSS 類名、組件類別及回調規範三個方面來和你們分享一些最佳實踐。
在組件庫項目中,並不推薦使用 CSS Modules,
一方面是由於其編譯出來的複雜類名不便於使用者在業務項目裏進行簡單覆蓋,
更重要的是咱們其實能夠很方便地將每個組件看做是一個獨立的模塊,
用添加 xui-componentName 類名前綴的方式來實現一套簡化版的 CSS Modules。
另外,在 jsx 中咱們能夠參考 antDesign 的作法,爲每個組件添加一個名爲 prefixCls 的 prop,
並將其默認值也設置爲 xui-componentName,這樣就在 jsx 層面也保證了代碼的統一性,方便團隊成員閱讀及維護。
在此次內部的組件庫重構項目中,咱們將全部的組件分爲純渲染與智能組件兩類,
並規範其寫法爲純函數與 ES6 class 兩種,完全拋棄了 React.createClass 的寫法。
這樣一方面能夠進一步規範代碼,加強可讀性,
另外一方面也可讓後續的維護者在一秒鐘內判斷出某個組件是純渲染組件仍是智能組件。
而在回調方面,全部的組件內部函數都以 handleXXX(handleClick, handleHover, handleMouseover 等)爲命名模板,
全部對外暴露的回調函數都以 onXXX(onChange、onSelect 等)爲命名模板,
這樣在維護一些依賴層級較深的底層組件時,就能夠在 render 方法中一眼看出某個回調是在處理內部狀態,仍是會拋回到更高一層。
在設計回調數據的數據結構時,咱們只使用了單一值(如 Input 組件的回調)和對象兩種數據結構,
儘可能避免了使用傳統組件庫中經常使用的數組。相較於對象,數組實際上是一種含義更爲豐富的數據結構,
由於它是有向的(有順序的),好比在上面時間組的例子中,timeList 就被設計爲數組,
這樣它就能夠在承載展現數據的同時表達出時間組展現的順序,極大地方便了組件使用。
但在給使用者拋出回調數據時,並非每一位使用者都可以像組件設計者那樣清楚回調數據的順序,
使用數組其實變相增長了使用者的記憶成本,並且筆者一直都不同意在代碼中出現相似於
const value = data[0];
這樣的表達式,由於沒有人可以保證被取值的這個數組長度知足須要且當前位上的元素就是要取的值。
另外一方面,對象由於鍵值對的存在,在具體到某一個元素的表意上要比數組更爲豐富。
例如選擇日曆區間後的回調須要同時返回開始日期及結束日期:
// array ['2016-11-11', '2016-12-12'] // object { firstDay: '2016-11-11', lastDay: '2016-12-12', }
嚴格來說上述的兩種表達方式沒有對錯之分,
只是對象的數據結構更可以清晰地表達每一個元素的含義並消除順序的影響,
更利於不瞭解組件庫內部代碼的使用者快速上手。
在本文中,咱們從設計思想、數據處理、回調規範三個方面
從整體上爲各位剖析了在前端組件化已經成爲了既定事實的今天,
咱們還能在組件化方面作出怎樣新的嘗試與突破。
也許這些新的嘗試與突破並不會像一個新的框架那樣給你帶來全新的震撼,
但咱們相信這些實用的思考與經驗能夠幫助你少走許多彎路或打開一些新的思路,
而且跳脫出前端這個狹小的圈子,站在軟件工程的高度去看待本身手頭這些看似簡單實則複雜的工做。
在稍後的文章中,咱們會從組件庫總體代碼架構、
組件庫國際化方案及複雜組件架構設計等方面爲你們帶來更多細節上的經驗與體會,
也會穿插更多的具體的代碼片斷來闡述咱們的設計思想與理念,敬請期待。
組件庫是全部前端項目的基礎,在和你們分享經驗的同時,也但願可以多和各位進行思想上的碰撞,
咱們會從全部留言的朋友中選出最有價值的一位,送上 pure render 專欄的最新力做《深刻 React 技術棧》
(@流形 老師簽名版)一本,歡迎各位多多留言,讓咱們在交流與討論中一塊兒成長!
《深刻 React 技術棧》購買連接: