Web應用元件化的權衡

發表於2015-12-12

1. 基本概念

什麼是Web應用?

所謂Web應用,指的是那些雖然用Web技術構建,但是展現形式卻跟桌面程式或者移動端原生應用類似的產品。這類產品的特點是邏輯較重,互動複雜,通常也是單頁式的。

主要包括:

  • 互動佔比較高的頁面體系
  • 以各種Hybrid技術構建的應用,其中的Web部分

大部分可以等同於所謂的“單頁面應用”,可以參見之前寫的這篇:構建單頁Web應用

元件化開發的優勢是什麼?

元件化的最重要作用就是提升開發和維護的效率。

最原始的元件,其功能可以單獨開發測試,然後逐級拼裝成更復雜的元件,直到整個應用。每一級都是易裝配,可追蹤,可管控的。

在Web應用中,元件化一般指什麼?

在開發Web應用的時候,無論技術選型,工程方案,還是對人員的技能需求都是有一些特點的,最重要的特點莫過於元件化。

元件化這個詞,在UI這一層通常指“標籤化”,也就是把大塊的業務介面,拆分成若干小塊,然後進行組裝。

狹義的元件化一般是指標籤化,也就是以自定義標籤(自定義屬性)為核心的機制。

廣義的元件化包括對資料邏輯層業務梳理,形成不同層級的能力封裝。

在Web應用中,元件化的主要目標是什麼?

很多人會把複用作為元件化的第一需求,但實際上,在UI層,複用的價值遠遠比不上分治。

分治帶來的是可管理性,相比一大團HTML和JavaScript的混雜,元件化之後,整個應用成為了一個很清晰的樹,一眼就能看清包含關係,也能夠很容易理清資料的傳遞方向。而且,整個應用可以從葉子節點,逐步向上測試,哪一級出了問題,可以很容易發現。

但是複用就很麻煩了,因為元件的內部實現與外部介面都很難取捨。很可能我們在設計之初,都是把元件設想成一個單一的東西,然後在實際專案中,發現最後都面目全非了。

所以,複用的工程成本很高,在使用的時候需要權衡,除了最常用了基礎控制元件,其他的不要刻意追求。

2. 元件化應當做到什麼程度?

一個軟體產品中,如果把核心穩定的部分視為資產,靈活可變的部分視為耗材,我們如何對待資產?如何對待耗材?

對待資產,我們一般會比較重視,會有長遠的規劃,優雅的實現,持續的維護,細緻的測試,詳盡的文件等等,但是對於耗材,基本上會視為一次性的東西,不會有這麼嚴謹的過程。

元件屬於資產還是耗材?模板呢?

按照上面的分類,元件明顯屬於資產,而模板一般屬於耗材。

在有些框架中,模板的使用度較低,但是常見的包含雙向繫結的框架中,都有很大比重的模板。有些模板是嵌入到元件內部的,有些則是獨立存在的,比如Angular中,可以使用ng-include動態包含一個模板,這個模板就是獨立的了。

大部分Web應用中,資產多一些,還是耗材多一些?

大部分Web系統的前端部分,其實都是耗材比資產多,人們選用Web相關技術的一個典型心理就是容易寫,而且相對隨意一些。

大部分Web應用都適合“全”元件化嗎?

這個問題要從幾個方面回答:

  • 成本。從技術角度,任何系統都是可以不計成本的,如果資源無限充足,我們可以把每個東西都實現得非常完美,但現實世界不是這樣的,每個東西都會有開發時間之類的限制,這就迫使我們只能對重要性較高,可複用性較高的東西多花時間,其他東西少花時間。
  • 實現難度。元件化方案是需要有規劃能力的,不但需要全域性的規劃能力,還需要各個區域性的規劃能力,這其實是比較高的需求了。
  • 整合難度。很多時候,我們做一個東西,並不是就只有它自己,還會有跟其他系統的整合,比如說“我的淘寶”PC版,它現在的版本是用React實現的,但仍然需要跟其他東西整合,比如公共頭尾,購物車之類,而這些東西是需要兼顧老系統,所以可能就會整合得比較彆扭。一切元件化框架,如果要跟其他異構系統作整合,基本上都不可能優雅。

元件與模板的對比

在展示內容偏多的網站中,模板是一個很常見的東西,它通過某種佔位的HTML,包含簡單的文字格式化,簡單的條件判斷,做一些很基礎的動態內容生成操作。

但是在Web應用中,因為強調元件化,所以很多人對模板的重要性有些忽視了。這裡的“模板”指的是雙向繫結的動態模板,不是傳統的靜態模板,這個基本概念之前有過回答:

Handlebars 和angularjs有什麼區別?分別在什麼情況下使用?

在Web應用中,應當如何看待模板的地位呢?我們先來看另外一個問題:

HTML,CSS,JS,這三者裡面,誰是整個Web工程的入口?

展示型的Web專案中,毫無疑問HTML是入口,也是根基,不管是JS還是CSS都是作為它的輔助。但到了Web應用中,還是這樣嗎?我們很多Web應用實際上是以JS為入口的,HTML不再被視為骨架,而是視為一種動態的東西,由JS建立並管理。

在這個前提下,人們對動態的HTML又有兩種不同方式的認知:它是模板,還是元件?

從典型的MVVM三層中,我們可以看到,View Model是Model的外圍,View是View Model的外圍,一層一層出去,外層實際上可以視為內層的配置檔案。而如果從元件化的角度出發,View跟View Model共同構成了元件層。

因此,動態的HTML究竟算是什麼,取決於我們從什麼角度去看待它,也取決於我們在使用什麼框架。

3. 元件化框架

目前有哪些流行的元件化框架?

我們現在開發Web應用,一般也不會從0開始,通常是選取一個核心框架(庫),然後在此基礎上確定一些規則,逐步構建外圍體系,現在比較火的有React,Angular,Vue,Polymer等。

“MV*”:Angular,Vue等
“反應式”:React,Reactive等
標準增強:Polymer

幾個流派各自特點是什麼?

MV*: 分層,繫結
React: 元件化,單向資料流

React中一般的元件相當於MVVM流派中的什麼?

以上提到的幾個東西,在元件化這塊,可能爭議最大的是Angular,因為Angular 1.x的官方指引中,並未在元件化這個方向上作一些指導,也沒有提倡,甚至連建議都沒有,而React和Polymer是天然元件化的,Vue提供的文件裡以很大篇幅詳細說明了元件化的機制和實踐方式。

但是,這並不是說,Angular 1.x就是與元件化衝突的,它仍然可以通過directive等相關機制,實現自己特色的元件化方案。

Directive可以實現自定義標籤和自定義屬性,這兩者可以理所當然地歸類到元件中,但是,在Angular中,模板本身也可以視為一種元件,一種輕量級的元件,它不一定就是靜態的,仍然可以有一些簡單的操作和行為。

Directive和模板相當於MVVM中的View層,它們的執行,一般是離不開ViewModel的支撐的,在Angular中,這就是controller。所以,如果以Angular框架來說,directive和模板、controller,共同形成了檢視層元件體系。推廣到其他MVVM框架來說,也就是View和ViewModel,而React整體就處於檢視層,所以這兩者算是一個對等關係。

這些流派有共同的未來嗎,會是什麼?

無論是哪種框架,在開發Web應用的時候都要面臨一個問題:業務資料層如何設計?

這一層東西,其實目前各路框架都未提出有力的解決方案,大家的重點都還是在做上層UI。

但是從長遠來看,業務資料層會是一個基本沒有框架差異的東西,同一個方案,大家都可以用,比如說之前有人把flux之類的東西放到React之外的框架用,也一樣可以。

而上層UI,其實現過程現在也很明確地是要往Web Components靠攏,實現邏輯都是使用ES新標準,資料繫結機制都是getter setter或者observe,載入方式都在考慮HTTP2之類,一旦某個領域出現了理念突破,很快就會被其他框架吸收融合。

所以總的來說,各框架是趨同的。

4. 元件化的實踐

一個全元件化體系,會形成元件樹,上下級元件之間應當如何通訊?不同層級的元件之間應當如何通訊?

當我們把一個應用使用元件化的理念進行構建的時候,整個應用就形成了一個倒置的樹,樹根就是應用本身,其餘節點是層層巢狀的元件們,葉子節點是最基礎的元件。

如何規劃元件樹的層級與元件的粒度?

如果我們有兩個不同團隊,同樣基於元件化的理念,使用同一個框架,做同樣功能的產品,最終形成的元件樹可能差別很大,這個差別主要在於:

把什麼視為元件,元件的粒度是怎樣的。

在元件化的應用中,元件樹的層級不宜過深,從根節點算起,應當儘可能控制在3到5層內,如果層級太多的話,會造成元件通訊和資料傳遞的負擔。

如何約定元件之間的通訊方式?

在一個元件化的應用中,會存在元件之間的資料傳遞。

以React為例,如果存在兩級巢狀的元件:

這裡面可能存在:

  • 直接對TodoList進行整組資料的賦值
  • 直接對某個TodoItem賦值
  • TodoList對下屬的TodoItem賦值
  • TodoList和TodoItem自己去某個“全域性”資料中讀取配置項

這裡面,前三種都可以通過該元件的props傳遞進去,屬於對元件的常規用法,第四種,則屬於對資料層的利用。

那麼,我們如何權衡兩種資料通訊方式呢?

一個比較粗糙的辦法是,從資料模型的角度去考慮。如果一個元件所要獲取的資料模型是比較獨立的,不依賴其他業務資料,可以直接去獲取,如果跟其他這個資料模型跟其他資料之間存在耦合,比如主從聯動關係,由父元件進行分發會比較好。

另外一個著眼點是權衡上下兩級元件之間的關係密切程度,如果它們之間的關係很強,對外界來說是一個緊密結合的整體,可以直接在它們之間傳遞資料,如果關係不強,或者在元件樹上距離較遠,適合通過第三方轉發通訊。

從這裡我們得出的結論是:

並不是選擇了框架,就可以順利把一個Web應用做出來了,還需要一件很重要的事,那就是:業務架構。元件之間的關係都是需要統籌規劃的,這裡面有很多技巧,可以參見一些大型桌面程式的架構,從中獲取不少經驗。

資料通訊層

全元件化還帶來另外一個課題,那就是資料層的設計。比如說,我們可能有一個選擇城市的列表元件,它的資料來源於服務端的一個查詢,為了方便起見,很可能你會選擇把查詢的呼叫封裝在元件內部,然後這個元件如果被同一個可見區域的多個部分使用,或者是這個查詢及其資料結果被同一可見區域的其他元件也呼叫了,就出現了兩個問題:

  • 資料同步
  • 請求的浪費

另外,對於關聯資料的更新,也不太便於控制,RESTful之類的服務端介面規範在複雜場景下會顯得力不從心。

在資料通訊這層,Meteor這樣的框架提出了自己的解決思路,跳出傳統HTTP的侷限,把眼光轉向WebSocket這樣的東西,並且在前端實現類似資料庫的訪問介面。

Facebook對此問題提出了更暴力的解決方式,Relay和GraphQL,這兩個東西我認為意義是很大的,它解決的不光是自己的痛點,而且是可以用於其他任意的前端元件化體系,對前端元件化這個領域的完善度作出了極其重大的貢獻。

5. 其他思考

如何看待“視覺化繼承”?

在不少元件化框架,包括桌面端的,Web端的,都有“視覺化繼承”這個概念,比如說,我們有一個List元件用於展現列表資料,然後,又有另外一個需求,在這個列表上顯示checkbox,用於多選。在很多元件化框架裡,都會存在這樣的繼承關係:

我覺得有必要探討一下這裡這個extends,是不是一定要用這樣的方式來實現一個形態類似原元件的新元件?

在全元件式體系中,繼承是不如組合優雅的,以上面這個情況來說,它會在render方法裡,重新實現自己的東西,所以,它繼承了什麼呢,很少很少的東西。

我們可以換種思路,保持元件不變,通過不同的配置項使其相應不同的功能。

模板外接的元件實現方式

在實現一個很基礎的UI元件的時候,我們一般都會想要把它搞得既簡潔,又強大,但這件事情本身是很難權衡的,針對不同的元件,可能會有不同的策略。

我們在開始實現元件的時候,通常會盡可能考慮需求,然後將其作為預設實現,並且對外提供一些配置項,用於開關這些功能。

還是用列表舉例,比如我們有一個列表,可以用於選中,內部結構可能會搞成這樣:

然後對外的形式這樣:

或者這樣:

然後,加需求了,列表有多種形態,一種橫著排的,一種豎著排的,一種片狀的,每行N個,排滿換行,然後這裡面還再分,元素是否定寬,還是流式。

那我們就面臨著幾個選擇:

加配置屬性,或者增加不同的元素,如TileList,HorizontalList等等。

接著,我們來了對列表項的自定義需求:

  • 每個列表項帶一個checkbox。
  • 列表可以設定有無表頭。
  • 表頭可以設定有無checkbox。
  • 如果表頭有checkbox,需要跟每行的checkbox狀態進行關聯。當表頭checkbox點選的時候,所有行的checkbox與它同步;當每行checkbox點選的時候,表頭checkbox狀態也與之同步。
  • checkbox需要可以設定顯示在列表左側還是右側
  • 列表內容可以自定義文字格式化函式
  • 列表內容可以自定義為其他元件,並且有一些資料傳遞和事件通訊方式……
  • 然後,還要可以自定義樣式…… ……

所以,這個元件變得非常複雜,對外的介面很複雜,內部實現也很複雜,程式碼更是臃腫不堪。擺在我們面前的有這麼一個矛盾:

怎樣讓我們的元件既強大,又便於使用?

面對此類場景,我想給出一個解決方案,那就是:

  • 把元件實現為一種外掛平臺
  • 針對元件的各種形態,將其特徵分離出來當成一種外掛

為了說明這個理念,我花了大約一個小時,寫了這樣一個demo,看其中datagrid那段。

其主體實現邏輯是這段:datagrid.js

看看這個程式碼,再對比所展示出來的這些功能,會不會覺得差異有點大?

奧祕在哪裡呢,在於我們給每種場景傳入了不同的模板,如下:

這個理念其實並不新鮮,在Adobe Flex的元件框架中,List系列的元件就通過開放自定義itemRenderer的方式,極大提升了可擴充套件性,並且保持原元件實現的優雅。同理,使用類似的方式,用React也可以這樣實現。

但我們這個地方會更加簡潔,其原因在於兩點:

  • Angular的模板即可起到輕量元件的作用,程式碼更精煉
  • Angular的作用域有繼承機制,這樣,傳入的模板直接與原元件融為一體,共享同一份資料

對於Angular的這個作用域機制,很多人都反感,但我認為,它並不一定就比全部在傳遞時候賦值的immutable機制差,在業務開發中,元件化固然是有用,但頻繁的上下級資料傳遞可能會讓整個系統更加零碎化,資料層的零碎化是非常不利的。

今年大家有了React,黑Angular就格外狠了,我舉這個例子也是為了說明,Angular 1.x的設計,除了module是完全的敗筆,變更檢測機制值得商榷,其他的並無大問題,甚至還存在一些優勢。使用某框架的時候,如果熟悉原理並加以合理利用,能夠巧妙解決業務上遇到的很多問題。

模板的意義

除了上面提到的,模板還有另外的意義。

我們會發現,在React的體系裡,HTML和DOM本身還重要嗎?重要性其實是大幅降低了,所以我們會看到ReactNative,ReactCanvas之類的實現,而且,最新版本的React中,把React DOM單獨抽取出來了,這意味著,React未來只把DOM作為它的可選檢視渲染層之一。

但是我們必須認識到,在Web體系中,HTML和DOM有不可替代的優勢,它們是當前Web技術的根基,儘管有缺點,並不代表應當被拋棄,至少是在現在這個時代。

所以,在Web應用這樣的體系中,元件的實現技術還是應當儘可能基於DOM來考慮。也正是在這種場景下,模板和繫結技術仍然存在很重要的作用,比如可訪問性等等特性,都是別的非DOM體系所缺乏積累的。

此外,模板某種程度上可以視為“元件的字面量形式”,也就是元件的一種序列化形式,如果我們要動態載入元件,使用模板會非常方便,這也就是我上面那個資料表格例子的意義所在。

HTML體系做元件化的不利因素

HTML本身的標籤,其實做元件化是有些彆扭的,這個原因在哪裡呢,兩點:

  • 標籤沒有名稱空間
  • 有些內建標籤是依賴於別的標籤而存在的,並且往往有預設的佈局語義,比如TR,比如LI,這些東西單獨跟內部一些元素一起封裝而成的“元件”,並不能做到可以任意放置。

在其他一些體系裡並不存在這樣的問題,比如WPF,比如Adobe Flex,因為他們沒有這樣的“歷史負擔”。

另外一個方面,所謂的元件巢狀,從宣告式程式碼的編寫方式來看,就是標籤的巢狀。標籤巢狀的含義在UI層被賦予了更多潛規則,比如這個程式碼:

如果Service並非有UI展現的東西,而是像polymer裡面的core-ajax那樣,或者Adobe Flash體系裡的WebService,你可以把它當做Panel例項裡面的一個成員變數,然後設定它的屬性或者呼叫方法。但是,對於更普通的情形:

同樣的寫法,這個含義一樣嗎?很明顯不一樣,因為Button也是一個可展示的元件,這時候你預設它是被放置在Panel的展現內部,作為它的視覺化子元素的。也就是說,這時候,你不但在邏輯上把兩者建立了關聯,還要在佈局上考慮它們的約束。

如果你的外層元素是一個佈局為主的容器,那好說,比如這裡的Panel,我們預設它有一塊展示區,所有子節點都放在裡面以某種方式排版,或者flow,或者float,或者flex,甚至border-layout,東西南北中。

如果外層元素不是一個佈局為主的容器,允許它巢狀別的東西,邏輯上就很難理解。它必須約束自己所能允許放置的子元素的型別。比如:

List下面就只能放ListItem型別的東西。

再回頭看Web Components

我覺得,在有了類似angular那種自定義元素、屬性的方式(具體實現可以改進),或者React那種自定義標籤之後,Web Components的使用場景變得很尷尬了。

我們現在看Web Components的作用,主要還是隔離,包括對邏輯和內部展現的隔離。JavaScript邏輯的隔離其實作用不是很大,因為我們用其他辦法也能達到相同的效果,但是Shadow DOM和Scoped CSS這兩個東西就很耐人尋味了。

比如說,我們現在用Shadow DOM實現了一個東西,然後,在瀏覽器裡面開啟檢視開關,還是可以看到裡面的東西,那如果不糾結它的實現機制的話,跟使用某種元件化框架建立的自定義元素相比,差異是不是就沒有那麼大了?因為寫的時候都只是寫一個自定義的元素,執行的時候在內部放了具體實現細節。

至於Scoped CSS,更有意思,因為它實際上帶來了對已有的工程方案的挑戰。我們思考Web Components普及之後的元件化思路,在樣式這塊幾乎都必然走到一條路上,那就是:樣式的inline化,把元件的樣式全部內建,否則,元件的獨立性無從保證。但我們不要忘了,/deep/和::shadow選擇器是用來幹什麼的?這是允許外部的樣式對元件內部的東西作調整,這是一個很無奈的選擇,因為確實有這種場景,比如你需要對所有元件設定全域性風格之類。另外上次聽誰說到父選擇器,允許元素控制其上級的樣式……真是被震驚了,我理解這種需求,比如某種圖片放到一個容器裡,不管它放在哪,都希望其父容器背景如何如何,但是,這是對元件化技術的一種挑戰……

在實際工程中,樣式inline化是有很多缺陷的,比如剛才提到的:theme怎麼辦?從我近期的一些文章可以看到觀點,就是不贊同全元件化,尤其是在上層更傾向於直接使用HTML模板而不是封裝過的元件,因為我認為:Web,或者說泛HTML體系,它跟其他任何的客戶端展現技術,比如Java Swing,WPF,QT,Adobe Flex之類相比,最本質的不同在於極其強大的CSS,正是因為有它,我們才有可能極盡所能地、簡單而優雅地打造不同的使用者體驗,而不是用各種畫布去繪製畫素。如果你決定在底層去各種繪製,那確實可以把UI層全元件化,但這個事情也只能在有限範圍幹,比如移動端,比如遊戲,否則代價不堪設想。

面對theme的需求,我們只能通過往動態構建的路上去走,這裡面也會有很多要考慮的點。

6. 小結

看到這裡,有什麼感覺?想要在有一定複雜度的Web應用中全面推行元件化,需要考慮的東西非常多,相當於從農業社會到工業社會的飛躍,我們不能期望一蹴而就,需要通盤考慮。

各類客戶端開發技術中有很多值得借鑑的地方,結合Web技術自身的一些特點,可以觸類旁通。

相關文章