深度解讀當代前端架構演進與趨勢(下)

軟件架構的核心思想,就是推斷軟件系統各個組件之間數據流動的方式。軟件架構的質量取決於你設法推斷這些數據流的難易程度!本文要講的內容,就是在今天的 Web 應用程序背後探索這些數據流和最終的體系結構。Web 應用已從簡單的靜態網站(雙層結構)發展爲複雜的多層次、SPA 和 SSR 驅動的 API 優先系統。CMS 系統已發展成無頭(Headless)和內容優先的系統。

當代前端架構

現代應用程序在各個層面都非常接近桌面應用程序。當今環境中的一些顯著變化包括:

  1. Web不再侷限於臺式機平臺。我們的設備類型多種多樣,如平板電腦、手機、智能手錶和智能眼鏡等等。事實上,智能手機驅動的Web流量超過了臺式機平臺。
  2. 作爲Web的支柱,JavaScript已經發展成爲一種成熟的語言,並且還在不斷髮展,不斷引入衆多新功能。
  3. 文件系統、相機、PWA和硬件傳感器等新型API都能用在Web應用程序中。
  4. 用戶體驗是用戶在互相競爭的服務之間做出選擇的決定性因素。
  5. 網絡基礎設施有所改善,但與此同時又有數以十億計的收入底層用戶正在加入Web世界。流媒體和視頻點播都變成了日常事務。
  6. 人們基於同一套JS技術棧來使用各種工具和交叉編譯器構建原生移動應用程序。
  7. 許多新的框架和工具使用JavaScript作爲目標語言,而非將其用作源語言。一些不錯的例子有Elm、PureScript和ReasonML等等。

當代前端架構是這些不斷變化的需求的反映。顧名思義,它們建立在整個前端社區從過去學到的經驗之上。

早期的軟件架構模式建立在有限的硬件功能上並尊重這一事實。今天的情況已經變了。計算能力在不斷提升,而且軟件架構反映了這種觀點。

以上假設可以與今天的前端架構相關聯。這些架構融合了三大核心原則。

  1. 數據流佔據舞臺中心

考慮到前端代碼需要運行的領域衆多、環境多樣,業務邏輯佔據了中心位置。作爲工程師,我們花費更多時間來閱讀和調試,而非編寫代碼;我們必須直觀地跟蹤代碼。每當修復錯誤或向現有代碼庫添加新功能時,瞭解其可能導致的影響和迴歸是至關重要的。

在大型代碼庫中做到這一點的唯一方法是確保我們瞭解數據在應用程序中的流動方式。這是軟件架構最重要的目標。

今天,任何框架都是圍繞這條關鍵原則構建的。明確區分狀態視圖是非常重要的——鑑於簡單的基於事件的容器在向更復雜的狀態容器(如ReduxVuexNgrx等)轉變,這也是顯而易見的。由於我們嚴重依賴事件發佈/訂閱系統,數據(或控制流)將流經應用程序的每一寸角落。對數據流的重視不再侷限在局部環境。相反,作爲一種核心思想,現在我們要在整個應用程序中考慮數據流。

Angular 1已經證明雙向數據流(即時只侷限在視圖或組件上)可以帶來紛繁複雜的控制流。此外,React已經證明單向數據流很容易推斷,因此包括Angular 2在內的現代框架也遵循了這一理念。

  1. 基於組件的架構

轉向基於組件的用戶界面是數據流第一原則的必然結果。在談論組件時,有三個方面需要考慮。

首先是數據流的重點:傳統架構側重於水平分工。但是基於數據流的思維要求垂直分工。在敏捷世界中,這就是我們設想用戶故事的方式。基於MVC的架構不容易做到這一點。沒有簡單的方法可以通過一項功能共享或複用UI代碼(這裏的問題是——當功能在橫向組織推動下在整個代碼庫中傳播時,我們怎樣才能真正隔離功能呢!)。但是,封裝入一個完整功能的組件可以輕鬆配置爲可共享和可打包的實體。

其次是不斷髮展的視圖狀態:過去,視圖狀態業務狀態相比佔比較少。但隨着應用程序變得更具交互性,其佔比也得到了爆發式增長。視圖狀態需要接近實際視圖。視圖狀態很複雜,通常表示必需的時間關聯數據,如動畫和轉換等。類似MVC的架構沒有什麼很好的方法來封裝這種狀態。在這裏,作爲封裝核心單元的組件非常有用。

第三個方面與UI開發的原子單元有關。首先我們提出一個問題:

共享UI功能的最佳方式是什麼?共享UI功能意味着它可以包含以下四個部分:

結構(HTML——視圖)、樣式(CSS——視圖)、行爲(JavaScript——視圖模型)和業務邏輯(模型)。

於是組件的概念應運而生。我們意識到組件只是MVVM模式MV*模式的一個很好的實現……

但具體而言,如何表示UI組件?我能找到的最接近的概述是Deric Baily的博客文章。在語言級別,通常使用模塊或包來表示可複用功能的構建塊。JavaScript也有模塊。但這還不夠。

組件的這種抽象概念允許框架作者根據他們的需要定義具體的實現。

此外,良好的組件是高度可組合的,可實現分形架構。例如,登錄表單組件可以是標準登錄頁面的一部分,或者在會話超時時顯示爲對話框。一個組件只要定義好接收的屬性和發送的事件,就可以在滿足基礎框架要求的前提下複用在任何地方。

組件可以是由Webpack等打包器生成的函數、類、模塊或打包代碼。

如前所述,組件只是一個MVVM模式的良好實現。並且由於組件的可組合性,我們將MVVM模式實現爲分形(分形是一種自我重複的無止境模式)。這意味着我們在多個抽象級別處理多個獨立的的MVVM控制流。


實現MVVM的無限組件分型——循環套着循環!是不是很像《盜夢空間》?

而且,這正是我們對架構所期望的。

一個好的架構可以實現多級抽象。它允許我們一次查看一個抽象(細節級別)而不必擔心其他級別。這是製作可測試和可讀解決方案的關鍵所在。

  1. 讓框架處理DOM

DOM用起來既昂貴又繁瑣。當狀態本地全局)發生變化時,DOM能以某種方式自動更新就最好了。此外,它應儘可能高效同時不干擾其他元素。在將DOM與狀態同步時有些事情需要注意。

  1. 將DOM表示爲模型的函數:與組件相結合,聲明式數據綁定是用模型表示視圖的一個很好的解決方案。Angular有模板,React使用JSX,Vue支持JSX和模板。結合數據綁定和組件,我們得到了一個完美的MVVM。
  2. 更改檢測:框架需要一種機制來識別狀態中所有數據的變化。Angular 1使用的是昂貴的消化週期,React則支持不可變數據結構。使用不可變數據檢測狀態更改只是一個等式檢查而已。不需要髒檢查了!Vue.js依賴建立在getter/setter之上的反應系統;Angular使用區域檢測更改;Vue 3將使用ES2015代理處理反應性。
  3. 更新DOM:在檢測到實際更改後,框架需要更新DOM。許多框架(如React、Vue和Preact等)使用虛擬DOM(對時間優化),而Angular使用增量DOM(對內存優化)

定義當代前端架構

早期GUI架構的範圍主要集中在代碼結構和組織上。它主要關注模型與其表示之間的關係,也就是視圖。那是當時的需求。

今天情況已經發生了變化。現代前端架構的需求已經遠不止簡單的代碼組織。在社區中,我們已經把這些知識傳承了下來。

大多數現代框架已經在組件的幫助下標準化了代碼結構和組織概念。它們是當今架構的基本單元。

還應注意,架構與其實現框架密切相關。這可以追溯到這樣一個事實,也就是GUI模式很複雜,並且由於它直接對接用戶而無法獨立於其實現來討論。我們可以繼續說一個框架就是它的架構。

設計良好的組件系統是任何前端架構或框架的最基本需求。除此之外,它必須解決前面討論的許多其他問題,如聲明式DOM抽象、顯式數據流、更改檢測等。大多數元素都在下圖中高亮顯示:

現代前端架構元素

考慮到所有這些因素,我們可能會想要創建一個參考架構,但這根本不可能。每個框架或庫在實現時都有自己的庫特性。例如,React和Vue都支持單向數據流,但React更喜歡不可變數據,而Vue則支持可變性。因此,最好針對單個框架來全面瞭解其架構。

話雖如此,我們仍然可以嘗試創建近似的參考架構,如下所示:

現代參考架構

到目前爲止我們描述的所有特徵或元素都包含在這個架構中。它乾淨地映射到三層架構,但第二層的中間件是可選的。第二層表示UI服務器,理解的人不多。UI服務器只是一個服務器,旨在爲現代重客戶端的UI應用程序提供服務。它負責正交領域,如SSO集成、身份驗證和授權,服務端渲染、會話管理、服務UI資產和緩存等。此外,當大規模應用程序某處部署API服務器時,它充當反向代理以調用API調用避免CORS問題。這裏事實上的標準選擇是Node.js,因爲許多框架都在Node中編寫了SSR渲染器,而Node由於其異步特性而擅長處理大I/O請求。我們將在另一篇關於Web應用程序拓撲的文章中進一步討論UI服務器。

當代架構最重要的變化是模型的概念。

模型不再被視爲一個黑盒子。它被分隔成應用程序範圍的全局狀態和組件範圍的本地狀態。全局狀態通常使用複雜的狀態容器(如Redux、Mobx和Vuex等)管理。每個組件的本地狀態是三個事物的聯合——全局狀態切片、組件的私有本地狀態(異步數據、動畫數據和UI狀態等)和由父組件作爲props傳遞的最終狀態。我們可以將本地狀態視爲模型視圖模型的更好抽象。當我們將GraphQL添加到這個等式中時,狀態管理會發生變化。(在後面的GraphQL部分中具體解釋)

數據從上到下,從父組件流向子組件時是單向的。雖然框架允許數據直接反方向流動,但不鼓勵這樣做。相反,事件是從子組件中觸發的。父組件可以監聽或忽略它們。

不完整的藍圖

這個參考架構並沒有真正捕捉到當代架構的全部本質。大多數Web流量由靜態網站和CMS(內容管理系統)驅動。現代工具鏈已經大大改變了我們開發和部署這些應用程序的方式。在CMS的情況下,他們通過解耦前端與後端而變得無頭(Headless)StrapiContentful等的崛起就是明證。與此同時,我們不再使用純HTML和CSS構建靜態網站。靜態站點構建器和生成器也變得非常流行。我們現在可以使用相同的前端框架來構建由複雜的構建工具輔助的靜態網站。使用React.js時,我們可以使用Gatsby.jsVue.js,我們有Nuxt.js。當我們編譯代碼時,它會生成一個靜態網站,可以完整部署到任何靜態Web服務器。這種技術稱爲預渲染,與服務端渲染對應。

這裏我們有了另一個用於構建靜態網站的當代架構。這裏的思想是使用像Strapi一樣的無頭CMS,並使用像Gatsby這樣的靜態網站構建器來構建前端。在構建中,當我們生成靜態網站時,我們從CMS中提取所有數據並生成頁面。

當作者更改無頭CMS中的內容時,我們重新觸發我們的靜態網站的構建,使用新內容構建網站並立即部署到服務器或CDN。

構建靜態網站——現代方式

這個新的工作流程就像運行一個成熟的動態網站一樣好用,也沒有CMS的缺點,如複雜的部署和緩慢的加載時間等等…由於靜態網站可以直接部署到CDN。我們得以快速加載並改進緩存。我們還擺脫了靜態網站的所有問題,如更新週期緩慢和缺乏可複用性等。這裏引述Nuxt.js網站的願景——

我們可以進一步考慮使用nuxt generate並託管在CDN上的電子商務Web應用程序。每當產品缺貨或補充庫存時,我們都會重新生成Web應用程序。但如果用戶在此期間瀏覽這個Web應用程序,因爲有了對電子商務API的API調用,應用將保持最新狀態。無需再用服務器+緩存的多組實例!

總而言之,現代前端解決方案構建在基於組件的單向架構之上。

進一步考慮單向架構的話,可以通過多種方式實現它們。框架有自己的做事方式。一些不錯的例子包括:

  1. Flux(爲React設計)
  2. Redux(主要與React共用,但視圖不可知)
  3. MVU——模型視圖更新(用於Elm)
  4. MVI——模型視圖意圖(用於Cycle.js)
  5. BEST、Vuex和Ngrx等

Andre Staltz在他的博客文章中很好地描述了這些模式:
AndréStaltz——單向用戶界面架構

當代還是現代???

到目前爲止,我們有意不用“現代”這個詞,而一直在說的是“當代”。今天的架構實踐僅僅是我們舊有理念的進化。我們社區試圖將新事物融入現有的生態系統,總是留在邊界內,很少打破常規。因此,“當代”這個詞更能準確地描述這種理念。

在定義“當代”時,我們必須將所有鬆散的目標聯繫起來,必須將過去與現在和未來聯繫起來。我可以想到三種可能的鏈接——

  1. 過去——將今天的組件與歷史上的MV*相關聯
  2. 現在——帶有Web組件的場景
  3. 未來——函數組件

將今天的組件與歷史上的MV*聯繫起來?

到這裏事情應該都很清楚,但可能會出現一個問題,那就是這些模式如何與之前的模式聯繫起來。組件不是MVVMMV*的更好實現嗎?

如前所述,對於當代架構而言這只是一個底層的問題。然而,當代模式是關於整個應用的推斷。它們處理的是更高級別的抽象。UI組件是一個原子單元,它從父級接收全局狀態切片,將其與自己的本地狀態組合並將輸出顯示給用戶。

單向模式可以解決更大的難題。它說的是在兄弟組件之間通信並維護應用程序範圍的狀態。如果組件允許垂直分工,這些模式會爲整個應用程序帶回水平分工。

如果還是有些糊塗,請考慮Vue.js這個例子。Vue組件是MVVM的完美實現,同時我們可以使用Vuex(Vue的單向狀態容器)來管理應用程序範圍的狀態。架構存在於多個抽象層次。

Web組件的場景

組件架構幾乎是所有框架的基礎,人們正在嘗試將組件的概念標準化爲官方Web標準,儘管它們背後的推斷方式完全不同。此外我將它們稱爲嘗試,因爲即使成爲了標準,許多框架作者也擔憂其可行性。

在本文中,最重要的關注點是數據流。Tom Dale很好地總結了這個問題:

根據我的經驗,與框架無關的組件還有很長的路要走。

關於其他問題需要看Rich Harris的博文:爲什麼我不用Web組件。

這並不是說當我們定義自己的技術棧時應該完全避免它們。一般的建議是從按鈕、複選框和收音機等枝葉組件開始一步步慢慢來。總是要謹慎行事。

函數組件和Hooks——這是啥?

當我們將組件當作MVVM的實現時,我們通常期望一個視圖模型對象,它的props和方法由視圖通過綁定使用。在React、Vue和Angular等情況下,它通常是類實例。但是這些框架還有函數組件(沒有任何本地狀態的組件)的概念,其中實例根本不存在。此外,React最近引入了一種使用Hooks編寫組件的新方法,允許我們在沒有類語法的情況下編寫有狀態組件。

React Hooks——你注意到這裏缺少“this”指針了嗎?

這裏的問題是——視圖模型對象在哪裏?Hooks的創意很簡單,但在跨調用維護本地狀態的理念上完全不一樣。但從架構的角度來看它仍然是之前的理念。我們可以將其視爲簡單的語法級別更改。我們都知道JavaScript語言中的類很糟糕,經常令人困惑,讓開發人員很難編寫乾淨的代碼。Hooks擺脫了類,從而解決了這個問題。

唯一改變的是視圖模型的概念。無論是帶有Hooks還是函數組件有狀態組件,我們都可以假設組件的視圖模型對象是它的詞法語境(Lexical Context)閉包。該組件接收的所有變量、Hooks值或props共同形成其視圖模型。其他框架也採用了這一理念。

看起來函數組件就是未來趨勢。不過我不會說Hooks是一個功能齊全的長期解決方案(聽起來好奇怪),但在語法層面上它很優雅,並且可以緩解古老的類問題。如果你不認爲語法很重要,請看看Svelte

當代架構的下一階段

與Web應用程序相關的每項新技術都會在某種程度上影響前端應用程序。目前有三種趨勢——GraphQL、SSR和編譯器,這裏必須具體介紹一下才算完整。

GraphQL

GraphQL是一種服務端查詢語言。你可能已經看過有人說它取代了REST,但事實並非如此。當我們談論REST時,它是一種元模式。在概念層面,它採用面向資源的架構來定義應用程序域模型;並且在實現層面,它使用HTTP協議的語義來交換這些資源,以賦予Web共享信息的方式。

現代業務需求很複雜,許多工作流程不能簡單地作爲HTTP CRUD類比的資源公開。這就是REST比較尷尬的地方。GraphQL旨在消息傳遞級別替換REST的純HTTP協議。GraphQL提供了自己的消息傳遞封裝,可以被GraphQL服務器理解,並且還支持查詢服務端資源(域模型)。

但是GraphQL客戶端實現的副作用是,GraphQL已經開始侵佔狀態容器的職責。我們來看基本的事實,那就是客戶端的模型只是服務端模型的一個子集,前者專門針對UI操作標準化,那麼像Redux/Flux這樣的狀態容器只是在客戶端緩存數據而已。

GraphQL客戶端內置了緩存支持,可跨多個請求來緩存。

這個簡單的事實讓開發人員可以省掉許多與狀態管理相關的樣板代碼。在宏觀層面,它的形態仍然有待觀察。以下帖子詳細描述了具體的機制:

GraphQL是怎樣取代Redux的。
用React Apollo瘦身我們的Redux代碼。

一定要探索GraphQL,因爲它是未來趨勢。

SSR——服務端渲染

過去服務端MVC是非常重要的,服務器會生成靜態或動態HTML頁面,這些頁面很容易被搜索引擎抓取。由於客戶端框架可以提供出色的用戶體驗,我們正逐漸在瀏覽器上渲染所有內容。完全客戶端渲染的應用程序在請求服務器時返回的典型HTML頁面幾乎是一個空頁面:

SPA應用程序的初始HTML文件——幾乎是空的!

一般來說這沒什麼問題,但在構建電子商務網站時遇到了麻煩,因爲這類網站需要較快的加載速度和對SEO友好的可抓取內容。麻煩還不止於此,移動電話的互聯網連接速度也很慢,而一些入門級設備的硬件也很差。搜索引擎對完全客戶端渲染的應用程序抓取的能力有限。

爲了緩解這個問題,老式的SSR——服務端渲染又回來了。有了SSR,我們可以在服務器上渲染客戶端SPA,合併狀態,然後將完整渲染的頁面發送到客戶端。它減少了應用程序頁面的初始加載時間,從而提高了網站的響應速度。

SSR是下一步進化,它填補了客戶端和服務端之間的鴻溝。

由於客戶端代碼是JavaScript,我們需要服務端的等效引擎來執行JS代碼。Node.js作爲JavaScript引擎是執行SSR的服務端技術棧的事實標準。雖然SSR設置可能變得非常醜陋和複雜,但許多流行的框架已經提供了一流的工具和更高級別的框架,爲SSR提供了非常流暢的開發人員體驗。

SSR徹底改變了我們的日常開發工作流程。我們比客戶端——服務端抽象更進一步。它引入了強制的三層架構,其中基於Node.js的服務器是關鍵的中間件。但從架構的角度來看——在數據流和分工層面所有這些都是相同的。SSR既不引入新的數據流,也沒有改變已有的存在。

編譯器時代:Svelte——編譯器還是縮小的框架?

我不知道該如何描述Svelte纔好。我能說的是——當用戶啓動Web應用程序時,框架就在瀏覽器中運行。框架提供的轉換抽象有其運行時成本。Svelte是不一樣的。

與靜態站點構建器一樣,Svelte在構建時運行,將組件轉換爲高效的命令式代碼,從外部更新DOM。

所以Svelte是一個基於組件的框架,它展示了當代前端框架的所有特性,但同時它也是一個編譯器。編譯器將源代碼編譯爲高性能的命令式JavaScript代碼。作爲一個編譯器,它可以做許多其他框架不能做的事情:

  1. 在構建時提供可訪問性警告
  2. 實現代碼級優化
  3. 生成較小的包
  4. 在不破壞語法的前提下將DSL整合到JavaScript中

其宗旨是編寫更少的代碼。Svelte證明編譯器可以實現許多以前用純JavaScript無法實現的功能。如果Svelte還不夠,那麼我們可以用Stencil.js,這是一個用於編寫Web組件TypeScript+JSX編譯器。

其中一些想法已經成爲某種形式的主流思想——Angular AOT編譯器和Vue單文件組件等等。然後還有其他人將這種思想推向極致

Rich Harris的這篇演講很好地展示了Svelte的底層哲學,並與React做了主觀對比:

同樣,編譯器的前端開發前景也很光明。

還有其他方法!偉大的單體!

雖然完整的客戶端框架現在風靡一時,但它並不是唯一的行事方式。Web依舊是多樣化的。仍然有許多應用程序是服務端驅動的,而且將繼續這樣做。

但這是否意味着它們的體驗會很差?當然不是!架構的設計目標是支持產品,Basecamp團隊開發的框架Stimulus就做得很好。要了解他們的理念可以在這裏查閱。

它是一個適度的框架,通過輕量級JavaScript提升後端渲染頁面的交互性,同時採用最新實踐和最新標準。Stimulus通常與Turbolinks並用,以創建一流的SPA用戶體驗。(我是Basecamp的老用戶了,發現它比其他許多SPA應用程序都更精緻。)

Stimulus在某種意義上是不一樣的,因爲它是通過HTML而非JavaScript驅動。狀態在HTML而非JavaScript對象中維護。數據流非常簡單:控制器附加到DOM,它公開了可以附加到操作上的方法,從而執行進一步的操作。

你可能會想到,它很像Backbone和Knockout的時代——確實如此。目標很簡單——爲後端驅動Web提供的交互式前端框架。唯一的不同是Stimulus採用了現代社區標準和實踐。

Strudel.js是另一個類似理念的適度框架。在2019年,我們可以使用像RE:DOM這樣的當代DOM庫。

雖然它們可能無法解決當代前端框架面臨的所有問題,但它們給JavaScript審美疲勞的世界帶來了一絲喘息之機。

總結

只有一個詞能用來描述GUI架構——華麗。雖然對於前端軟件開發來說,MVC作爲一種模式已經逝去了,但原則是永恆不變的。

我們從原始的MVC開始探索了著名的桌面模式。然後我們轉到Web應用程序並使用相同的原則來得到了流行的模式。之後我們轉向早期的獨立客戶端模式,最後全面討論了SPA框架。

重要的一點是,今天的前端框架是面向組件的,它們將MVC/MVVM關注點作爲一個層面,同時要處理新的環境和挑戰。

最後,我們瀏覽了一遍前端架構的新面孔,包括JavaScript驅動的SSR和GraphQL的崛起。同時我們跳過了許多令人興奮的新技術,如HTTP2和WebAssembly等,它們可以改變前端架構的未來,改變我們對應用程序的看法。

由於所有這些模式涉及的術語都有所重疊,並且通常是由社區各自獨立開發的,因此很難根據進化時間來定義明確的線性時間軸。有時將不同的模式聯繫在一起是有問題的。此外爲了簡單起見,我們可以自由地描述某些概念,不用特別研究它們的細節。沒有現成的命名法則可用,所以有些理念我用了自己發明的術語。

Web應用程序拓撲是Web應用程序架構的另一個關係密切的領域。拓撲通常在技術棧選擇、安全約束和性能等方面對前端開發產生深遠影響。因此這將是下一篇文章的主題。

原文鏈接
https://blog.webf.zone/contemporary-front-end-architectures-fb5b500b0231

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章