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