PS:本文所針對(duì)的場(chǎng)景,都是復(fù)雜業(yè)務(wù)場(chǎng)景下的 Web 應(yīng)用糯彬。簡(jiǎn)單的 Web 應(yīng)用不適合復(fù)雜的架構(gòu)模式凭语,它為帶來巨大的成本。
在接觸了領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的概念撩扒,其中關(guān)于核心域的想法讓人頗為激動(dòng)叽粹。而在微服務(wù)架構(gòu)中,核心域是一個(gè)或者多個(gè)服務(wù)的域却舀,而位于核心域的核心則是領(lǐng)域模型。簡(jiǎn)單地來說锤灿,對(duì)于一個(gè)系統(tǒng)來說挽拔,它的核心的核心的 ”核心“ 就是:領(lǐng)域模型。
過去但校,當(dāng)我們談及領(lǐng)域模型的時(shí)候螃诅,我們往往將其置于 Web 后端的領(lǐng)域。當(dāng)我 GET 了一些基本的理念之后,便嘗試整合到前端架構(gòu)中术裸。去年倘是,我開始了 在項(xiàng)目上的第一次嘗試,并在 GitHub 上創(chuàng)建了 clean-frontend 項(xiàng)目袭艺,它以 Angular 作為示例搀崭,介紹了如何開發(fā)一個(gè)整潔前端架構(gòu)的前端應(yīng)用。
引子 1:模型一致化
我習(xí)慣于在公司的項(xiàng)目中引入成熟的框架猾编,從幾年前的 Angular.js 到近幾年的 Angular瘤睹,完整的框架能為企業(yè)帶來更少的維護(hù)成本,從而降低軟件開發(fā)成本答倡。幾年前轰传,我們引入了 Angular 之后,便開始大量地 TypeScript 項(xiàng)目實(shí)踐瘪撇。作為一個(gè)支持靜態(tài)類型的語言获茬,它非常適合于開發(fā)大型應(yīng)用、企業(yè)應(yīng)用倔既,在這一點(diǎn)上你可以從 React恕曲、Vue 等框架建議使用 TypeScript 看到一種趨勢(shì)。
采納了 TypeScript 之后叉存,當(dāng)前端從后端獲取到數(shù)據(jù)之后码俩,那么它需要將 JSON 數(shù)據(jù)映射到對(duì)應(yīng)的 interface。如此一來歼捏,前端的模型也就有了對(duì)應(yīng)的領(lǐng)域模型稿存。而這個(gè)數(shù)據(jù)模型和后端的數(shù)據(jù)模型應(yīng)該是完成一致的,但是實(shí)際上瞳秽,它往往是落后的瓣履。后端的 API 發(fā)生的變更之后,才需要前端同步去修改模型练俐。
而當(dāng)我作為一個(gè)前端的 Tech Lead 來考慮這個(gè)問題的時(shí)候袖迎,我首先想到的是:讀取后端的 Java 代碼,然后生成對(duì)應(yīng)的前端模型腺晾。在沒有造 Chapi 的輪子之前燕锥,通過 TypeScript Compiler 轉(zhuǎn)換獲得對(duì)應(yīng)的類型,是我覺得比較靠譜的方案悯蝉。在有了 Chapi 之后归形,我覺得它們都是小問題了。
不過鼻由,然后我一想這樣做的意義并不大暇榴,還不如:套用后端的契約測(cè)試厚棵,造一個(gè)前端的自動(dòng)化契約測(cè)試,即:mest蔼紧。它通過 API 返回?cái)?shù)據(jù)和 TypeScript 的 Interface 來完成對(duì)于契約的測(cè)試婆硬。一個(gè)簡(jiǎn)單的測(cè)試數(shù)據(jù)如下:
url,interface
https://phodal.github.io/mest-test/error.json,mock/IError.ts
通過 HTTP 請(qǐng)求獲取對(duì)應(yīng)的測(cè)試數(shù)據(jù),再將其與本地的模型進(jìn)行比較奸例。
不過呢彬犯,我們還要使用 JavaScript 語言重寫部分 Java 代碼,那么我們?yōu)槭裁床灰?JavaScript / TypeScript 重寫一切呢哩至?
引子 2:JavaScript / TypeScript 重寫一切
遺憾的是躏嚎,使用 JavaScript 編寫后端應(yīng)用(BFF、膠水層除外)菩貌,在大部分的大公司是比較難的卢佣。內(nèi)部的生態(tài)鏈和運(yùn)維影響了技術(shù)決策,使用 C++ 不香嗎箭阶,使用 JavaScript 會(huì)存在無人在背后支持虚茶。
純 Node.js 后端應(yīng)用
在過去的幾年里,我建議:小公司的后端應(yīng)用不要使用 JavaScript / TypeScript 編寫仇参,因?yàn)檫\(yùn)維嘹叫、監(jiān)控、APM 等生態(tài)尚不夠完善诈乒。小公司應(yīng)該優(yōu)先投資于業(yè)務(wù)領(lǐng)域罩扇,在基礎(chǔ)設(shè)施的投入見效比較短,除非能控制好開發(fā)人員的流動(dòng)性怕磨。
不過喂饥,不管怎樣,已經(jīng)有大量的小公司因?yàn)槿肆Τ杀镜脑虺辏呀?jīng)使用上了 Node.js 來開發(fā)后端應(yīng)用员帮。
Serverless 應(yīng)用
而隨著 JavaScript 系生態(tài)的完善,基礎(chǔ)設(shè)施已經(jīng)不是會(huì)成為小公司的負(fù)擔(dān)导饲,我便覺得這是一個(gè)好的時(shí)機(jī)捞高。不過,我指的是采用 Serverlesss 架構(gòu)渣锦,而非自建 JavaScript 系生態(tài)硝岗。Serverless 不僅幫助小企業(yè)解決了基礎(chǔ)設(shè)施的問題,還能為小企業(yè)降低軟件運(yùn)維成本袋毙。一旦企業(yè)做大之后辈讶,也可以自建采用 OpenFaas 等開源方案解決 Serverless 的供應(yīng)商鎖定問題。
不論是 Node.js 后端應(yīng)用還是 Serverless 應(yīng)用娄猫,因?yàn)槭褂玫氖峭环N語言贱除,我們可以輕松地在前后端之間共享代碼,可以是 Git submodule媳溺、NPM 包月幌、遠(yuǎn)程等等的方式。
引子 3:純編譯到 JavaScript
市面上有各種各樣支持 compile to js 的語言悬蔽、框架扯躺,諸如于 Kotlin.js、Scala.js蝎困、Python.js 等等录语。
對(duì)于小前端的應(yīng)用來說,這種架構(gòu)非常的不錯(cuò)禾乘。它相當(dāng)于是漸進(jìn)式的系統(tǒng)架構(gòu)方案澎埠,當(dāng)前采用了主流的前端框架,而非傳統(tǒng)的后端渲染機(jī)制始藕,并統(tǒng)一了技術(shù)棧蒲稳,降低了組織內(nèi)部的學(xué)習(xí)成本。不過伍派,它帶來額外的調(diào)試因素江耀,畢竟每多一層封裝,系統(tǒng)的復(fù)雜度就需要 * 2诉植。
而為了讓框架的使用者支持不同的框架 React祥国、Angular、Vue晾腔,這個(gè)框架還需要提供這些框架的 bind 或者是 wrapper舌稀,以提升框架使用者的幸福感。
但是建车,我們的挑戰(zhàn)依然是復(fù)雜的前端應(yīng)用扩借,以及它難以消除交互的復(fù)雜度。
引子 4:共享領(lǐng)域模型/模式庫(kù)
開始之前缤至,我不得不強(qiáng)調(diào)一潮罪,領(lǐng)域模型是一種包含數(shù)據(jù)和行為的抽血模型。
編譯到 JavaScript 是一種輕前端的方案领斥,而對(duì)于重前端的項(xiàng)目來說嫉到,它們完成可以采用一種新的模式:共享領(lǐng)域模型。即將領(lǐng)域模型作為模式庫(kù)月洛,提供給前后端一起使用何恶。即,我們只需要編譯所需要的部分嚼黔。
這在我使用了 Kotlin 的多平臺(tái)技術(shù)(multiplatform)重寫了 Chapi 的 domain 層之后细层,我意識(shí)到了這是一個(gè)非常迷人的方案惜辑。我即使用在前端代碼中使用 Chapi 的領(lǐng)域模型,我還需要在后端的代碼中使用這套模型疫赎。原先盛撑,我需要手動(dòng)翻譯一行行代碼,現(xiàn)在我并不需要這樣的一個(gè)步驟捧搞。只需要在 pom.xml抵卫、build.gradle 或者是 package.json 中引入依賴及其對(duì)應(yīng)的版本即可。
在引入了這部分的代碼之后胎撇,我們?cè)訇P(guān)注于 UI 交互部分即可介粘。
引子 5:領(lǐng)域模型編譯到 WASM
考慮到并非所有的語言都能支持 compile to JavaScript,一種頗為有效的方式就是使用 WASM晚树。
WebAssembly 或稱 wasm 是一個(gè)實(shí)驗(yàn)性的低端編程語言姻采,應(yīng)用于瀏覽器內(nèi)的客戶端。WebAssembly 將讓開發(fā)者能運(yùn)用自己熟悉的編程語言編譯题涨,再藉虛擬機(jī)引擎在瀏覽器內(nèi)運(yùn)行偎谁。 —— 維基百科
我嘗試將使用 Go 編寫的 Coca 編譯成 WASM,但是遇到一系列的問題(我已經(jīng)忘了)纲堵,體積似乎是個(gè)問題巡雨,所以我嘗試使用 Rust 去構(gòu)建另外一種可能性。Rust 官方提供了 Rust Webpack Template席函,因此我可以將其集成到我現(xiàn)有的前端應(yīng)用中铐望。只是呢,似乎還沒有人會(huì)使用 Rust 去編寫后端應(yīng)用茂附。
但是 WASM 提供了一種更友好的方式正蛙,即我們不需要重寫現(xiàn)有的代碼,而是只需要添加一些代碼营曼,便可以將現(xiàn)有的后端模型代碼提到給前端使用乒验。并且,與混淆后的 JavaScript 相比蒂阱,它看上去更加安全 —— 學(xué)習(xí)成本更高一些锻全。
引子 6:ComponentLess
在研究 Serverless 和微前端的期間,我突然有了一點(diǎn)想法录煤,我對(duì)于客戶端領(lǐng)域的 Serverless 式架構(gòu)有了一些基本的構(gòu)想鳄厌,叫:ComponentLess。盡管有了一些基礎(chǔ)的理念妈踊,但是還缺乏一個(gè)真實(shí)可用的 Demo了嚎,所以我并沒有定義出什么是 ComponentLess。
起先,我以為無代碼編程是一個(gè) ComponentLess 方向歪泳,但是一研究發(fā)現(xiàn)并不是萝勤。無代碼編程傾向于可視化編程,而 ComponentLess 傾向于使用 DSL 編程夹囚。就這一點(diǎn)來說纵刘,我便偏向于使用 Web Components + WAM 技術(shù)來構(gòu)建新的前端架構(gòu)。
ComponentLess
從單體應(yīng)用轉(zhuǎn)向微服務(wù)架構(gòu)的一大特質(zhì)是荸哟,組件(非單指 UI 組件,可以視為服務(wù))由函數(shù)調(diào)用轉(zhuǎn)向了 HTTP 調(diào)用瞬捕。而 Serverless 進(jìn)一步地將微服務(wù)的服務(wù)級(jí)別 HTTP 調(diào)用鞍历,細(xì)化為函數(shù)級(jí)別的 HTTP 調(diào)用。
對(duì)于前端領(lǐng)域來說肪虎,也是如此劣砍。微前端將單體應(yīng)用拆分一個(gè)個(gè)的獨(dú)立運(yùn)行前端應(yīng)用,我們可以隨意地組合這些應(yīng)用扇救。進(jìn)一步地刑枝,結(jié)合諸如于 Web Component 這樣的組件級(jí)方案,便可以將拆分細(xì)分為到 UI 組件的粒度迅腔。我們可以使用 (HTML Imports 已遺棄)script 標(biāo)簽從遠(yuǎn)程導(dǎo)入:
<script type="module" src="my-element.js"></script>
<my-element></my-element>
因此装畅,在未來,不論是前端開發(fā)人員沧烈,還是開發(fā)人員掠兄,都可以通過集成組件的方式來開發(fā)應(yīng)用。也就是說锌雀,我們只需要關(guān)注于編寫核心業(yè)務(wù)代碼即可蚂夕,剩下的部分可以通過一些特殊的方式來實(shí)現(xiàn)顶霞。
DSL 抽象化代碼
ComponentLess + Serverless 是代碼即基礎(chǔ)設(shè)施開始的一個(gè)標(biāo)志姐直。當(dāng)代碼開始作為基礎(chǔ)設(shè)施的一部分時(shí),代碼便需要以某種方式才能組合到一起谐檀。在 Serverless Framework 中惩歉,開發(fā)人員通過配置接入服務(wù)端所使用的基礎(chǔ)信息等脂。而 YAML 配置本身也是 DSL 的一種,缺乏靈活度柬泽,但是使用非常簡(jiǎn)單慎菲。
事實(shí)上,這是兩個(gè)選擇:
- 配置 + 編程語言锨并。 上手容易露该,遷移難
- DSL。 上手復(fù)雜第煮,易于遷移
但是解幼,無論如何我還是如何 DSL抑党,它聽上去有著更豐富的 KPI。至于如何抽象化基礎(chǔ)設(shè)計(jì)代碼撵摆,可以參考《云研發(fā):研發(fā)即代碼》一文底靠。
模型復(fù)用
不論是 ComponentLess 還是 Serverless,它們都是由函數(shù)特铝、UI 組件變?yōu)橐粋€(gè)獨(dú)立可運(yùn)行的單元暑中。為了與別人交互,它需要包含輸入和輸出鲫剿。而輸入和輸出本身是需要一個(gè)數(shù)據(jù)模型作為支撐的鳄逾,以此才能完成整個(gè)系統(tǒng)的穩(wěn)定性。
而這個(gè)話題已經(jīng)回到我們開頭所討論的內(nèi)容里灵莲。
前后端分離將死雕凹?
在所有的引子里, 我們已經(jīng)準(zhǔn)備了所有的論據(jù)政冻,所以只需要:
- 使用可以跨前后端的語言枚抵,構(gòu)建領(lǐng)域模型
- 將后端服務(wù)、前端設(shè)施細(xì)化為更小的組件
- 設(shè)計(jì) DSL 將領(lǐng)域模型轉(zhuǎn)換到特定平臺(tái)的代碼
你就可以殺死前后端分離明场,就是這么簡(jiǎn)單汽摹。
前后端分離將死,不是現(xiàn)在榕堰,但是可能在五年后開始竖慧。
你說呢?
其它
架構(gòu)逆屡,沒有終點(diǎn)圾旨。