软件架构设计案例_透过现象看本质:常见的前端架构风格和案例
所謂軟件架構(gòu)風(fēng)格,是指描述某個特定應(yīng)用領(lǐng)域中系統(tǒng)組織方式的慣用模式。架構(gòu)風(fēng)格定義一個詞匯表和一組約束,詞匯表中包含一些組件及連接器,約束則指出系統(tǒng)如何將構(gòu)建和連接器組合起來。軟件架構(gòu)風(fēng)格反映了領(lǐng)域中眾多系統(tǒng)所共有的結(jié)構(gòu)和語義特性,并指導(dǎo)如何將系統(tǒng)中的各個模塊和子系統(tǒng)有機的結(jié)合為一個完整的系統(tǒng)
沒多少人能記住上面的定義,需要注意的是本文不是專業(yè)討論系統(tǒng)架構(gòu)的文章,筆者也還沒到那個水平. 所以暫時沒必要糾結(jié)于什么是架構(gòu)模式、什么是架構(gòu)風(fēng)格。在這里尚且把它們都當(dāng)成一個系統(tǒng)架構(gòu)上的套路, 所謂的套路就是一些通用的、可復(fù)用的,用于應(yīng)對某類問題的方式方法. 可以理解為類似“設(shè)計模式”的東西,只是解決問題的層次不一樣。
透過現(xiàn)象看本質(zhì),本文將帶你領(lǐng)略前端領(lǐng)域一些流行技術(shù)棧背后的架構(gòu)思想。直接進入正題吧
文章大綱
- 分層風(fēng)格
- Virtual DOM
- Taro
- 管道和過濾器
- 中間件(Middleware)
- 事件驅(qū)動
- MV*
- 家喻戶曉的MVC
- Redux
- 復(fù)制風(fēng)格
- 微內(nèi)核架構(gòu)
- 微前端
- 組件化架構(gòu)
- 其他
- 擴展閱讀
分層風(fēng)格
沒有什么問題是分層解決不了,如果解決不了, 就再加一層 —— 魯迅 不不,原話是: Any problem in computer science can be solved by anther layer of indirection.
分層架構(gòu)是最常見的軟件架構(gòu),你要不知道用什么架構(gòu),或者不知道怎么解決問題,那就嘗試加多一層。
一個分層系統(tǒng)是按照層次來組織的,每一層為在其之上的層提供服務(wù),并且使用在其之下的層所提供的服務(wù). 分層通常可以解決什么問題?
- 是隔離業(yè)務(wù)復(fù)雜度與技術(shù)復(fù)雜度的利器. 典型的例子是網(wǎng)絡(luò)協(xié)議, 越高層越面向人類,越底層越面向機器。一層一層往上,很多技術(shù)的細節(jié)都被隱藏了,比如我們使用HTTP時,不需要考慮TCP層的握手和包傳輸細節(jié),TCP層不需要關(guān)心IP層的尋址和路由。
- 分離關(guān)注點和復(fù)用。減少跨越多層的耦合, 當(dāng)一層變動時不會影響到其他層。例如我們前端項目建議拆分邏輯層和視圖層,一方面可以降低邏輯和視圖之間的耦合,當(dāng)視圖層元素變動時可以盡量減少對邏輯層的影響;另外一個好處是, 當(dāng)邏輯抽取出去后,可以被不同平臺的視圖復(fù)用。
關(guān)注點分離之后,軟件的結(jié)構(gòu)會變得容易理解和開發(fā), 每一層可以被復(fù)用, 容易被測試, 其他層的接口通過模擬解決. 但是分層架構(gòu),也不是全是優(yōu)點,分層的抽象可能會丟失部分效率和靈活性, 比如編程語言就有'層次'(此例可能不太嚴謹),語言抽象的層次越高,一般運行效率可能會有所衰減:
分層架構(gòu)在軟件領(lǐng)域的案例實在太多太多了,咱講講前端的一些'分層'案例:
我自己是一名從事了多年開發(fā)的web前端老程序員,目前辭職在做自己的web前端私人定制課程,今年年初我花了一個月整理了一份最適合2019年學(xué)習(xí)的web前端學(xué)習(xí)干貨,各種框架都有整理,送給每一位前端小伙伴,想要獲取的可以關(guān)注我的頭條號并在后臺私信我:前端,即可免費獲取。
Virtual DOM
前端石器時代,我們頁面交互和渲染,是通過服務(wù)端渲染或者直接操作DOM實現(xiàn)的, 有點像C/C++這類系統(tǒng)編程語言手動操縱內(nèi)存. 那時候JQuery很火:
后來隨著軟硬件性能越來越好、Web應(yīng)用也越來越復(fù)雜,前端開發(fā)者的生產(chǎn)力也要跟上,類似JQuery這種命令式的編程方式無疑是比較低效的. 盡管手動操作 DOM 可能可以達到更高的性能和靈活性,但是這樣對大部分開發(fā)者來說太低效了,我們是可以接受犧牲一點性能換取更高的開發(fā)效率的.
怎么解決,再加一層吧,后來React就搞了一層VirtualDOM。我們可以聲明式、組合式地構(gòu)建一顆對象樹, 然后交由React將它映射到DOM:
一開始VirtualDOM和DOM的關(guān)系比較曖昧,兩者是耦合在一起的。后面有人想,我們有了VirtualDOM這個抽象層,那應(yīng)該能多搞點別的,比如渲染到移動端原生組件、PDF、Canvas、終端UI等等。
后來VirtualDOM進行了更徹底的分層,有著這個抽象層我們可以將VirtualDOM映射到更多類似應(yīng)用場景:
所以說 VirtualDOM 更大的意義在于開發(fā)方式的轉(zhuǎn)變: 聲明式、 數(shù)據(jù)驅(qū)動, 讓開發(fā)者不需要關(guān)心 DOM 的操作細節(jié)(屬性操作、事件綁定、DOM 節(jié)點變更),換句話說應(yīng)用的開發(fā)方式變成了view=f(state), 這對生產(chǎn)力的解放是有很大推動作用的; 另外有了VirtualDOM這一層抽象層,使得多平臺渲染成為可能。
當(dāng)然VirtualDOM或者React,不是唯一,也不是第一個這樣的解決方案。其他前端框架,例如Vue、Angular基本都是這樣一個發(fā)展歷程。
上面說了,分層不是銀彈。我們通過ReactNative可以開發(fā)跨平臺的移動應(yīng)用,但是眾所周知,它運行效率或者靈活性暫時是無法與原生應(yīng)用比擬的。
Taro
Taro 和React一樣也采用分層架構(gòu)風(fēng)格,只不過他們解決的問題是相反的。React加上一個分層,可以渲染到不同的視圖形態(tài);而Taro則是為了統(tǒng)一多樣的視圖形態(tài): 國內(nèi)現(xiàn)如今市面上端的形態(tài)多種多樣,Web、React-Native、微信小程序...... 針對不同的端去編寫多套代碼的成本非常高,這種需求催生了Taro這類框架的誕生. 使用 Taro,我們可以只書寫一套代碼, 通過編譯工具可以輸出到不同的端:
(圖片來源: 多端統(tǒng)一開發(fā)框架 - Taro)
管道和過濾器
在管道/過濾器架構(gòu)風(fēng)格中,每個組件都有一組輸入和輸出,每個組件職責(zé)都很單一, 數(shù)據(jù)輸入組件,經(jīng)過內(nèi)部處理,然后將處理過的數(shù)據(jù)輸出。所以這些組件也稱為過濾器,連接器按照業(yè)務(wù)需求將組件連接起來,其形狀就像‘管道’一樣,這種架構(gòu)風(fēng)格由此得名。
這里面最經(jīng)典的案例是*unix Shell命令,Unix的哲學(xué)就是“只做一件事,把它做好”,所以我們常用的Unix命令功能都非常單一,但是Unix Shell還有一件法寶就是管道,通過管道我們可以將命令通過標準輸入輸出串聯(lián)起來實現(xiàn)復(fù)雜的功能:
# 獲取網(wǎng)頁,并進行拼寫檢查。代碼來源于wikicurl "http://en.wikipedia.org/wiki/Pipeline_(Unix)" | sed 's/[^a-zA-Z ]/ /g' | r 'A-Z ' 'a-z' | grep '[a-z]' | sort -u | comm -23 - /usr/share/dict/words | less另一個和Unix管道相似的例子是ReactiveX, 例如RxJS. 很多教程將Rx比喻成河流,這個河流的開頭就是一個事件源,這個事件源按照一定的頻率發(fā)布事件。Rx真正強大的其實是它的操作符,有了這些操作符,你可以對這條河流做一切可以做的事情,例如分流、節(jié)流、建大壩、轉(zhuǎn)換、統(tǒng)計、合并、產(chǎn)生河流的河流......
這些操作符和Unix的命令一樣,職責(zé)都很單一,只干好一件事情。但我們管道將它們組合起來的時候,就迸發(fā)了無限的能力.
import { fromEvent } from 'rxjs';import { throttleTime, map, scan } from 'rxjs/operators';fromEvent(document, 'click') .pipe( throttleTime(1000), map(event => event.clientX), scan((count, clientX) => count + clientX, 0) ) .subscribe(count => console.log(count));除了上述的RxJS,管道模式在前端領(lǐng)域也有很多應(yīng)用,主要集中在前端工程化領(lǐng)域。例如'老牌'的項目構(gòu)建工具Gulp, Gulp使用管道化模式來處理各種文件類型,管道中的每一個步驟稱為Transpiler(轉(zhuǎn)譯器), 它們以 NodeJS 的Stream 作為輸入輸出。整個過程高效而簡單。
不確定是否受到Gulp的影響,現(xiàn)代的Webpack打包工具,也使用同樣的模式來實現(xiàn)對文件的處理, 即Loader, Loader 用于對模塊的源代碼進行轉(zhuǎn)換, 通過Loader的組合,可以實現(xiàn)復(fù)雜的文件轉(zhuǎn)譯需求.
// webpack.config.jsmodule.exports = { ... module: { rules: [{ test: /.scss$/, use: [{ loader: "style-loader" // 將 JS 字符串生成為 style 節(jié)點 }, { loader: "css-loader" // 將 CSS 轉(zhuǎn)化成 CommonJS 模塊 }, { loader: "sass-loader" // 將 Sass 編譯成 CSS }] }] }};中間件(Middleware)
如果開發(fā)過Express、Koa或者Redux, 你可能會發(fā)現(xiàn)中間件模式和上述的管道模式有一定的相似性,如上圖。相比管道,中間件模式可以使用一個洋蔥剖面來形容。但和管道相比,一般的中間件實現(xiàn)有以下特點:
- 中間件沒有顯式的輸入輸出。這些中間件之間通常通過集中式的上下文對象來共享狀態(tài)
- 有一個循環(huán)的過程。管道中,數(shù)據(jù)處理完畢后交給下游了,后面就不管了。而中間件還有一個回歸的過程,當(dāng)下游處理完畢后會進行回溯,所以有機會干預(yù)下游的處理結(jié)果。
我在谷歌上搜了老半天中間件,對于中間件都沒有得到一個令我滿意的定義. 暫且把它當(dāng)作一個特殊形式的管道模式吧。這種模式通常用于后端,它可以干凈地分離出請求的不同階段,也就是分離關(guān)注點。比如我們可以創(chuàng)建這些中間件:
- 日志:記錄開始時間 ? 計算響應(yīng)時間,輸出請求日志
- 認證:驗證用戶是否登錄
- 授權(quán):驗證用戶是否有執(zhí)行該操作的權(quán)限
- 緩存:是否有緩存結(jié)果,有的話就直接返回 ? 當(dāng)下游響應(yīng)完成后,再判斷一下響應(yīng)是否可以被緩存
- 執(zhí)行:執(zhí)行實際的請求處理 ? 響應(yīng)
有了中間件之后,我們不需要在每個響應(yīng)處理方法中都包含這些邏輯,關(guān)注好自己該做的事情。下面是Koa的示例代碼:
const Koa = require('koa');const app = new Koa();// loggerapp.use(async (ctx, next) => { await next(); const rt = ctx.response.get('X-Response-Time'); console.log(`${ctx.method} ${ctx.url} - ${rt}`);});// x-response-timeapp.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; ctx.set('X-Response-Time', `${ms}ms`);});// responseapp.use(async ctx => { ctx.body = 'Hello World';});app.listen(3000);事件驅(qū)動
事件驅(qū)動, 或者稱為發(fā)布-訂閱風(fēng)格, 對于前端開發(fā)來說是再熟悉不過的概念了. 它定義了一種一對多的依賴關(guān)系, 在事件驅(qū)動系統(tǒng)風(fēng)格中,組件不直接調(diào)用另一個組件,而是觸發(fā)或廣播一個或多個事件。系統(tǒng)中的其他組件在一個或多個事件中注冊。當(dāng)一個事件被觸發(fā),系統(tǒng)會自動通知在這個事件中注冊的所有組件.
這樣就分離了關(guān)注點,訂閱者依賴于事件而不是依賴于發(fā)布者,發(fā)布者也不需要關(guān)心訂閱者,兩者解除了耦合。
生活中也有很多發(fā)布-訂閱的例子,比如微信公眾號信息訂閱,當(dāng)新增一個訂閱者的時候,發(fā)布者并不需要作出任何調(diào)整,同樣發(fā)布者調(diào)整的時候也不會影響到訂閱者,只要協(xié)議沒有變化。我們可以發(fā)現(xiàn),發(fā)布者和訂閱者之間其實是一種弱化的動態(tài)的關(guān)聯(lián)關(guān)系。
解除耦合目的是一方面, 另一方面也可能由基因決定的,一些事情天然就不適合或不支持用同步的方式去調(diào)用,或者這些行為是異步觸發(fā)的。
JavaScript的基因決定事件驅(qū)動模式在前端領(lǐng)域的廣泛使用. 在瀏覽器和Node中的JavaScript是如何工作的? 可視化解釋 簡單介紹了Javascript的執(zhí)行原理,其中提到JavaScript是單線程的編程語言,為了應(yīng)對各種實際的應(yīng)用場景,一個線程以壓根忙不過來的,事件驅(qū)動的異步方式是JavaScript的救命稻草.
瀏覽器方面,瀏覽器就是一個GUI程序,GUI程序是一個循環(huán)(更專業(yè)的名字是事件循環(huán)),接收用戶輸入,程序處理然后反饋到頁面,再接收用戶輸入... 用戶的輸入是異步,將用戶輸入抽象為事件是最簡潔、自然、靈活的方式。
需要注意的是:事件驅(qū)動和異步是不能劃等號的。異步 !== 事件驅(qū)動,事件驅(qū)動 !== 異步
擴展:
- 響應(yīng)式編程: 響應(yīng)式編程本質(zhì)上也是事件驅(qū)動的,下面是前端領(lǐng)域比較流行的兩種響應(yīng)式模式:
- 函數(shù)響應(yīng)式(Functional Reactive Programming), 典型代表RxJS
- 透明的函數(shù)響應(yīng)式編程(Transparently applying Functional Reactive Programming - TFRP), 典型代表Vue、Mobx
- 消息總線:指接收、發(fā)送消息的軟件系統(tǒng)。消息基于一組已知的格式,以便系統(tǒng)無需知道實際接收者就能互相通信
MV*
MV*架構(gòu)風(fēng)格應(yīng)用也非常廣泛。我覺MV*本質(zhì)上也是一種分層架構(gòu),一樣強調(diào)職責(zé)分離。其中最為經(jīng)典的是MVC架構(gòu)風(fēng)格,除此之外還有各種衍生風(fēng)格,例如MVP、MVVM、MVI(Model View Intent). 還有有點關(guān)聯(lián)Flux或者Redux模式。
家喻戶曉的MVC
如其名,MVC將應(yīng)用分為三層,分別是:
- 視圖層(View) 呈現(xiàn)數(shù)據(jù)給用戶
- 控制器(Controller) 模型和視圖之間的紐帶,起到不同層的組織作用:
- 處理事件并作出響應(yīng)。一般事件有用戶的行為(比如用戶點擊、客戶端請求),模型層的變更
- 控制程序的流程。根據(jù)請求選擇適當(dāng)?shù)哪P瓦M行處理,然后選擇適當(dāng)?shù)囊晥D進行渲染,最后呈現(xiàn)給用戶
- 模型(Model) 封裝與應(yīng)用程序的業(yè)務(wù)邏輯相關(guān)的數(shù)據(jù)以及對數(shù)據(jù)的處理方法, 通常它需要和數(shù)據(jù)持久化層進行通信
目前前端應(yīng)用很少有純粹使用MVC的,要么視圖層混合了控制器層,要么就是模型和控制器混合,或者干脆就沒有所謂的控制器. 但一點可以確定的是,很多應(yīng)用都不約而同分離了'邏輯層'和'視圖層'。
下面是典型的AngularJS代碼, 視圖層:
Todo
{{todoList.remaining()}} of {{todoList.todos.length}} remaining [ archive ] {{todo.text}}邏輯層:
angular.module('todoApp', []) .controller('TodoListController', function() { var todoList = this; todoList.todos = [ {text:'learn AngularJS', done:true}, {text:'build an AngularJS app', done:false}]; todoList.addTodo = function() { todoList.todos.push({text:todoList.todoText, done:false}); todoList.todoText = ''; }; todoList.remaining = function() { var count = 0; angular.forEach(todoList.todos, function(todo) { count += todo.done ? 0 : 1; }); return count; }; todoList.archive = function() { var oldTodos = todoList.todos; todoList.todos = []; angular.forEach(oldTodos, function(todo) { if (!todo.done) todoList.todos.push(todo); }); }; });至于MVP、MVVM,這些MVC模式的延展或者升級,網(wǎng)上都大量的資源,這里就不予贅述。
Redux
Redux是Flux架構(gòu)的改進、融合了Elm語言中函數(shù)式的思想. 下面是Redux的架構(gòu)圖:
從上圖可以看出Redux架構(gòu)有以下要點:
- 單一的數(shù)據(jù)源.
- 單向的數(shù)據(jù)流.
單一數(shù)據(jù)源, 首先解決的是傳統(tǒng)MVC架構(gòu)多模型數(shù)據(jù)流混亂問題(如下圖)。單一的數(shù)據(jù)源可以讓應(yīng)用的狀態(tài)可預(yù)測和可被調(diào)試。另外單一數(shù)據(jù)源也方便做數(shù)據(jù)鏡像,實現(xiàn)撤銷/重做,數(shù)據(jù)持久化等等功能
單向數(shù)據(jù)流用于輔助單一數(shù)據(jù)源, 主要目的是阻止應(yīng)用代碼直接修改數(shù)據(jù)源,這樣一方面簡化數(shù)據(jù)流,同樣也讓應(yīng)用狀態(tài)變化變得可預(yù)測。
上面兩個特點是Redux架構(gòu)風(fēng)格的核心,至于Redux還強調(diào)不可變數(shù)據(jù)、利用中間件封裝副作用、范式化狀態(tài)樹,只是一種最佳實踐。還有許多類Redux的框架,例如Vuex、ngrx,在架構(gòu)思想層次是一致的:
復(fù)制風(fēng)格
基于復(fù)制(Replication)風(fēng)格的系統(tǒng),會利用多個實例提供相同的服務(wù),來改善服務(wù)的可訪問性和可伸縮性,以及性能。這種架構(gòu)風(fēng)格可以改善用戶可察覺的性能,簡單服務(wù)響應(yīng)的延遲。
這種風(fēng)格在后端用得比較多,舉前端比較熟悉的例子,NodeJS. NodeJS是單線程的,為了利用多核資源,NodeJS標準庫提供了一個cluster模塊,它可以根據(jù)CPU數(shù)創(chuàng)建多個Worker進程,這些Worker進程可以共享一個服務(wù)器端口,對外提供同質(zhì)的服務(wù), Master進程會根據(jù)一定的策略將資源分配給Worker:
const cluster = require('cluster');const http = require('http');const numCPUs = require('os').cpus().length;if (cluster.isMaster) { console.log(`Master ${process.pid} is running`); // Fork workers. for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code, signal) => { console.log(`worker ${worker.process.pid} died`); });} else { // Workers可以共享任意的TCP連接 // 比如共享HTTP服務(wù)器 http.createServer((req, res) => { res.writeHead(200); res.end('hello world'); }).listen(8000); console.log(`Worker ${process.pid} started`);}利用多核能力可以提升應(yīng)用的性能和可靠性。我們也可以利用PM2這樣的進程管理工具,來簡化Node集群的管理,它支持很多有用的特性,例如集群節(jié)點重啟、日志歸集、性能監(jiān)視等。
復(fù)制風(fēng)格常用于網(wǎng)絡(luò)服務(wù)器。瀏覽器和Node都有Worker的概念,但是一般都只推薦在CPU密集型的場景使用它們,因為瀏覽器或者NodeJS內(nèi)置的異步操作已經(jīng)非常高效。實際上前端應(yīng)用CPU密集型場景并不多,或者目前階段不是特別實用。除此之外你還要權(quán)衡進程間通信的效率、Worker管理復(fù)雜度、異常處理等事情。
有一個典型的CPU密集型的場景,即源文件轉(zhuǎn)譯. 典型的例子是CodeSandbox, 它就是利用瀏覽器的Worker機制來提高源文件的轉(zhuǎn)譯性能的:
除了處理CPU密集型任務(wù),對于瀏覽器來說,Worker也是一個重要的安全機制,用于隔離不安全代碼的執(zhí)行,或者限制訪問瀏覽器DOM相關(guān)的東西。小程序抽離邏輯進程的原因之一就是安全性
其他示例:
- ServerLess
微內(nèi)核架構(gòu)
微內(nèi)核架構(gòu)(MicroKernel)又稱為"插件架構(gòu)
總結(jié)
以上是生活随笔為你收集整理的软件架构设计案例_透过现象看本质:常见的前端架构风格和案例的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 十一、加权线性回归案例:预测鲍鱼的年龄
- 下一篇: python 函数式编程包_python