基本思路
1. 為什麼要做元件化?
無論前端也好,後端也好,都是整個軟體體系的一部分。軟體產品也是產品,它的研發過程也必然是有其目的。絕大多數軟體產品是追逐利潤的,在產品目標確定的情況下,成本有兩個途徑來優化:減少部署成本,提高開發效率。
減少部署成本的方面,業界研究得非常多,比如近幾年很流行的“去IOE”,就是很典型的,從一些費用較高的高效能產品遷移到開源的易替換的產品叢集,又比如使用Linux + Mono來部署.net應用,避開Windows Server的費用。
提高開發效率這方面,業界研究得更多,主要途徑有兩點:加快開發速度,減少變更代價。怎樣才能加快開發速度呢?如果我們的開發不是重新造輪子,而是 每一次做新產品都可以利用已有的東西,那就會好很多。怎樣才能減少變更代價呢?如果我們能夠理清模組之間的關係,合理分層,每次變更只需要修改其中某個部 分,甚至不需要修改程式碼,僅僅是改變配置就可以,那就更好了。 我們先不看軟體行業,來看一下製造行業,比如汽車製造業,他們是怎麼造汽車的呢?造汽車之前,先設計,把整個汽車分解為不同部件,比如輪子,引擎,車門, 座椅等等,分別生產,最後再組裝,所以它的製造過程可以較快。如果一輛汽車輪胎被扎破了,需要送去維修,維修的人也沒有在每個地方都修一下,而是隻把輪胎 拆下來修修就好了,這個輪胎要是實在壞得厲害,就乾脆換上個新的,整個過程不需要很多時間。
席德梅爾出過一款很不錯的遊戲,叫做《文明》(Civilization),在第三代裡面,有一項科技研究成功之後,會讓工人工作效率加倍,這項科技的名字就叫做:可替換部件(Replacement Parts)。所以,軟體行業也應當引入可替換的部件,一般稱為元件。
2. 早期的前端怎麼做元件化的?
在服務端,我們有很多元件化的途徑,像J2EE的Beans就是一種。元件建造完成之後,需要引入一些機制來讓它們可配置,比如說,工作流引擎,規 則引擎,這些引擎用配置的方式組織最基礎的元件,把它們串聯為業務流程。不管使用什麼技術、什麼語言,服務端的元件化思路基本沒有本質差別,大家是有共識 的,具體會有服務、流程、規則、模型等幾個層次。
早期展示層基本以靜態為主,服務端把介面生成好,瀏覽器去拿來展示,所以這個時期,有程式碼控制的東西幾乎全在服務端,有分層的,也有不分的。如果做了分層,大致結構就是下圖這樣:
這個圖裡,JSP(或者其他什麼P,為了舉例方便,本文中相關的服務端技術都用Java系的來表示)響應瀏覽器端的請求,把HTML生成出來,跟相 關的JavaScript和CSS一起拿出去展示。注意這裡的關鍵,瀏覽器端對介面的形態和相關業務邏輯基本都沒有控制權,屬於別人給什麼就展示什麼,想 要什麼要先提申請的尷尬局面。
這個時期的Web開發,前端的邏輯是基本可忽略的,所以前端元件化方式大同小異,無論是ASP還是JSP還是其他什麼P,都可以自定義標籤,把HTML程式碼和行間邏輯打包成一個標籤,然後使用者直接放置在想要的地方,就可以了。
在這一時代,所謂的元件化,基本都是taglib這樣的思路,把某一塊介面包括它的業務邏輯一起打成一個端到端的元件,整個非常獨立,直接一大塊從介面到邏輯都有,而且邏輯基本上都是在服務端控制,大致結構如下圖所示。
3. SPA時代,出現了新問題
自從Web2.0逐漸流行,Web前端已經不再是純展示了,它逐漸把以前在C/S裡面做的一些東西做到B/S裡面來,比如說Google和微軟的線上Office,這種複雜度的Web應用如果還用傳統那種方式做元件化,很顯然是行不通的。
我們看看之前這種元件化的方式,本質是什麼?是展現層跟業務邏輯層的隔離,後端在處理業務邏輯,前端純展現。如果現在還這麼劃分,就變成了前端有界 面和邏輯,後端也有邏輯,這就比較亂了。我們知道,純邏輯的分層元件化還是比較容易的,任何邏輯如果跟展現混起來,就比較麻煩了,所以我們要把分層的點往 前推,推到也能把單獨的展現層剝離出來。
如下圖所示,因為實際上HTML、CSS、JavaScript這些都逐漸靜態化,所以不再需要把它們放在應用伺服器上了,我們可以把它們放在專門 的高效能靜態伺服器上,再進一步發展,就可以是CDN(Content Delivery Network,內容分發網路)。前端跟後端的通訊,基本都是通過AJAX來,也會有一些其他的比如WebSocket之類,總之儘量少重新整理了。
在這張圖裡面可以看到,真正的前端已經形成了,它跟應用伺服器之間形成了天然的隔離,所以也能夠很獨立地進行一些發展演進。
現在很多Web程式在往SPA(單頁面程式,Single Page Application)的方向發展,這類系統通常比較類似傳統的C/S程式,互動過程比較複雜,因此它的開發過程也會遇到一些困難。
那為什麼大家要做SPA呢?它有很多明顯的好處,最核心的優勢就是高效。這個高效體現在兩個方面:一是對於使用者來說,這種方式做出來的東西體驗較 好,類似傳統桌面程式,對於那些需要頻繁操作的行業使用者,有很大優勢。二是執行的效率較高,之前整合一些選單功能,可能要用iframe的方式引入,但每 個iframe要獨立引入一些公共檔案,伺服器檔案傳輸的壓力較大,還要初始化自己的一套記憶體環境,比較浪費,互相之間也不太方便通訊,一般要通過 postMessage之類的方式去互動。
有了SPA之後,比如一塊介面,就可以是一個HTML片段,用AJAX去載入過來處理之後放到介面上。如果有邏輯的JavaScript程式碼,也可以用require之類的非同步載入機制去執行時載入,整體的思路是比較好的。
很多人說,就以這樣的需求,用jQuery再加一個非同步js載入框架,不是很足夠了嗎?這兩個東西用得好的話,也是能夠解決一些問題的,但它們處理 的並不是最關鍵的事情。在Web體系中,展現層是很天然的,因為就是HTML和CSS,如果只從檔案隔離的角度,也可以做出一種劃分的方式,邏輯放在單獨 的js檔案裡,html內部儘量不寫js,這就是之前比較主流的前端程式碼劃分方式。
剛才我們提到,SPA開發的過程中會遇到一些困難,這些困難是因為複雜度大為提升,導致了一些問題,有人把這些困難歸結為純介面的複雜度,比如說, 控制元件更復雜了之類,沒有這麼簡單。問題在於什麼呢?我打個比方:我們在電腦上開兩個資源管理器視窗,瀏覽到同一個目錄,在一個目錄裡把某個檔案刪了,你猜 猜另外一個裡面會不會重新整理?
毫無疑問,也會重新整理,但是你看看你用的Web頁面,如果把整個複雜系統整合成單頁的,能保證對一個資料的更新就實時反饋到所有用它的地方嗎?怎麼做,是不是很頭疼?程式碼組織的複雜度大為提高,所以需要做一些架構方面的提升。
4. 架構的變更
提到架構,我們通常會往設計模式上想。在著名的《設計模式》一書中,剛開始就講了一種典型的處理客戶端開發的場景,那就是MVC。
傳統的MVC理念我們並不陌生,因為有Struts,所以在Web領域也有比較經典的MVC架構,這裡面的V,就負責了整個前端的渲染,而且是服務端的渲染,也就是輸出HTML。如下圖所示:
在SPA時代,這已經不合適了,所以瀏覽器端形成了自己的MVC等層次,這裡的V已經變成客戶端渲染了,通常會使用一些客戶端的HTML模版去實現,而模型和控制器,也相應地在瀏覽器端形成了。
我們有很多這個層面的框架,比如Backbone,Knockout,Avalon,Angular等,採用了不同的設計思想,有的是MVC,有的是MVP,有的是MVVM,各有其特點。
以Angular為例,它推薦使用雙向繫結去實現檢視和模型的關聯,這麼一來,如果不同檢視繫結在同一模型上,就解決了剛才所說的問題。而模型本身也通過某種機制,跟其他的邏輯模組進行協作。
這種方式就是依賴注入。依賴注入的核心理念就是通過配置來例項化所依賴的元件。使用這種模式來設計軟體架構,會犧牲一些效能,在跟蹤除錯的便利性等方面也會有所損失,但換來的是無與倫比的鬆耦合和可替代性。
比如說,這些元件就可以單獨測試,然後在用的時候隨手引入,毫無壓力。對於從事某一領域的企業來說,光這一條就足以吸引他在上面大量投入,把所有不常變動領域模型的業務程式碼都用此類辦法維護起來,這是一種財富。
5. MV*框架的基本原理
如果我們來設計Angular這麼一個前端框架,應當如何入手呢?很顯然,邏輯的控制必須使用JavaScript,一個框架,最本質的事情在於它的邏輯處理方式。
我們的介面為什麼可以多姿多彩?因為有HTML和CSS,注意到這兩種東西都是配置式的寫法,參照後端的依賴注入,如果把這兩者視為跟Spring框架中一些XML等同的配置檔案,思路就豁然開朗了。
與後端不同的是,充當前端邏輯工具的JavaScript不能做入口,必須掛在HTML裡才能執行,所以出現了一個怪異的狀況:邏輯要先掛在配置文 件(HTML)上,先由另外的容器(瀏覽器或者Hybird的殼)把配置檔案載入起來,然後才能從某個入口開始執行邏輯。好訊息是,過了這一步,邏輯層就 開始大放異彩了。
從這個時候開始,框架就啟動了,它要做哪些事情呢?
- 初始化自身(bootstrap)
- 非同步載入可能尚未引入的JavaScript程式碼(require)
- 解析定義在HTML上的規則(template parser)
- 例項化模型(scope)
- 建立模型和DOM的關聯關係(binding, injection)
這些是主線流程,還有一些支線,比如:
- 解析url的search字串,恢復狀態(route)
- 載入HTML部件模板(template url)
- 部件模板和模型的關聯(binding)
6. 如何做元件化
6.1. HTML的元件化
SPA的一個典型特徵就是部分載入,介面的部件化也是其中比較重要的一環。介面片段在動態請求得到之後,藉助模版引擎之類的技術,經過某種轉換,放置到主介面相應的地方。所以,從這個角度來看,HTML的元件化非常容易理解,那就是介面的片段化和模板化。
6.2. JavaScript的元件化
JavaScript這個部分有好幾個發展階段。
- 早期的共享檔案,把公共功能的程式碼提出出來,多個頁面共用
- 動態引用,消滅全域性變數
- 在某些框架上進一步劃分,比如Angular裡面又分為provider,service,factory,controller
JavaScript元件化的目標是什麼呢,是清晰的職責,鬆耦合,便於單元測試和重複利用。這裡的鬆耦合不僅體現在js程式碼之間,也體現在js跟 DOM之間的關係,所以像Angular這樣的框架會有directive的概念,把DOM操作限制到這類程式碼中,其他任何js程式碼不操作DOM。
如上圖所示,總的原則是先分層次,層內再作切分。這麼做的話,不再存在之前那種端到端元件了,使用起來沒有原先那麼方便,但在另外很多方面比較好。
6.3. CSS的元件化
這方面,業界也有很多探索,比如LESS,SASS,Stylus等。為什麼CSS也要做元件化呢?傳統的CSS是一種扁平的文字結構,變更成本較 高,比如說想要把結構從鬆散改緊湊,需要改動很多。如果把實際使用的CSS只當作輸出結果,而另外有一種適合變更的方式當作中間過程,這就好多了。比如 說,我們把一些東西定義成變數,每個細節元素使用這些變數,當需要整體變更的時候,只需修改這些變數然後重新生成一下就可以了。
以上,我們討論了大致的Web前端開發的元件化思路,後續將闡述元件化之後的協作過程和管控機制。