為什麼在web前端很少有人會提到分層架構,例如經典MVC架構,這是因為瀏覽器誕生之初就只是作為一個後端數據的GUI渲染器。也就是説整體來看,web1.0時代的整個web前端工程就是一個View層,而Model和Controller就是指後端,所以根本無需在web前端工程中去提什麼MVC。
然而web生態發展到今天,瀏覽器越來越強大,賦能越來越多,甚至不亞於一個小型操作系統,這時候的Web前端早已不是當初簡單的數據渲染器,隨着PWA、小程序、快應用的推廣,WebAPP已經和傳統的“富客户端”沒什麼兩樣了。那麼在這種趨勢下,如果我們的Web前端架構還停留在View的時代,那麼顯然是落伍的。
View是最直觀的、最原始的驅動源,解決了所有View的問題也就實現了WebApp的所有功能,前端開發人員的全部工作就是一個又一個的編寫Component。正是因為這種以View為核心的架構思維,導致我們只為解決表層問題而架構,不去思考頂層設計,這也是很多Web前端工程經過幾輪迭代和人員轉手後,很難繼續維護下去的根本原因。
近年來涌現出一些優秀的UI渲染框架,比如React/Vue/Anglar,它們很強大,可以幹很多分內和分外的事情。正因為如此,更造成了開發者的惰性,將所有邏輯都直接以最原始的方式寫在View中,不管是Mode還是Controller,不管是業務邏輯、還是渲染邏輯、還是存儲邏輯、還是通訊邏輯、還是路由邏輯...所有各類邏輯疊加各種UI生命週期,全部都耦合在一個Component中。
從視圖驅動到領域驅動
是時候改變我們的架構思維,從視圖驅動昇華到領域驅動。雖然視圖驅動是最直觀也是最簡單的一種架構模式,但是我們不僅要解決問題,還要思考如何優雅的解決問題,這也好比是排版和設計的區別吧!
或者説,在項目野蠻生長的混沌初期你可以從下往上構建,所建即所得。但到了某個重構的時間點,你還是得站在頂部進行上層設計。
當然,這裏有個默認前提,那就是需要長期維護的中大型項目,小而快的項目無需談各種架構模式...
將UI框架拉下神壇
UI不是工程的全部,UI的指責只有2個:輸入與輸出,僅此而已。所以即便React/Vue再強大,你也應當僅把它們當成一個GUI工具,類似於Java Swing和.Net WPF,而非整個工程React/Vue一把梭。
~~ 敲黑板:UI只是指令的收集者、傳達者、反饋者,而不應當成為指令的執行者。
應用的核心是業務邏輯
現在問你一個問題:
如果沒有UI,你的應用可以通過命令來驅動嗎?
你可能會懟我説,用户怎麼可能會通過命令來使用我的應用,沒有這樣的業務場景,你這個假設是毫無意義的。然而,這種場景或許並不是為用户而設,而是為合作伙伴而設、為測試工具而設、為日誌分析而設、為今後的擴展和生態建設而設...
再問你一個問題:
- UI説:應用要改版,皮膚、交互、頁面組織都要調整,要多久?
- 產品説:把H5改改,做成小程序、APP吧,要多久?
- 經理説:React人太難招了,要不我們換成Vue吧,要多久?
- Leader説:Vue3出來了,我們升級為最新版吧,要多久?
- 架構師説:Svelte快到飛,我們重構為Svelte吧,要多久?
業務邏輯不變,僅調整UI和其運行平台,問你要多久?這個時候,如果你的全部邏輯都是耦合在某種特定的UI框架中的,那你只能是哀默之心大於死...
UI邏輯與業務邏輯分離
從早期的前後端一體化,到後面的前後端邏輯分離,在到這裏我重提UI邏輯與業務邏輯分離,都只是順着2件事件在做:讓需要內聚的更內聚,讓可以解耦的更鬆散。
- 業務邏輯抽象,UI邏輯具體:業務邏輯是抽象的數據模型,而UI邏輯則是抽象事物逐層泛化後的具體表現,它們兩個本身就不再同一個Level上。
- 業務邏輯通用,UI邏輯特殊:UI邏輯通常需要藉助於某個具體的UI框架而表達,而UI框架通常都會與運行平台息息相關,同時也會加入自己的各種各種限制、約束、約定、魔術方法等。所以形象來説,前者是JS國度的普通話,而後者則是方言。
- 業務邏輯直觀,UI邏輯隱晦:UI渲染本身就是一件很複雜的事情,其間涉及到各種組件的各種生命週期、創建、銷燬、更新、重繪等等、在加上各種優化手段造成的心智負擔。所以你會發現某些業務邏輯放在
Redux或是Vuex等純狀態管理框架中,比使用組件內部的Hooks去維護更簡單和直觀。儘管Hooks也可以模擬這些Flux框架,但是不及前者有條理,比如測試、監控、分析、回滾一個Action多容易,你測試、監控、分析、回滾一個Hooks試試... - 業務邏輯穩定、UI邏輯多變:UI/UE太感性、太靈活了,帶有很強的個人主觀色彩,經常會被優化、修改、甚至推翻,不像業務邏輯那麼穩定。這也是要將UI邏輯和業務邏輯分開的重要原因,如果你將業務邏輯和UI邏輯寫在一起,你本來穩定的代碼將受到不穩定代碼的嚴重騷撓。
鐵打的MV流水的C
現在提MVC是不是過時了?非也,MVC是其實一種最簡單的哲學思想,Model代表抽象事物,View代表具體事物,而Controller則是代表如何在抽象事物和具體事物之間泛化。所以MVC是一種思想,而不是特指某種框架,過時的只會是框架,而非思想。
近年來各種演化如:MVP、MVVM、MVI等等,其實都是在圍繞C做文章,最終的目的無非就是更好的隔離M和V,也就是千方百計分離UI邏輯和業務邏輯。
Flux架構其實也是一種MVC,而Redux/Vuex/Pinia、以及本人的框架Elux,都可以看作它的變種。下面我們就看看如何利用Flux架構來分離UI邏輯與業務邏輯:
從State到Model
MVVM應用中充斥着狀態,有的用來描述Component的內部狀況,它與Component唇齒相依,跟隨Component誕生和銷燬,這就是我們常説的ComponentState。\
還有一些狀態,用來描述業務狀態,與具體哪個Component沒有直接關係,不存在複用與銷燬,我們可以稱它為Model。
- ComponentState 是UI邏輯,應當封裝在Component裏面,外界也無需知道。
- Model 是業務邏輯,反映整個APP的狀況,與Component無關,應當由Flux框架來統一管理。
從Event到Action
用户通過UI界面產生的人機交互事件,我們習慣叫"Event事件",而"Event事件"背後的業務目的我們可以叫"Action動作",它們一個是因一個是果,一個是表一個本。
- 處理 Event 的 Handler 是UI邏輯,應當寫在UI組件中
- 處理 Action 的 Handler 是業務邏輯,應當寫在Controller裏面
舉個具體的例子吧:"SubmitLoginForm|提交登錄表單",通常要完成如下邏輯:
- 驗證輸入是否有效
- 驗證當前用户是否已經登錄
- 請求後端API,並等待返回
- 如果成功,保存用户信息,並跳回原頁面
- 如果失敗,提示錯誤,並留在原地
這是一個業務動作,因為它可以不依賴哪個具體UI而運行,用户可能通過“onClick事件”點擊登錄按鈕來觸發,也可能通過“onKeyPress”按下回車鍵來觸發,甚至你可以直接讓用户通過“Login命令”來觸發。
所以“onSubmitLoginForm()”應當寫在Controller而非UI組件中。\
UI組件中只有"onLoginButtonClick()"或"onEnterKeyPress()",而它們往往也就一句話,就是Dispatch一個Action來觸發Controller中的“onSubmitLoginForm()”
將業務邏輯移出UI組件,這樣UI層就變薄了,迴歸到了它的本質:只負責收集業務動作,不負責處理它。
改良Flux框架
傳統的Flux框架也有痛點:
- 全局中心化管理導致邏輯過於集中;
- 單實例、不銷燬容易造成信息累積爆炸;
- DispatchAction機制過於簡單,不適合處理前因後果的長流程業務。
Elux正是針對以上痛點進行了改良:
- 雖然堅持全局中心化管理,但Elux提出“微模塊”的概念,將應用拆分成獨立自治的一個個“微模塊”,每個微模塊僅處理自己領域內的事情。
- 不再單實例,每次路由變化都會產生一個新的空白Store,然後重新挑選有用的狀態掛載,類似一種垃圾回收機制。
- 提出了ActionBus的概念,讓Action作為Model中的事件來廣播。
- 讓Action的處理鏈條具備“協程”機制,更好的協同各業務動作之間的關聯。
Elux踐行的分層而治
正是因為遵循了輕UI、重Model的設計思想,讓Elux可以掛接React/Vue等各種不同的UI框架,它們已經變得沒那麼重要了。
正式因為分離了UI邏輯和業務邏輯,讓Elux可以用一種工程模式開發Web(瀏覽器)、Micro(微前端)、SSR(服務器渲染)、MP(小程序)、APP(手機應用)。
此致!歡迎交流:https://eluxjs.com
最後也想跟大家交流一下前端“微模塊”的話題:從"微前端"到“微模塊”