前端比較簡單,不需要架構(gòu)?
可能一些同學(xué)會認(rèn)為前端比較簡單而不需要架構(gòu),或者因?yàn)榍岸私换ゼ?xì)節(jié)雜而亂難以統(tǒng)一抽象,所以沒辦法進(jìn)行架構(gòu)設(shè)計。這個理解是片面的,雖然一些前端項(xiàng)目是沒有仔細(xì)考慮架構(gòu)就堆起來的,但這不代表不需要架構(gòu)設(shè)計。任何業(yè)務(wù)程序都可以通過代碼堆砌的方式實(shí)現(xiàn)功能,但背后的可維護(hù)性、可拓展性自然也就千差萬別了。
為什么前端項(xiàng)目也要考慮架構(gòu)設(shè)計?有如下幾點(diǎn)原因:
- 從必要性看,前后端應(yīng)用都跑在計算機(jī)上,計算機(jī)從硬件到操作系統(tǒng),再到上層庫都是有清晰架構(gòu)設(shè)計與分層的,應(yīng)用程序作為最上層的一環(huán)也是嵌入在整個大架構(gòu)圖里的。
- 從可行性看,交互雖然多而雜,但這不構(gòu)成不需要架構(gòu)設(shè)計的理由。對計算機(jī)基礎(chǔ)設(shè)計來說,也面臨著多種多樣的輸入設(shè)備與輸出設(shè)備,進(jìn)而產(chǎn)生的標(biāo)準(zhǔn)輸入輸出的抽象,那么前端也應(yīng)當(dāng)如此。
- 從廣義角度看,大部分通用的約定與模型早已沉淀下來了,如編程語言,前端框架本身就是業(yè)務(wù)架構(gòu)的一部分,用 React 哪怕寫個 “Hello World” 也使用了數(shù)據(jù)驅(qū)動的設(shè)計理念。
從必要性看,雖然操作系統(tǒng)和各類基礎(chǔ)庫屏蔽了底層實(shí)現(xiàn),讓業(yè)務(wù)可以僅關(guān)心業(yè)務(wù)邏輯,大大解放了生產(chǎn)力,但一款應(yīng)用必然是底層操作系統(tǒng)與業(yè)務(wù)層代碼協(xié)同才能運(yùn)行的,從應(yīng)用程序往下有一套邏輯井然的架構(gòu)分層設(shè)計,如果到了業(yè)務(wù)層沒有很好的架構(gòu)設(shè)計,技術(shù)抽象是一團(tuán)亂麻,很難想象這樣形成的整體運(yùn)行環(huán)境是健康的。
業(yè)務(wù)模塊的架構(gòu)設(shè)計應(yīng)當(dāng)類似計算機(jī)基礎(chǔ)的架構(gòu)設(shè)計,從需求分析出發(fā),設(shè)計有哪些業(yè)務(wù)子模塊,并定義這些子模塊的職責(zé)與子模塊之間的關(guān)系。子模塊的設(shè)計取決于業(yè)務(wù)的特性,子模塊間的分層取決于業(yè)務(wù)的拓展能力。
比如一個繪圖軟件設(shè)計時只要需要組件子系統(tǒng)與布局子系統(tǒng),它們之間互相獨(dú)立,也能無縫結(jié)合。對于 BI 軟件來說,就增加了篩選聯(lián)動與通用數(shù)據(jù)查詢的概念,因此對應(yīng)的也會增加篩選聯(lián)動模型、數(shù)據(jù)模型、圖形語法這幾個子模塊,并按照其作用關(guān)系上下分層:
如果分層清晰而準(zhǔn)確,可以看出這兩個業(yè)務(wù)上層具有相同的抽象,即最上層都是組件與布局的結(jié)合,而篩選聯(lián)動與數(shù)據(jù)查詢,以及從數(shù)據(jù)模型映射到圖元關(guān)系的映射功能都屬于附加項(xiàng),這些項(xiàng)移除了也不影響系統(tǒng)的運(yùn)行。如果不這么設(shè)計,可能就理不清系統(tǒng)之間的相似點(diǎn)與差異點(diǎn),導(dǎo)致功能耦合,要維護(hù)一個大系統(tǒng)可能要時刻關(guān)系各模塊之間的相互影響,這樣的系統(tǒng)即不清晰,也不夠可拓展,關(guān)鍵是要維護(hù)它的理解成本也高。
從可行性看,前端的特點(diǎn)在于用戶輸入的觸點(diǎn)非常多,但這不妨礙我們抽象標(biāo)準(zhǔn)輸入接口,比如用戶點(diǎn)擊按鈕或者輸入框是輸入,那鍵盤快捷鍵也是一種輸入方式,URL 參數(shù)也是一種輸入方式,在業(yè)務(wù)前置的表單配置也是一種輸入方式,如果輸入方式很多,對標(biāo)準(zhǔn)輸入的抽象就變得重要,使業(yè)務(wù)代碼的實(shí)際復(fù)雜度不至于真的膨脹到用戶使用的復(fù)雜度那么高。
不止輸入觸點(diǎn)多,前端系統(tǒng)的功能組合也非常多,比如圖形繪制軟件,畫布可以放任意數(shù)量的組件,每個組件有任意多的配置,組件之間還可以相互影響。這種系統(tǒng)屬于開放式系統(tǒng),用戶很容易試出開發(fā)者都未曾想到過的功能組合,有些時候開發(fā)者都驚嘆這些新的組合竟然能一起工作!用戶會感嘆軟件能力的強(qiáng)大,但開發(fā)者不能真的把這些功能組合一一嘗試來解決沖突,必須通過合理的分層抽象來保證功能組合的穩(wěn)定性。
其實(shí)這種挑戰(zhàn)也是計算機(jī)面臨的問題,如何設(shè)計一個通用架構(gòu)的計算機(jī),使上面可以運(yùn)行任何開發(fā)者軟件,且軟件之間可以相互獨(dú)立,也可以相互調(diào)用,系統(tǒng)還不容易產(chǎn)生 BUG。從這個角度來看,計算機(jī)的底層架構(gòu)設(shè)計對前端架構(gòu)設(shè)計是有參考意義的,大體上來說,計算機(jī)通過硬件、操作系統(tǒng)、軟件這個三個分層解決了要計算一切的難題。
馮·諾依曼體系就解決了硬件層面的問題。為了保證軟件層的可拓展性,通過 CPU、存儲、輸入輸出設(shè)備的抽象解決了計算、存儲、拓展的三個基本能力。再細(xì)分來看,CPU 也僅僅支持了三個基本能力:數(shù)學(xué)計算、條件控制、子函數(shù)。這使得計算機(jī)底層設(shè)計既是穩(wěn)定的,設(shè)計因素也是可枚舉的,同時擁有了強(qiáng)大的拓展能力。
操作系統(tǒng)也一樣,它不需要知道軟件具體是怎么執(zhí)行的,只需要給軟件提供一個安全的運(yùn)行環(huán)境,使軟件不會受到其他軟件的干擾;提供一些基本范式統(tǒng)一軟件的行為,比如多窗口系統(tǒng),防止軟件同時在一塊區(qū)域繪圖而相互影響;提供一些基礎(chǔ)的系統(tǒng)調(diào)用封裝給上層的語言進(jìn)行二次封裝,而考慮到這些系統(tǒng)調(diào)用封裝可能會隨著需求而拓展,進(jìn)而采用動態(tài)鏈接庫的方式實(shí)現(xiàn),等等。操作系統(tǒng)為了讓自身功能穩(wěn)定與可枚舉,對自己與軟件定義了清晰的邊界,無論軟件怎么拓展,操作系統(tǒng)不需要拓展。
回到前端業(yè)務(wù),想要保障一個復(fù)雜的繪圖軟件代碼清晰與好的可維護(hù)性,一樣需要從最底層穩(wěn)定的模塊開始網(wǎng)上,一步步構(gòu)建模塊間依賴關(guān)系,只有這樣,模塊內(nèi)邏輯才能可枚舉,模塊與模塊間才敢大膽的組合,各自設(shè)計各自的拓展點(diǎn),使整個系統(tǒng)最終擁有強(qiáng)大的拓展能力,但細(xì)看每個子模塊又都是簡單清晰、可枚舉可測試的代碼邏輯。
以 BI 系統(tǒng)舉例,劃分為組件、篩選、布局、數(shù)據(jù)模型四個子系統(tǒng)的話:
- 對組件系統(tǒng)來說,任何組件實(shí)現(xiàn)都可接入,這就使這個 BI 系統(tǒng)不僅可以展示報表,也可以展示普通的按鈕,甚至表單,可以搭建任意數(shù)據(jù)產(chǎn)品,或者可以搭建任意的網(wǎng)站,能力拓展到哪完全由業(yè)務(wù)決定。
- 對篩選系統(tǒng)來說,任何組件之間都能關(guān)聯(lián),不一定是篩選器到圖表,也可以是圖表到圖表,這樣就支持了圖表聯(lián)動。不僅是 BI 聯(lián)動場景,即便是做一個表單聯(lián)動都可以復(fù)用這個篩選能力,使整個系統(tǒng)實(shí)現(xiàn)統(tǒng)一而簡單。
- 對布局系統(tǒng)來說,不關(guān)心布局內(nèi)的組件是什么,有哪些關(guān)聯(lián)能力,只要做好布局就行。這樣畫布系統(tǒng)容易拓展為任何場景,比如生產(chǎn)效率工具、儀表盤、ppt 或者大屏,而對其他系統(tǒng)無影響。
- 對數(shù)據(jù)模型系統(tǒng)來說,其承擔(dān)了數(shù)據(jù)配置到 sql 查詢,最后映射到圖形通道展示的過程,它本身是對組件系統(tǒng)中,統(tǒng)計圖表類型的抽象實(shí)現(xiàn),因此雖然邏輯復(fù)雜,但也不影響其他子系統(tǒng)的設(shè)計。
從廣義角度看,前端業(yè)務(wù)代碼早就處于一系列架構(gòu)分層中,也就是編程語言與前端框架。編程語言與前端框架會自帶一些設(shè)計模式,以減少混用代碼范式帶來的溝通成本,其實(shí)架構(gòu)設(shè)計本身也要解決代碼一致性問題,所以這些內(nèi)容都是架構(gòu)設(shè)計的一環(huán)。
前端框架帶來的數(shù)據(jù)驅(qū)動特性本身就很大程度上解決了前端代碼在復(fù)雜應(yīng)用下可維護(hù)問題,大大降低了過程代碼帶來的復(fù)雜度。React 或 Vue 框架本身也起到了類似操作系統(tǒng)的操作,即定義上層組件(軟件規(guī)格)的規(guī)格,為組件渲染和事件響應(yīng)抹平瀏覽器差異(硬件差異),并提供組件渲染調(diào)度功能(軟件調(diào)度)。同時也提供了組件間變量傳遞(進(jìn)程通信),讓組件與組件間通信符合統(tǒng)一的接口。
但是沒有必要把每個組件都類比到進(jìn)程來設(shè)計,也就是說,組件與組件之間不用都通過通信方式工作。比較合適的類比粒度是模塊,把一個大模塊抽象為組件,模塊與模塊間互相不依賴,用數(shù)據(jù)通信來交流。小粒度組件就做成狀態(tài)無關(guān)的元件,注意相似功能的組件接口盡量保持一致,這樣就能體驗(yàn)到類似多態(tài)的好處。
所以話說回來,遵循前端框架的代碼規(guī)范不是一件可有可無的事情,業(yè)務(wù)架構(gòu)設(shè)計從編程語言和前端框架時就已經(jīng)開始了,如果一個組件不遵循框架的最佳實(shí)踐,就無法參與到更上層的業(yè)務(wù)架構(gòu)規(guī)劃里,最終可能導(dǎo)致項(xiàng)目混亂,或者無架構(gòu)可言。所以重視架構(gòu)設(shè)計從代碼規(guī)范就要開始。
所以前端架構(gòu)設(shè)計是必要的,那怎么做好前端架構(gòu)設(shè)計呢?這個話題太過于龐大,本次就從操作系統(tǒng)借鑒一些靈感,先談一談對分層與抽象的理解。
沒有絕對的分層
分層是架構(gòu)設(shè)計的重點(diǎn),但一個模塊在分層的位置可能會隨著業(yè)務(wù)迭代而變化,類比到操作系統(tǒng)舉兩個例子:
語音輸入現(xiàn)在由各個軟件自行提供,背后的語音識別與 NLP 能力可能來自各大公司的 AI 中臺,或者一些提供 AI 能力的云服務(wù)。但語音輸入能力成熟后,很可能會成為操作系統(tǒng)內(nèi)置能力,因?yàn)檎Z音輸入與鍵盤輸入都屬于標(biāo)準(zhǔn)輸入,只是語音輸入難度更大,操作系統(tǒng)短期難以內(nèi)置,所以目前發(fā)展在各個上層應(yīng)用里。
Go 語言的協(xié)程實(shí)現(xiàn)在編程語言層,但其對標(biāo)的線程實(shí)現(xiàn)在操作系統(tǒng)層,協(xié)程運(yùn)行在用戶態(tài),而線程運(yùn)行在內(nèi)核態(tài)。但如果哪天操作系統(tǒng)提供了更高效的線程,內(nèi)存占用也采用動態(tài)遞增的邏輯,說不定協(xié)程就不那么必要了。
按理說語音輸入屬于標(biāo)準(zhǔn)輸入的一部分,應(yīng)該實(shí)現(xiàn)在操作系統(tǒng)的通用輸入層,協(xié)程也屬于多任務(wù)處理的一部分,應(yīng)該實(shí)現(xiàn)在操作系統(tǒng)多任務(wù)處理層,但它們都被是現(xiàn)在了更上層,有的在編程語言層,有的在業(yè)務(wù)服務(wù)層。之所以產(chǎn)生了這些意外,是因?yàn)橥ㄓ幂斎胼敵鰧优c多任務(wù)處理層的需求并沒有想象中那么穩(wěn)定,隨著技術(shù)的迭代,需要對其拓展時,因?yàn)閮?nèi)置在底層不方便拓展,只能在更上層實(shí)現(xiàn)了。
當(dāng)然我們也要注意到的是,即便這些拓展點(diǎn)實(shí)現(xiàn)在更上層,但對軟件工程師來說并沒有特別大的侵入性影響,比如 goroutine,程序員并不接觸操作系統(tǒng)提供的 API,所以編程語言層對操作系統(tǒng)能力的拓展對程序員是透明的;語音輸入就有一點(diǎn)影響了,如果由操作系統(tǒng)來實(shí)現(xiàn),可能就變成與鍵盤輸出保持一致的事件結(jié)構(gòu)了,但由業(yè)務(wù)層實(shí)現(xiàn)就有無數(shù)種 API 格式了,業(yè)務(wù)流程可能也更加復(fù)雜,比如增加鑒權(quán)。
從計算機(jī)操作系統(tǒng)的例子我們可以學(xué)習(xí)到兩點(diǎn):
- 站在分層合理性視角對輸入做進(jìn)一步的抽象與整合。比如將語音識別封裝到標(biāo)準(zhǔn)的輸入事件,讓其邏輯上成為標(biāo)準(zhǔn)輸入層。
- 業(yè)務(wù)架構(gòu)的設(shè)計必然也會遇到分層不滿足業(yè)務(wù)拓展性的場景。
業(yè)務(wù)分層與硬件、操作系統(tǒng)不同的是,業(yè)務(wù)分層中,幾乎所有層都方便修改與拓展,因此如果遇到分層不合理的設(shè)計,最好將其移動到應(yīng)該歸屬的層。操作系統(tǒng)與硬件層不方便隨意拓展的原因是版本更新的頻率和軟件更新的頻率不匹配。
同時,也要意識到分層需要一個演進(jìn)過程,等新模塊穩(wěn)定后再移動到其歸屬所在層可能更好,因?yàn)閺纳蠈优驳降讓右馕吨啾荒K共享使用,就像我們不會輕易把軟件層某個包提供的函數(shù)內(nèi)置到編程語言一樣,也不會隨意把編程語言實(shí)現(xiàn)的函數(shù)內(nèi)置到操作系統(tǒng)內(nèi)置的系統(tǒng)調(diào)用。
在前端領(lǐng)域的一個例子是,如果一個搭建平臺項(xiàng)目中已經(jīng)有了一套組件元信息描述,最好先讓其在業(yè)務(wù)代碼里跑一段時間,觀察一下元信息定義的屬性哪些有缺失,哪些是不必要的,等業(yè)務(wù)穩(wěn)定一段時間后,再把這套元信息運(yùn)行時代碼抽成一個通用包提供給本業(yè)務(wù),甚至其他業(yè)務(wù)使用。但即便這個能力沉淀到了通用包,也不代表它就是永遠(yuǎn)不能被迭代的,操作系統(tǒng)的多任務(wù)管理都有協(xié)程來挑戰(zhàn),何況前端一個抽象包的能力呢?所以要慎重抽象,但抽象后也要敢于質(zhì)疑挑戰(zhàn)。
沒有絕對的抽象
抽象粒度永遠(yuǎn)是架構(gòu)設(shè)計的難題。
計算機(jī)把一切都理解為數(shù)據(jù)。計算結(jié)果是數(shù)據(jù),執(zhí)行程序的代碼也是數(shù)據(jù),所以 CPU 只要專注于對數(shù)據(jù)的計算,再加上存儲與輸入輸出,就可以完成一切工作。想一想這樣抽象的偉大之處:所有程序最終對計算機(jī)來說都是這三個概念,CPU 在計算時無需關(guān)心任何業(yè)務(wù)含義,這也使得它可以計算任何業(yè)務(wù)。
另一個有爭議的抽象是 Unix 一切皆文件的抽象,該抽象使文件、進(jìn)程、線程、socket 等管理都抽象為文件的 API,且都擁有特定的 “文件路徑”,比如你甚至可以通過 /proc 訪問到進(jìn)程文件夾, ls 可以看到所有運(yùn)行的進(jìn)程。當(dāng)然進(jìn)程不是文件,這只是說明了 Unix 的一種抽象哲學(xué),即 “文件” 本身就是一種抽象,開發(fā)和可以用理解文件的方式理解一切事物,這帶來了巨大的理解成本降低,也使許多代碼模式可以不關(guān)心具體資源類型。但這樣做的爭議點(diǎn)在于,并不是一切資源都適合抽象成文件,比如輸入輸出中的顯示器,它作為一個呈現(xiàn)五彩繽紛像素點(diǎn)的載體,實(shí)在難以用文件系統(tǒng)來統(tǒng)一描述。
計算機(jī)設(shè)計與操作系統(tǒng)設(shè)計已經(jīng)給了我們很明顯的啟發(fā),即一切能抽象的都要盡可能的抽象,如此才能提高系統(tǒng)各模塊內(nèi)的穩(wěn)定性。但從如 Unix 一切皆文件的抽象來看,有時候的技術(shù)抽象難免被當(dāng)時的業(yè)務(wù)需求所局限,當(dāng)輸入輸出設(shè)備的種類增加后,這種極致的抽象未必能永遠(yuǎn)合適。但永遠(yuǎn)要相信抽象,因?yàn)榧偃羲匈Y源都可以被文件抽象所描述,且使用起來沒有不便捷的地方,為什么還要造其他的抽象概念呢?如無必要勿增實(shí)體。
比如 BI 場景的篩選、聯(lián)動、下鉆場景是否都能抽象為組件與組件間的聯(lián)動關(guān)系呢?如果一套標(biāo)準(zhǔn)聯(lián)動設(shè)計可以解決這三個場景,那自然不需要為某個具體場景單獨(dú)引入概念。從原始場景來看,無論篩選、聯(lián)動還是下鉆場景都是修改組件的取數(shù)參數(shù)以改變查詢條件,我們就可以抽象出一種組件間聯(lián)動的規(guī)范,使其可以驅(qū)動取數(shù)參數(shù)的變化,但未來需求可能引入更多的可能性,如在篩選時觸發(fā)一些額外的追加分析查詢,此時之前的抽象就收到了挑戰(zhàn),我們需要權(quán)衡維持統(tǒng)一性的收益與通用接口不適用于特殊場景帶來成本之間的平衡。
抽象的方式是無數(shù)的,哪種更好取決于業(yè)務(wù)如何變化,不用過于糾結(jié)完美的抽象,就連 Unix 一切皆文件的最基礎(chǔ)抽象都備受爭議,業(yè)務(wù)抽象的穩(wěn)定性肯定會更差,也更需要隨著需求變化而調(diào)整。
總結(jié)
我們從計算機(jī)與操作系統(tǒng)的架構(gòu)設(shè)計出發(fā),探討了前端架構(gòu)設(shè)計的必要性,并從分層與抽象兩個角度分析了架構(gòu)設(shè)計時的考量,希望你在架構(gòu)設(shè)計遇到拿捏不定的問題時,可以向下借助計算機(jī)的架構(gòu)設(shè)計獲得一些靈感或支持。