基本思路
1. 為什么要做組件化?
無(wú)論前端也好,后端也好,都是整個(gè)軟件體系的一部分。軟件產(chǎn)品也是產(chǎn)品,它的研發(fā)過(guò)程也必然是有其目的。絕大多數(shù)軟件產(chǎn)品是追逐利潤(rùn)的,在產(chǎn)品目標(biāo)確定的情況下,成本有兩個(gè)途徑來(lái)優(yōu)化:減少部署成本,提高開發(fā)效率。
減少部署成本的方面,業(yè)界研究得非常多,比如近幾年很流行的“去IOE”,就是很典型的,從一些費(fèi)用較高的高性能產(chǎn)品遷移到開源的易替換的產(chǎn)品集群,又比如使用Linux + Mono來(lái)部署.net應(yīng)用,避開Windows Server的費(fèi)用。
提高開發(fā)效率這方面,業(yè)界研究得更多,主要途徑有兩點(diǎn):加快開發(fā)速度,減少變更代價(jià)。怎樣才能加快開發(fā)速度呢?如果我們的開發(fā)不是重新造輪子,而是每一次做新產(chǎn)品都可以利用已有的東西,那就會(huì)好很多。怎樣才能減少變更代價(jià)呢?如果我們能夠理清模塊之間的關(guān)系,合理分層,每次變更只需要修改其中某個(gè)部分,甚至不需要修改代碼,僅僅是改變配置就可以,那就更好了。
我們先不看軟件行業(yè),來(lái)看一下制造行業(yè),比如汽車制造業(yè),他們是怎么造汽車的呢?造汽車之前,先設(shè)計(jì),把整個(gè)汽車分解為不同部件,比如輪子,引擎,車門,座椅等等,分別生產(chǎn),最后再組裝,所以它的制造過(guò)程可以較快。如果一輛汽車輪胎被扎破了,需要送去維修,維修的人也沒有在每個(gè)地方都修一下,而是只把輪胎拆下來(lái)修修就好了,這個(gè)輪胎要是實(shí)在壞得厲害,就干脆換上個(gè)新的,整個(gè)過(guò)程不需要很多時(shí)間。
席德梅爾出過(guò)一款很不錯(cuò)的游戲,叫做《文明》(Civilization),在第三代里面,有一項(xiàng)科技研究成功之后,會(huì)讓工人工作效率加倍,這項(xiàng)科技的名字就叫做:可替換部件(Replacement Parts)。所以,軟件行業(yè)也應(yīng)當(dāng)引入可替換的部件,一般稱為組件。
2. 早期的前端怎么做組件化的?
在服務(wù)端,我們有很多組件化的途徑,像J2EE的Beans就是一種。組件建造完成之后,需要引入一些機(jī)制來(lái)讓它們可配置,比如說(shuō),工作流引擎,規(guī)則引擎,這些引擎用配置的方式組織最基礎(chǔ)的組件,把它們串聯(lián)為業(yè)務(wù)流程。不管使用什么技術(shù)、什么語(yǔ)言,服務(wù)端的組件化思路基本沒有本質(zhì)差別,大家是有共識(shí)的,具體會(huì)有服務(wù)、流程、規(guī)則、模型等幾個(gè)層次。
早期展示層基本以靜態(tài)為主,服務(wù)端把界面生成好,瀏覽器去拿來(lái)展示,所以這個(gè)時(shí)期,有代碼控制的東西幾乎全在服務(wù)端,有分層的,也有不分的。如果做了分層,大致結(jié)構(gòu)就是下圖這樣:
這個(gè)圖里,JSP(或者其他什么P,為了舉例方便,本文中相關(guān)的服務(wù)端技術(shù)都用Java系的來(lái)表示)響應(yīng)瀏覽器端的請(qǐng)求,把HTML生成出來(lái),跟相關(guān)的JavaScript和CSS一起拿出去展示。注意這里的關(guān)鍵,瀏覽器端對(duì)界面的形態(tài)和相關(guān)業(yè)務(wù)邏輯基本都沒有控制權(quán),屬于別人給什么就展示什么,想要什么要先提申請(qǐng)的尷尬局面。
這個(gè)時(shí)期的Web開發(fā),前端的邏輯是基本可忽略的,所以前端組件化方式大同小異,無(wú)論是ASP還是JSP還是其他什么P,都可以自定義標(biāo)簽,把HTML代碼和行間邏輯打包成一個(gè)標(biāo)簽,然后使用者直接放置在想要的地方,就可以了。
在這一時(shí)代,所謂的組件化,基本都是taglib這樣的思路,把某一塊界面包括它的業(yè)務(wù)邏輯一起打成一個(gè)端到端的組件,整個(gè)非常獨(dú)立,直接一大塊從界面到邏輯都有,而且邏輯基本上都是在服務(wù)端控制,大致結(jié)構(gòu)如下圖所示。
3. SPA時(shí)代,出現(xiàn)了新問題
自從Web2.0逐漸流行,Web前端已經(jīng)不再是純展示了,它逐漸把以前在C/S里面做的一些東西做到B/S里面來(lái),比如說(shuō)Google和微軟的在線Office,這種復(fù)雜度的Web應(yīng)用如果還用傳統(tǒng)那種方式做組件化,很顯然是行不通的。
我們看看之前這種組件化的方式,本質(zhì)是什么?是展現(xiàn)層跟業(yè)務(wù)邏輯層的隔離,后端在處理業(yè)務(wù)邏輯,前端純展現(xiàn)。如果現(xiàn)在還這么劃分,就變成了前端有界面和邏輯,后端也有邏輯,這就比較亂了。我們知道,純邏輯的分層組件化還是比較容易的,任何邏輯如果跟展現(xiàn)混起來(lái),就比較麻煩了,所以我們要把分層的點(diǎn)往前推,推到也能把單獨(dú)的展現(xiàn)層剝離出來(lái)。
如下圖所示,因?yàn)閷?shí)際上HTML、CSS、JavaScript這些都逐漸靜態(tài)化,所以不再需要把它們放在應(yīng)用服務(wù)器上了,我們可以把它們放在專門的高性能靜態(tài)服務(wù)器上,再進(jìn)一步發(fā)展,就可以是CDN(Content Delivery Network,內(nèi)容分發(fā)網(wǎng)絡(luò))。前端跟后端的通信,基本都是通過(guò)AJAX來(lái),也會(huì)有一些其他的比如WebSocket之類,總之盡量少刷新了。
在這張圖里面可以看到,真正的前端已經(jīng)形成了,它跟應(yīng)用服務(wù)器之間形成了天然的隔離,所以也能夠很獨(dú)立地進(jìn)行一些發(fā)展演進(jìn)。
現(xiàn)在很多Web程序在往SPA(單頁(yè)面程序,Single Page Application)的方向發(fā)展,這類系統(tǒng)通常比較類似傳統(tǒng)的C/S程序,交互過(guò)程比較復(fù)雜,因此它的開發(fā)過(guò)程也會(huì)遇到一些困難。
那為什么大家要做SPA呢?它有很多明顯的好處,最核心的優(yōu)勢(shì)就是高效。這個(gè)高效體現(xiàn)在兩個(gè)方面:一是對(duì)于用戶來(lái)說(shuō),這種方式做出來(lái)的東西體驗(yàn)較好,類似傳統(tǒng)桌面程序,對(duì)于那些需要頻繁操作的行業(yè)用戶,有很大優(yōu)勢(shì)。二是運(yùn)行的效率較高,之前集成一些菜單功能,可能要用iframe的方式引入,但每個(gè)iframe要獨(dú)立引入一些公共文件,服務(wù)器文件傳輸?shù)膲毫^大,還要初始化自己的一套內(nèi)存環(huán)境,比較浪費(fèi),互相之間也不太方便通信,一般要通過(guò)postMessage之類的方式去交互。
有了SPA之后,比如一塊界面,就可以是一個(gè)HTML片段,用AJAX去加載過(guò)來(lái)處理之后放到界面上。如果有邏輯的JavaScript代碼,也可以用require之類的異步加載機(jī)制去運(yùn)行時(shí)加載,整體的思路是比較好的。
很多人說(shuō),就以這樣的需求,用jQuery再加一個(gè)異步j(luò)s加載框架,不是很足夠了嗎?這兩個(gè)東西用得好的話,也是能夠解決一些問題的,但它們處理的并不是最關(guān)鍵的事情。在Web體系中,展現(xiàn)層是很天然的,因?yàn)榫褪荋TML和CSS,如果只從文件隔離的角度,也可以做出一種劃分的方式,邏輯放在單獨(dú)的js文件里,html內(nèi)部盡量不寫js,這就是之前比較主流的前端代碼劃分方式。
剛才我們提到,SPA開發(fā)的過(guò)程中會(huì)遇到一些困難,這些困難是因?yàn)閺?fù)雜度大為提升,導(dǎo)致了一些問題,有人把這些困難歸結(jié)為純界面的復(fù)雜度,比如說(shuō),控件更復(fù)雜了之類,沒有這么簡(jiǎn)單。問題在于什么呢?我打個(gè)比方:我們?cè)陔娔X上開兩個(gè)資源管理器窗口,瀏覽到同一個(gè)目錄,在一個(gè)目錄里把某個(gè)文件刪了,你猜猜另外一個(gè)里面會(huì)不會(huì)刷新?
毫無(wú)疑問,也會(huì)刷新,但是你看看你用的Web頁(yè)面,如果把整個(gè)復(fù)雜系統(tǒng)整合成單頁(yè)的,能保證對(duì)一個(gè)數(shù)據(jù)的更新就實(shí)時(shí)反饋到所有用它的地方嗎?怎么做,是不是很頭疼?代碼組織的復(fù)雜度大為提高,所以需要做一些架構(gòu)方面的提升。
4. 架構(gòu)的變更
提到架構(gòu),我們通常會(huì)往設(shè)計(jì)模式上想。在著名的《設(shè)計(jì)模式》一書中,剛開始就講了一種典型的處理客戶端開發(fā)的場(chǎng)景,那就是MVC。
傳統(tǒng)的MVC理念我們并不陌生,因?yàn)橛蠸truts,所以在Web領(lǐng)域也有比較經(jīng)典的MVC架構(gòu),這里面的V,就負(fù)責(zé)了整個(gè)前端的渲染,而且是服務(wù)端的渲染,也就是輸出HTML。如下圖所示:
在SPA時(shí)代,這已經(jīng)不合適了,所以瀏覽器端形成了自己的MVC等層次,這里的V已經(jīng)變成客戶端渲染了,通常會(huì)使用一些客戶端的HTML模版去實(shí)現(xiàn),而模型和控制器,也相應(yīng)地在瀏覽器端形成了。
我們有很多這個(gè)層面的框架,比如Backbone,Knockout,Avalon,Angular等,采用了不同的設(shè)計(jì)思想,有的是MVC,有的是MVP,有的是MVVM,各有其特點(diǎn)。
以Angular為例,它推薦使用雙向綁定去實(shí)現(xiàn)視圖和模型的關(guān)聯(lián),這么一來(lái),如果不同視圖綁定在同一模型上,就解決了剛才所說(shuō)的問題。而模型本身也通過(guò)某種機(jī)制,跟其他的邏輯模塊進(jìn)行協(xié)作。
這種方式就是依賴注入。依賴注入的核心理念就是通過(guò)配置來(lái)實(shí)例化所依賴的組件。使用這種模式來(lái)設(shè)計(jì)軟件架構(gòu),會(huì)犧牲一些性能,在跟蹤調(diào)試的便利性等方面也會(huì)有所損失,但換來(lái)的是無(wú)與倫比的松耦合和可替代性。
比如說(shuō),這些組件就可以單獨(dú)測(cè)試,然后在用的時(shí)候隨手引入,毫無(wú)壓力。對(duì)于從事某一領(lǐng)域的企業(yè)來(lái)說(shuō),光這一條就足以吸引他在上面大量投入,把所有不常變動(dòng)領(lǐng)域模型的業(yè)務(wù)代碼都用此類辦法維護(hù)起來(lái),這是一種財(cái)富。
5. MV*框架的基本原理
如果我們來(lái)設(shè)計(jì)Angular這么一個(gè)前端框架,應(yīng)當(dāng)如何入手呢?很顯然,邏輯的控制必須使用JavaScript,一個(gè)框架,最本質(zhì)的事情在于它的邏輯處理方式。
我們的界面為什么可以多姿多彩?因?yàn)橛蠬TML和CSS,注意到這兩種東西都是配置式的寫法,參照后端的依賴注入,如果把這兩者視為跟Spring框架中一些XML等同的配置文件,思路就豁然開朗了。
與后端不同的是,充當(dāng)前端邏輯工具的JavaScript不能做入口,必須掛在HTML里才能運(yùn)行,所以出現(xiàn)了一個(gè)怪異的狀況:邏輯要先掛在配置文件(HTML)上,先由另外的容器(瀏覽器或者Hybird的殼)把配置文件加載起來(lái),然后才能從某個(gè)入口開始執(zhí)行邏輯。好消息是,過(guò)了這一步,邏輯層就開始大放異彩了。
從這個(gè)時(shí)候開始,框架就啟動(dòng)了,它要做哪些事情呢?
初始化自身(bootstrap)
異步加載可能尚未引入的JavaScript代碼(require)
解析定義在HTML上的規(guī)則(template parser)
實(shí)例化模型(scope)
創(chuàng)建模型和DOM的關(guān)聯(lián)關(guān)系(binding, injection)
這些是主線流程,還有一些支線,比如:
解析url的search字符串,恢復(fù)狀態(tài)(route)
加載HTML部件模板(template url)
部件模板和模型的關(guān)聯(lián)(binding)
6. 如何做組件化
6.1. HTML的組件化
SPA的一個(gè)典型特征就是部分加載,界面的部件化也是其中比較重要的一環(huán)。界面片段在動(dòng)態(tài)請(qǐng)求得到之后,借助模版引擎之類的技術(shù),經(jīng)過(guò)某種轉(zhuǎn)換,放置到主界面相應(yīng)的地方。所以,從這個(gè)角度來(lái)看,HTML的組件化非常容易理解,那就是界面的片段化和模板化。
6.2. JavaScript的組件化
JavaScript這個(gè)部分有好幾個(gè)發(fā)展階段。
早期的共享文件,把公共功能的代碼提出出來(lái),多個(gè)頁(yè)面共用
動(dòng)態(tài)引用,消滅全局變量
在某些框架上進(jìn)一步劃分,比如Angular里面又分為provider,service,factory,controller
JavaScript組件化的目標(biāo)是什么呢,是清晰的職責(zé),松耦合,便于單元測(cè)試和重復(fù)利用。這里的松耦合不僅體現(xiàn)在js代碼之間,也體現(xiàn)在js跟DOM之間的關(guān)系,所以像Angular這樣的框架會(huì)有directive的概念,把DOM操作限制到這類代碼中,其他任何js代碼不操作DOM。
如上圖所示,總的原則是先分層次,層內(nèi)再作切分。這么做的話,不再存在之前那種端到端組件了,使用起來(lái)沒有原先那么方便,但在另外很多方面比較好。
6.3. CSS的組件化
這方面,業(yè)界也有很多探索,比如LESS,SASS,Stylus等。為什么CSS也要做組件化呢?傳統(tǒng)的CSS是一種扁平的文本結(jié)構(gòu),變更成本較高,比如說(shuō)想要把結(jié)構(gòu)從松散改緊湊,需要改動(dòng)很多。如果把實(shí)際使用的CSS只當(dāng)作輸出結(jié)果,而另外有一種適合變更的方式當(dāng)作中間過(guò)程,這就好多了。比如說(shuō),我們把一些東西定義成變量,每個(gè)細(xì)節(jié)元素使用這些變量,當(dāng)需要整體變更的時(shí)候,只需修改這些變量然后重新生成一下就可以了。
以上,我們討論了大致的Web前端開發(fā)的組件化思路,后續(xù)將闡述組件化之后的協(xié)作過(guò)程和管控機(jī)制。
管控平臺(tái)
1. HTML片段
我們?yōu)槭裁匆芾鞨TML片段?因?yàn)橛薪缑嬉盟鼈?,?dāng)這些片段多了之后,需要有個(gè)地方來(lái)管理起來(lái),可以檢索、預(yù)覽它們,還能看到大致描述。
這應(yīng)該是整個(gè)環(huán)節(jié)中一個(gè)相對(duì)很簡(jiǎn)單的東西,照理說(shuō),有目錄結(jié)構(gòu),然后剩下的就是單個(gè)的HTML片段文件了,這就可以解決存儲(chǔ)和檢索的問題了,但我們還要考慮更多。
已有的HTML片段,如何被使用呢?這肯定是一種類似include的方式,通過(guò)某種特殊標(biāo)簽(不管是前端還是后端的方式)把這些片段引用進(jìn)來(lái),這時(shí)候就有了第一個(gè)問題:
假設(shè)有界面A和界面B同時(shí)引用了片段C,在某個(gè)開發(fā)人員修改片段C內(nèi)容的時(shí)候,他如何得知將會(huì)影響到界面A和B呢?一個(gè)比較勉強(qiáng)的方式是全項(xiàng)目查找,但這在很多情況下是不夠的。
如果我們的HTML片段是作為獨(dú)立的公共庫(kù)存在的,它已經(jīng)不能通過(guò)項(xiàng)目?jī)?nèi)查找去解決這一問題了,因?yàn)椴还蹵還是B,只要他不處于片段C的項(xiàng)目空間,就無(wú)從追尋。
這時(shí)候很多人會(huì)問兩個(gè)問題:
跨項(xiàng)目的界面片段重用,意義在哪里?
如果我們的產(chǎn)品是針對(duì)一個(gè)小領(lǐng)域,它的復(fù)雜度根本不需要?jiǎng)澐侄鄠€(gè)項(xiàng)目部分來(lái)協(xié)作完成。設(shè)想場(chǎng)景是面對(duì)很大的行業(yè),各項(xiàng)目都是子產(chǎn)品,將來(lái)可能是其中若干個(gè)聯(lián)合部署,這時(shí)候,保持其中的一致性是非常重要的。比如我們有個(gè)基本配置界面,在多個(gè)子產(chǎn)品中都要用,如果各自開發(fā)一個(gè),其操作風(fēng)格很可能就是不一致的,給人的印象就是不專業(yè)。所以會(huì)需要把常見的界面片段都?xì)w集起來(lái),供業(yè)務(wù)方挑選使用。
修改C,只提供說(shuō)明,但是不通知A和B,不實(shí)時(shí)更新他們的版本,然后自行決定怎樣升級(jí),如何?
這會(huì)有一個(gè)問題,每次有小功能升級(jí)的時(shí)候,代碼是最容易同步合并的,所以才會(huì)有“持續(xù)集成”這個(gè)概念,如果是一直伴隨升級(jí),總要比隔一個(gè)大階段才升級(jí)好,升級(jí)成本應(yīng)盡量分?jǐn)偟狡綍r(shí),就像農(nóng)婦養(yǎng)小豬,小豬每天長(zhǎng)一點(diǎn),每天都抱來(lái)抱去,不覺得吃力,即使長(zhǎng)大了也還能抱得動(dòng)。
現(xiàn)在問題就很明確了,一定要有一種方式來(lái)把這個(gè)依賴關(guān)系管理起來(lái),很顯然,已有的版本庫(kù)是肯定管不了這些的,所以只能在外圍做一些處理。
我們建立一個(gè)管理平臺(tái),除了管理實(shí)體文件的版本,還管它們之間的關(guān)系。具體這個(gè)關(guān)系如何收集整理,有兩種方式:手動(dòng)配置,代碼分析。
手動(dòng)配置是比較土的方式,開發(fā)人員每提交一個(gè)文件,就去這系統(tǒng)上手動(dòng)配置它的依賴關(guān)系。代碼分析的話,要在每次提交文件的時(shí)候解析文件的包含規(guī)則,找出確切的文件。這兩者各有利弊,前者比較笨,但容易做,后者對(duì)代碼格式的要求比較高,要考慮的情況較多。
我們的界面往往不是那么簡(jiǎn)單,HTML片段也可能有層次的,舉例來(lái)說(shuō):
界面A里面包含了片段B,但是片段B自身又包含了片段C,所以這個(gè)依賴關(guān)系也是有層級(jí)的,需要在設(shè)計(jì)的時(shí)候一并考慮。
2. JavaScript模塊
JavaScript代碼的管理,比HTML片段的狀況好一些,因?yàn)闃I(yè)界很多這方面的解決方案。但它們還是沒有解決當(dāng)依賴項(xiàng)產(chǎn)生變更的時(shí)候反向通知的問題。
所以我們還是得像HTML片段一樣,把它們的依賴關(guān)系都管理到平臺(tái)里。于是,每個(gè)JavaScript模塊都顯式配置了自己所依賴的其他模塊,通過(guò)這種單向關(guān)系,形成了一套完整的視圖。
在JavaScript模塊的代碼實(shí)現(xiàn)中,我們是不提倡直接寫依賴關(guān)系的。很多通用規(guī)范,比如AMD,往往建議我們這樣寫模塊:
JavaScript Code復(fù)制內(nèi)容到剪貼板
- define(['dep1', 'dep2'], function (dep1, dep2) {
- var moduleA = function () {};
- return moduleA;
- });
但我們的系統(tǒng)是面向行業(yè)的,比這種通用解決方案要苛刻一些。比如說(shuō),如果有一天重構(gòu)代碼,JavaScript模塊們調(diào)整了目錄或者名字,這么寫的就痛苦了,他必須把所有影響到的都去調(diào)整一遍,這是要搜索替換的。況且,就像上面HTML模板的部分提到的,影響了處于其他項(xiàng)目中依賴它的代碼,缺少合適的方式去通知他們修改。
所以我們期望的是,在每個(gè)編寫的JavaScript模塊中只存放具體實(shí)現(xiàn),而把依賴關(guān)系放在我們的平臺(tái)上管理,這樣,即使當(dāng)前模塊作了改名之類的重構(gòu)處理,處于外部項(xiàng)目中依賴它的那些代碼也不必修改,下一次版本發(fā)布的生成過(guò)程會(huì)自動(dòng)把這些事情干掉。
對(duì)應(yīng)到上面的這段代碼,我們需要開發(fā)人員做的只是其中的實(shí)現(xiàn),也就是moduleA的那個(gè)部分,外面這些依賴的殼子,是會(huì)在發(fā)布階段根據(jù)已配置的依賴關(guān)系自動(dòng)生成的。
如果需要,JavaScript模塊還可以細(xì)分,比如類似Angular里面那樣,把factory,controller和directive分離出來(lái),這會(huì)對(duì)后續(xù)有些處理提供方便。
現(xiàn)在我們有必要討論一下模塊的粒度了,我們這里提到的都是基本的粒度,每個(gè)JavaScript模塊中存放的應(yīng)該只有一個(gè)很具體東西的實(shí)現(xiàn)。那么,有個(gè)問題,在我們發(fā)布的時(shí)候,是不是就按照這個(gè)粒度發(fā)布出去呢?
很顯然不行,如果這么做,很可能會(huì)出現(xiàn)復(fù)雜界面一次要用10多個(gè)HTTP請(qǐng)求才能加載完它所需要的所有JavaScript代碼的情況,所以需要做一些合并。
那么,合并的策略是什么?在我們這個(gè)平臺(tái)上,開發(fā)人員又是要怎樣定義這個(gè)合并關(guān)系的呢?我們需要在模塊之上定義一個(gè)更大粒度的組織方式,這個(gè)方式與模塊的關(guān)系,就好比Java里面,jar文件與class的關(guān)系。如果開發(fā)人員不顯式配置,也可以通過(guò)全局策略,比如按最下層目錄來(lái)合并。
這個(gè)時(shí)候,在實(shí)際使用這些代碼的時(shí)候,需要帶兩個(gè)配置信息過(guò)去,一個(gè)是要?jiǎng)討B(tài)載入的JavaScript文件(合并之后的),二是每個(gè)JavaScript文件中包含的原始模塊。
3. 單元測(cè)試
如果JavaScript模塊都已經(jīng)被良好有序管理起來(lái),就可以為它們考慮單元測(cè)試的事情了。單元測(cè)試對(duì)于提高基礎(chǔ)單元的可靠度,是有非常重要意義的。
在我們這個(gè)平臺(tái)里,可以把單元測(cè)試跟JavaScript模塊關(guān)聯(lián)起來(lái),每個(gè)JavaScript模塊可以掛一組單元測(cè)試代碼,這些代碼可以在線編寫,在線運(yùn)行。
單元測(cè)試的本質(zhì)就是編寫模擬代碼來(lái)調(diào)用已有模塊,考慮到我們的模塊是JavaScript,所以很多思路都傾向于在瀏覽器端執(zhí)行它們,對(duì)于單個(gè)模塊的單元測(cè)試,這不是個(gè)問題。
如果要批量執(zhí)行整個(gè)系統(tǒng)的單元測(cè)試,那就不一樣了。把JavaScript代碼先加載到瀏覽器中,然后再執(zhí)行,很多時(shí)候并不需要這么復(fù)雜。我們完全可以在服務(wù)端把它們做了。
借助Node.js的能力,我們可以在服務(wù)端執(zhí)行JavaScript代碼,也就意味著能夠把絕大多數(shù)JavaScript模塊的單元測(cè)試在服務(wù)端就執(zhí)行掉。當(dāng)然,我們?yōu)榇丝赡芤嘧霾簧偈虑?,比如說(shuō),有些庫(kù)需要移植一份node版的,常見的有AJAX調(diào)用等等。
注意了,能夠在服務(wù)端做JavaScript單元測(cè)試是有先決條件的,代碼的分層必須很良好,除了視圖層,其他任何層面都不能操作DOM。所以我們這里主要測(cè)試的也正是除了視圖層之外的所有JavaScript業(yè)務(wù)邏輯。至于視圖層怎么辦?這個(gè)真的很難解決,這世界上不是所有東西都能自動(dòng)做的,只能先把可做的做了,以后再來(lái)考慮這些。
4. 文檔和示例管理
4.1. 文檔
現(xiàn)在我們有HTML片段和JavaScript模塊了,需要給它們多一些描述信息。簡(jiǎn)單描述顯然是不夠的,我們還要詳細(xì)文檔。
這種詳細(xì)文檔可以通過(guò)某種方式生成,也可以由開發(fā)人員手動(dòng)編寫。與傳統(tǒng)的離線文檔不同,在線的文檔更實(shí)時(shí),并且,每當(dāng)一個(gè)開發(fā)人員變更了他的文檔之后,不需要經(jīng)過(guò)全量構(gòu)建,訪問者可以實(shí)時(shí)訪問到他的最新版本。
熟悉GitHub的朋友們可能早已習(xí)慣這種方式,在項(xiàng)目庫(kù)里面存在一些以md格式結(jié)尾的文本文件,使用markdown語(yǔ)法來(lái)編寫一些說(shuō)明文檔。
毫無(wú)疑問,這類格式很適合在線協(xié)作,所以我們也會(huì)在平臺(tái)上集成這么一種編寫文檔的方式,無(wú)論是針對(duì)HTML模板還是JavaScript模塊,或者是其他什么類型,甚至還可以用來(lái)當(dāng)博客,就像月影同學(xué)的gitpress平臺(tái),能直接從GitHub上拉取文本或者HTML文件形成博客。
文檔除了以集成的形式瀏覽之外,應(yīng)當(dāng)也可以以單獨(dú)鏈接的方式發(fā)出去,這時(shí)候用戶就可以像看一個(gè)新聞網(wǎng)頁(yè)一樣去瀏覽。如果再進(jìn)一步做下去,還可以做電子書的生成,提供打包的離線文檔。
4.2. 示例
在編寫代碼文檔的過(guò)程中,可能免不了要插入示例,示例有兩種形態(tài),一種是純文本,類似gist這樣,一種是可在線運(yùn)行,類似jsfiddle和jsbin這樣。
這兩種都有各自的優(yōu)點(diǎn),所以可以都做,示例的存放可以與文檔類似,也應(yīng)當(dāng)能通過(guò)一個(gè)鏈接獨(dú)立運(yùn)行。
4.3. 幻燈片
有時(shí)候我們看到一些在線的幻燈片,覺得效果很帥,比如reveal.js,我們的開發(fā)人員有時(shí)候作代碼分析或者走查的時(shí)候也不免要寫一些演示,如果能把這些東西也隨項(xiàng)目管理起來(lái),能在線查看,會(huì)是很不錯(cuò)的一件事。所以我們也可以考慮給它們加個(gè)存儲(chǔ)界面,甚至做個(gè)簡(jiǎn)易的在線編寫器。
5. 項(xiàng)目與目錄管理
說(shuō)到現(xiàn)在,我們似乎還遺漏了一點(diǎn)什么。那就是以上提到的這些東西,以什么為組織單位來(lái)存儲(chǔ)?
考慮到我們的這個(gè)平臺(tái)是要管理一整個(gè)大產(chǎn)品的全部前端內(nèi)容的,它里面應(yīng)該分了很多項(xiàng)目,對(duì)應(yīng)到子產(chǎn)品上,這么一來(lái),很自然地,項(xiàng)目就成了第一級(jí)組織單位。項(xiàng)目之下,沒有懸念地,只有目錄了。
對(duì)于一個(gè)項(xiàng)目而言,它有哪些要做的事情呢?首先要能配置其實(shí)體存儲(chǔ)位置。前面提到的這么多代碼、文檔之類,最終都是要實(shí)體存儲(chǔ)的,怎么存?我們當(dāng)然可以自己搞一套,在文件系統(tǒng)上做起來(lái),但是還要考慮它們的版本管理,非常麻煩,所以不如直接對(duì)接某個(gè)版本庫(kù),調(diào)用它的接口去存取文件,這里配置的就是版本庫(kù)的路徑。
其次,要考慮從已有項(xiàng)目復(fù)制,類似GitHub里面的fork功能,不過(guò)內(nèi)部處理機(jī)制可以略有不同,fork的項(xiàng)目默認(rèn)未必要有實(shí)體文件,只有當(dāng)產(chǎn)生了修改或者新增操作的時(shí)候才創(chuàng)建,剩下的還引用原來(lái)的就可以了。我們這里的項(xiàng)目復(fù)制功能是為項(xiàng)目化版本而考慮的,經(jīng)常出現(xiàn)一個(gè)產(chǎn)品版本支持多個(gè)客戶項(xiàng)目的情況,所以可能會(huì)用得著這個(gè)特性。
然后,也要考慮項(xiàng)目的依賴關(guān)系。依賴一個(gè)項(xiàng)目,意思是需要用到它里面的組件,所以實(shí)質(zhì)是組件的依賴。提供項(xiàng)目依賴這個(gè)視圖,只是為了未來(lái)變更的一些考慮。
6. 評(píng)論管理
之前提到,我們整個(gè)平臺(tái)的目的是為了提高大型前端團(tuán)隊(duì)的協(xié)作能力,協(xié)作是離不開交流的。上述的任何功能,都應(yīng)當(dāng)帶有交流溝通的能力。
比如說(shuō),如果開發(fā)人員A使用了其他人寫的一個(gè)代碼組件a,對(duì)其中一些細(xì)節(jié)有疑問,他應(yīng)當(dāng)可以對(duì)它進(jìn)行評(píng)論。在他評(píng)論的時(shí)候,任何參與維護(hù)過(guò)這個(gè)組件的人員都能收到一個(gè)提醒,這時(shí)候他可以選擇過(guò)來(lái)看看,回復(fù)這個(gè)疑問。同理,在文檔、示例下也可以如此操作。
在互聯(lián)網(wǎng)上有這類產(chǎn)品,用于在任意URL下掛接評(píng)論交流系統(tǒng),比較有名的就是Disqus,我們可以看到很多網(wǎng)站下面掛著它,用于做交流評(píng)論,這樣用戶可以用一個(gè)賬號(hào)在多個(gè)網(wǎng)站之間交流。國(guó)內(nèi)也有同類的,比如多說(shuō),能夠用微博、QQ等賬號(hào)登錄進(jìn)行交流。
從我們這個(gè)平臺(tái)本身看,如果是部署在企業(yè)內(nèi)部作流程提升,引入外部評(píng)論系統(tǒng)的可能性就比較小了。因?yàn)樵谄髽I(yè)內(nèi)部用,一定是希望這個(gè)員工的賬號(hào)信息跟工號(hào)掛鉤,也能夠跟版本服務(wù)器賬號(hào)等模塊作集成,權(quán)限也便于控制。
從另外一個(gè)角度講,某個(gè)人員登錄這個(gè)系統(tǒng)的時(shí)候,他可能收到很多消息,來(lái)自不同的代碼或文檔位置,挨個(gè)點(diǎn)過(guò)去回復(fù)也有些麻煩,我們應(yīng)當(dāng)給他提供一個(gè)全局視圖,讓他能在一個(gè)統(tǒng)一的界面把這些問題都答復(fù)掉,如果他需要的話,也是可以點(diǎn)進(jìn)去到實(shí)際的位置。
7. 用戶和權(quán)限控制
從以上部分我們已經(jīng)看到,這個(gè)系統(tǒng)是一個(gè)比較復(fù)雜的開發(fā)過(guò)程管控平臺(tái)。這樣的話,每個(gè)使用的人就應(yīng)當(dāng)可以登錄,然后分配不同的權(quán)限等級(jí)。
未登錄用戶應(yīng)當(dāng)有一些東西的查看權(quán)限,但是不能發(fā)表評(píng)論。已登錄的用戶根據(jù)權(quán)限級(jí)別,可以控制能否創(chuàng)建、修改項(xiàng)目,創(chuàng)建、修改目錄,代碼,單元測(cè)試,文檔等。
8. 國(guó)際化字符串管理
一個(gè)跨語(yǔ)言區(qū)域的Web應(yīng)用不可避免要跟國(guó)際化打交道,這個(gè)事情通常是在服務(wù)端做,比如通過(guò)在界面代碼中嵌入類似% =getRes(key, lan) %>這樣的代碼,去獲取相應(yīng)的字符串,替換到界面里來(lái)。
這個(gè)事情是要占用應(yīng)用服務(wù)器資源的,而且國(guó)際化本身其實(shí)是一個(gè)在運(yùn)行之前就已經(jīng)確定的事,完全可以把這個(gè)過(guò)程放在發(fā)布階段就做掉。比如說(shuō),我們給每種語(yǔ)言預(yù)先就把代碼生成多份,只是部署在一起,根據(jù)需要的情況來(lái)動(dòng)態(tài)加載特定的那一份。
有不少客戶端的國(guó)際化方案,是把資源文件拆細(xì),以頁(yè)面為單位存儲(chǔ),但這其實(shí)是不太合理的。第一個(gè)原因就是在Web2.0時(shí)代,“頁(yè)面”這個(gè)概念本身就已經(jīng)弱化了,到了單頁(yè)應(yīng)用里,整個(gè)應(yīng)用都只是一個(gè)頁(yè)面,這個(gè)時(shí)候,資源文件以什么粒度來(lái)組織呢?
我們提到過(guò),采用MV*框架去做Web應(yīng)用的架構(gòu),有一個(gè)目標(biāo)是做組件化。組件化的意圖就是某個(gè)組件可以盡可能隨心所欲地放在需要的地方用。如果把資源文件的粒度弄小到對(duì)應(yīng)HTML片段和JavaScript模塊這一級(jí),靈活性倒是有了,帶來(lái)的問題就是管理成本增大。
做一個(gè)行業(yè)應(yīng)用,最重要的就是業(yè)務(wù)一致性,這包括邏輯的一致性,也包括了術(shù)語(yǔ)的一致性。某一個(gè)詞,可能在多個(gè)資源文件中都出現(xiàn),這就增加了不一致的可能性。
所以,應(yīng)當(dāng)有一個(gè)統(tǒng)一的術(shù)語(yǔ)管理平臺(tái),一切界面上出現(xiàn)的文字或者提示,都必須來(lái)自這個(gè)平臺(tái)。
9. 靜態(tài)資源的管理
在發(fā)布系統(tǒng)的時(shí)候,除了需要發(fā)布代碼,還需要發(fā)布圖片等靜態(tài)資源,這些東西也應(yīng)當(dāng)被管理起來(lái)。
靜態(tài)資源在兩種情況下可用:隨產(chǎn)品發(fā)布,在本平臺(tái)被引用。比如說(shuō)有一個(gè)圖片,在這個(gè)平臺(tái)上作了管理,它可以被配置到某個(gè)項(xiàng)目上,在發(fā)布的時(shí)候?qū)С?。這個(gè)圖片還可以被用鏈接的方式查看或者下載,如果本平臺(tái)內(nèi)部的一個(gè)文檔或者示例要引用它,也是可以的。
10. 樣式與主題管理
在Web系統(tǒng)里,樣式和主題是很重要的一環(huán)。樣式的管理和發(fā)布一直是一個(gè)比較復(fù)雜的話題,早幾年一般都是分塊寫,然后組合合并,最近這些年有LESS,SASS和Stylus這類技術(shù),解決了編寫和發(fā)布的分離問題。
我們看看發(fā)布的最大問題是什么?是不同部分的合并。為了追求靈活性,不得不把東西拆得很細(xì),之前HTML片段和JavaScript模塊的處理方式都是這樣。這么做,我們就需要另外一件事:這些細(xì)小的東西,盡可能要覆蓋全面。
對(duì)應(yīng)到CSS里面,我們要做的是把每種在系統(tǒng)中可能出現(xiàn)的元素、類別都作為單獨(dú)的規(guī)則維護(hù)起來(lái),生成一個(gè)全局的規(guī)則列表。不同項(xiàng)目間,實(shí)現(xiàn)可以不同,但規(guī)則的名字是固定的,定制只允許修改實(shí)現(xiàn),不允許修改規(guī)則。如果要新增之前沒有的規(guī)則,也必須在全局規(guī)則列表里先添加,再作實(shí)現(xiàn)。
樣式規(guī)則被管理之后,可以在界面組件上對(duì)它作關(guān)聯(lián),也可以不做。做的好處是發(fā)布的時(shí)候能只把用到的那些樣式規(guī)則生成發(fā)布出去,如果能接受每次發(fā)布全量CSS,那也無(wú)所謂。
除了規(guī)則,也需要考慮一些變量的管理,在CSS中合理使用變量,會(huì)大為減輕定制化所導(dǎo)致的工作量。
11. 一鍵發(fā)布
我們引入了這么一堆東西,其實(shí)是增加了發(fā)布的復(fù)雜度。為什么呢?
之前不管HTML、JavaScript還是CSS,都是手寫出來(lái),最多經(jīng)過(guò)一個(gè)minify的工作,就發(fā)布了,整個(gè)過(guò)程很簡(jiǎn)單,兩句腳本搞定。
現(xiàn)在可復(fù)雜了,先要分析依賴關(guān)系,然后提取文件,然后國(guó)際化字符串替換,然后合并,然后代碼壓縮,整個(gè)過(guò)程很折騰,不給配置管理員一個(gè)解釋的話,他一定過(guò)來(lái)砍人。
我們有個(gè)原則:解決問題的過(guò)程中,如果引入了新的問題,要求負(fù)責(zé)解決原問題的人也一起解決掉?,F(xiàn)在為了一些意圖,增加了版本發(fā)布的復(fù)雜度,那也要有個(gè)辦法再把這事擺平,至少不能比原來(lái)復(fù)雜。
所以我們就要把這些過(guò)程都集成到管控平臺(tái)里,做一個(gè)一鍵發(fā)布的過(guò)程,把所有的這些操作都集成起來(lái),配置管理員發(fā)布版本的時(shí)候只要點(diǎn)一下就可以把所有這些事情做掉。甚至說(shuō),這些流程還可以配置,能夠加減環(huán)節(jié)。
這時(shí)候我們做到了跟之前發(fā)版本一樣方便,能不能多做點(diǎn)什么呢?
可以把JavaScript單元測(cè)試集成到版本發(fā)布階段。因?yàn)槲覀円呀?jīng)把JavaScript按照職責(zé)做了分層,并且把UI部分做了隔離,就可以在瀏覽器之外把這個(gè)單元測(cè)試做掉,平時(shí)提交代碼的時(shí)候也可以做,最終在版本發(fā)布階段再全量做一下,也是很有意義的。
代碼依賴關(guān)系管理的另一個(gè)目的是什么呢?是最小化發(fā)布,既然我們都管理了文件之間的關(guān)系,那么,從根出發(fā),顯然是能夠得出哪些代碼文件在本項(xiàng)目中使用的,就可以每次從我們的全量代碼庫(kù)中取得確切需要的一部分來(lái)發(fā)布。這也是我們整個(gè)管控平臺(tái)帶來(lái)的優(yōu)勢(shì)。
12. 小結(jié)
我們這一篇比較復(fù)雜,提出了一整套解決大規(guī)模前端協(xié)作的管控機(jī)制。這套理論的本質(zhì)是在開發(fā)和版本發(fā)布之間加了一個(gè)環(huán)節(jié),把Web體系中除了服務(wù)之外的一切靜態(tài)資源都納入其中,強(qiáng)化了現(xiàn)有主流的一些基于命令行的前端工程化組織模式。
相比于傳統(tǒng)行業(yè),比如汽車制造,我們這個(gè)環(huán)節(jié)相當(dāng)于生產(chǎn)流水線的設(shè)計(jì),其中一些組件的存儲(chǔ)就類似倉(cāng)儲(chǔ)機(jī)制,發(fā)布就類似出廠過(guò)程。
這個(gè)平臺(tái)本身還有不少其他的可做的東西,比如甚至可以在上面做界面的可視化定制等,這些是長(zhǎng)遠(yuǎn)的終極目標(biāo),在后面的文章里會(huì)談?wù)勔恍┛紤]。