在我寫的其他系列的文章中有提到——
在軟件工程中,「組件(component)」一般是指軟件的可複用塊,好比製造業所使用的「構件」。這是一個比較寬泛的概念,它可以是軟件包,可以是 web 服務,也可以是模塊等。
但在前端眼裏,「組件」通常是指頁面上的視圖單元,即「UI 組件」。可以説,「UI 組件」是「組件」的子集。你可能還總會聽到「控件(control)」這個詞。放輕鬆,別抓頭,它只是「UI 組件」的一個別名而已。
普通的組件通用性很差,也就是説,它基本只能用於某個特定的系統且不能被替換。有一種組件,它是基於標準化的接口規範開發出來的,能用在任何對接了該接口的系統,也能被任何符合該接口規範的組件替換——它就是「可交換組件」,就像製造業所使用的「標準件」。
可交換的 UI 組件是前端 GUI 開發從手工作坊到自動裝配的關鍵所在。
歐雷《聊聊前端 UI 組件:核心概念》
本文就來簡單説説在中後台前端應用開發中,一個可複用性儘可能高的組件體系大概是什麼樣的。
前端開發是 GUI 開發,會同時涉及到視覺交互和數據邏輯,因此在看待前端時要能從視覺和數據這兩個角度出發——
視覺視角
從視覺角度看,就是在拿到設計稿後在腦中把各個界面分解成若干具有層級關係且結構化的「區塊(block)」:
將這些區塊按照職責、粒度等可大致劃分為「控件(control)」、「部件(widget)」和「頁面(page)」這三類。其中,「頁面」粒度最大,「控件」粒度最小,再小就是 HTML 元素了,這不在探討範圍內。區塊的粒度越大,可複用性就越低。
控件
正如本文開頭所説,在常規語境中「控件」是「UI 組件」的別名;但在我所闡述的組件體系中,它是指那些業務無關的原子組件,也就是「基礎組件」。
較真的人看到上面描述中的「業務無關」和「原子組件」,也許會要問「怎樣算業務無關」及「如何判斷一個 UI 組件是不是原子組件」。
説實話,「業務無關」和「原子組件」都不是具有清晰邊界的概念,我無法精確地去定義——就像「桌子」一樣。要明確「業務」和「原子」的邊界,需要結合所處行業、企業、項目等環境的特點以及自己和團隊的理解。
UI 組件是什麼?可以認為它是一個返回視圖結構的函數,而 UI 組件的屬性(prop)和事件(event)就是這個「函數」的參數。屬性是 UI 組件的外部與其內部進行主動通信的數據,事件則是進行被動通信的回調函數。
一個封裝得好的函數,它的參數應儘可能少,要想明白每個參數的語義,且必須確實有其存在的意義——UI 組件的屬性和事件的設計也該如此。
在設計 UI 組件的屬性時,先思考下要加的這個屬性是不是屬於這個 UI 組件本身的特性?若不是,那要加的屬性的值所對應的 UI 組件的特性是什麼?如果這兩個問題都沒有得到答案,那麼這個屬性可以不用加了。
UI 組件的屬性只應與其本身的特性有關,與業務意義無關——自身特性是自然特性,業務意義是附加特性。
歐雷《聊聊前端 UI 組件:組件設計》
這段引文一方面説了 UI 組件的本質是「返回視圖結構的函數」,UI 組件的屬性和事件都是這個「函數」的參數;另一方面強調 UI 組件或者説控件的屬性和事件的設計只應與其本身的自然特性有關,並且要儘可能少——控件的內部與外部主要是靠屬性和事件進行通信。
作為體系中最小粒度的視圖單元,控件提供了單純且純淨的結構(包括視覺結構和內容結構)、表現(主題風格)與交互的複用。
關於控件的研究我已經/將要在文章系列「聊聊前端 UI 組件」中進行,這裏就不多做贅述了。
部件
在做業務開發時總會碰到這種情形——
幾個表格頁或者表單頁看起來很相似,有很多重複代碼,覺得可以封裝成一個「業務組件」,於是就那麼去做了;當看到重複代碼減少了一大半,很有成就感,心裏美滋滋~
但隨着這類看似相近的頁面越來越多,自己封裝的那個「業務組件」的屬性也跟着多了起來,並且那些屬性中使用率高的也沒幾個,這時不禁開始懷疑自己:「我是不是被眼前的表象給忽悠了?!」
這種在業務開發中封裝的比較強依賴於特定場景的「業務組件」,在我所闡述的組件體系中叫做「部件」。
過往的經驗讓自己意識到在相對大粒度的部件中像相對小粒度的控件一樣主要靠屬性和事件進行通信不夠理想。那麼,在部件中的主要通信手段該是什麼呢?
還記不記得,有個「八股文」前端面試題大概是這樣的:「請説説組件間如何進行通信?」
把「八股文」背得滾瓜爛熟的人會立馬脱口而出:「父子間用屬性和事件,兄弟間就以父組件為中介通過屬性和事件;跨層級的話,在 Vue 中用 provide / inject,在 React 中用 Context;再就是 Vuex 之類全局的狀態管理。」——如此用心的準備,值得面試官呱唧呱唧幾下。
有的面試官可能還會進一步問:「跨層級通信的上下文和狀態管理是什麼?為什麼會有它們?」聽到這個問題,面試者心裏一顫:「這……這有點超綱了吧?!我背的八股文裏沒有這個啊……」
在我的理解中,雖然細節上有點區別,但「上下文」和「狀態管理」都是更廣義一點的「上下文」——
在編程語言中,「上下文」一般是指讓程序能夠正常執行的一組環境變量,如執行上下文;而在應用開發中,通常衍生為用來維護作用於一定範圍的狀態的對象。
構造並傳入或注入「上下文」是一種比較好的讓 UI 組件變「瘦」的實踐——
在 UI 組件樹中從某一層往下的幾層所包含的 UI 組件是一個相對獨立的子系統,它們要協作完成同一個任務,與這個任務相關的狀態和操作無需分散在各個 UI 組件中,經由「上下文」集中管理可讓狀態更好維護,狀態變化更容易追蹤。
歐雷《聊聊中後台前端應用:模塊相關的一些事》
在相對大粒度的部件中,如果主要依賴屬性和事件進行通信的話,它們的數量很容易變得失控,並且內部的結構和邏輯也會被改得面目全非,維護起來十分困難和難受——這就變成了一坨翔💩!
所以,與 UI 組件本身的自然特性無關的東西不應作為其屬性或事件存在,而是「上下文」。
先來拿人做類比,幫助理解下——
人有頭、軀幹、四肢、腦、五臟六腑等組成部分,經過腦活動可以進行交流、創造等——這些是人的自然特性。人的職業、角色、身份等是自然特性嗎?當然不是!這些是人腦的運作機制在特定的環境、上下文中對接收到的信息處理後所形成的結果。
由此可見,人的自然特性是有限的,而由自然特性所衍生出來職業、角色、身份等則是無限的。鑑於此,倘若把 UI 組件的非自然特性設計為屬性,其數量將多如牛毛。那些具有業務、配置語義的東西,理應作為環境、上下文被 UI 組件內部起到人腦作用的程序所「理解」,並做出相應的「反應」或「動作」。
另外,集成了像「數據上下文」(後續文章中會講)這種業務應用開發框架所提供的機制的 UI 組件也叫「部件」。這類部件大、小粒度的都有,比如下文將要説到的「字段」,就很可能是小粒度的。
綜上所述,「上下文」是業務的狀態和邏輯複用的一種方式,是「部件」的主要通信手段;而「部件」則是連接了「上下文」與「控件」的「適配器」。
頁面
一般意義上的「頁面」就是指網頁整體,但在這個組件體系中,它是具備網頁整體佈局功能,即包含了頁頭、頁腳、側邊欄、主體區域等槽位的 UI 組件;在此之上,除了主體區域之外的其他槽位都已被填充的 UI 組件,也可被稱作「頁面」:
主體區域與其他部分可以看作是兩個相互隔離的環境——主體區域是顯示錶格、表單、圖表等以領域/業務為中心的內容;而其他部分則顯示導航菜單、麪包屑等以(一般意義層面的)頁面為中心的內容。
一般來説,主體區域與其他部分之間不會有通信,若有,可以利用應用級的上下文。
對其他部分影響最大的是路由配置,因為無論是導航菜單、麪包屑還是頁面標題都能夠基於它和 URL 計算出來;所以最好不要直接用 Vue Router 或 React Router 的方式去配置路由,而是在自定義的結構的基礎上經過一定的處理之後生成它們所需要的結構。
數據視角
從數據的視角來看,那些承載着數據輸入、輸出相關職責的區塊,稱為「視圖」和「字段」——列表和對象結構的數據對應的是「視圖」,對象結構數據的屬性/鍵是「字段」。
根據列表與對象、對象與屬性/鍵、屬性/鍵與值之間的內在關係,它們也形成了層級結構——
更多描述請看《聊聊前端 UI 組件:核心概念》的「數據及其形態」部分。
「視圖」的具體呈現就是列表、表格、表單等,而「字段」則是輸入框、下拉列表、文本框等。
理論上來説,與數據輸入、輸出無關的區塊不是「視圖」,但為了架構、工程等方面的統一,把不包含網頁整體佈局部分的大粒度區塊(主要是指在頁面或對話框主體區域中的)都叫做「視圖」——這就產生了狹義與廣義之分。
從數據角度看前端是後續文章中要講的「數據上下文」的重要前提。
總結
一個人如果什麼都幹,那他很可能什麼都會,但什麼都不精。這對做做自己「能用就行」的「玩具」來説也許沒什麼問題,但在與他人協作去想要打造一個「精品」時,往往是不行的——每個人都需要分清職責,儘量分工沒有重疊,並盡力做好自己負責的那攤子事兒。
我所闡述的組件體系也是這樣,始終貫徹關注點分離和單一職責原則,細化分工並有能力有機結合成一個龐大複雜的前端應用——
從視覺視角將界面分解為若干具有層級結構的「區塊」,按照職責、粒度等把它們大致分為「控件」、「部件」和「頁面」。
其中,「控件」是業務無關的原子組件,由「風格組件」(基本對應所謂的「Design Tokens」)、「視覺組件」、「無頭組件」和「結構組件」所構成(詳見《聊聊前端 UI 組件:組件體系》),提供了結構、表現與交互的複用能力;「部件」是連接「上下文」與「控件」的「適配器」;「頁面」則是包含了頁頭、頁腳、側邊欄、主體區域等槽位的具備網頁整體佈局功能的 UI 組件。
以數據視角劃分的「視圖」和「字段」,催生出後續文章要講的「數據上下文」,為業務的狀態和邏輯複用提供了一種方式。
這個組件體系的目標之一就是「輕」UI——將非交互、展示類的業務邏輯從 UI 組件中剝離,令 UI 組件變薄變輕巧;讓 UI 組件變得不被重視,減輕它的負擔並降低它的地位。
理想情況下,最終會發現——除了業務邏輯,好像其餘部分幾乎都是接口(interface)——具體實現可以任意移除,隨意替換!
本文其他閲讀地址:個人網站|微信公眾號