從 SPA 到 PWA:Web App的下一站在哪?

weixin_33766168發表於2019-03-27

從AJAX(Asynchronous JavaScript + XML,非同步JavaScript和XML)開始, 尤其是 AngularJS 推出之後,SPA(Single Page App,單頁應用)已經成為前端 App 的必選方案。

SPA 可以在客戶端提供完整的路由、頁面渲染、甚至一部分資料處理; 這往往需要一個比 jQuery 時代更重的 JavaScript 框架,來實現這些原本發生在後端的邏輯。

多數框架如 React、Vue 還會內建元件化機制來幫助開發者組織程式碼, 它們甚至進化到專門負責檢視元件的程度,路由和資料交由各種外掛來處理, 比如 vuex、Redux、Vue Router 等等。這些工具已經相當先進和完整,提供了路由方案、伺服器端渲染方案、前端狀態管理方案。

但 SPA 的本質還是瀏覽器端 App,底層技術仍然依賴 history API、defineProperty、AJAX。 這些 API 的能力和完備性,決定了 SPA 能達到的使用者體驗和上層架構設計。

也正是這些底層 API 的不足和缺陷使得 SPA 很難企及原始 Web 的架構優勢。 比如在內容可訪問性(Accessibility)、服務的獨立部署和演化(Independent Deployment) 等方面遠不及十年前搭建的同類站點。

同時還在不同程度上破壞了 HTTP、URL、HTML 的語義, 這些缺陷使我們需要花費大量精力去修復日誌統計、效能優化、首屏渲染、靜態分析和測試等環節。而陷阱在於決策使用 SPA 方案時不一定能有足夠的遠見看到這些問題對架構帶來的深遠影響。

與此同時 Web 標準也在持續迭代,諸如 Web Bluetooth、Push API、Web of Things、Service Worker 的標準已經在主流瀏覽器(尤其考慮國內 webkit 核心的普及程度)有不同程度的支援。 尤其是PWA概念的提出,給出了一種在不破壞 Web 架構的前提下實現流暢使用者體驗的方式。

本文就 SPA 架構的一些不足展開討論,並探討 PWA 方案(這裡說是方案,其實更是一種技術方向的選擇)的價值和私有平臺的最佳演化方式。

我們想要怎樣的 Web App?

Web 頁面尤其是動態 Web 頁面和 Web App 的區別非常模糊, 但為了更清晰地討論 SPA、PWA 這些技術方案,還是先來定性地分析一下 Web App 背後的產品需求:

1.平滑的、不被打斷的互動體驗。如果互動過程中,頁面重新載入而丟失狀態、網路原因使得頁面無法顯示,這樣使用者體驗就會被打斷,就不夠App。

2.與裝置相適應的佈局。例如在移動瀏覽器中展示 PC 頁面的完整佈局,就會使使用者需要縮放和拖動才能檢視資訊,就不是 App 的體驗。

3.快速的呈現和響應。進入每一頁都需要漫長的等待,或者使用者操作後得不到立即反饋,可能是 Web 頁面常見的問題。

4.符合移動端的互動習慣。移動端特有的硬體使其 Native API 更加豐富,例如藍芽、二維碼、相機、支付、手勢滑動、手勢縮放、觸感反饋等。

以上是筆者對 Web App 需求的理解(歡迎留下評論),下文基於此展開討論。

Web 架構的優勢

值得思考的是,即使 Web 頁面與我們對移動 App 的需求相差甚遠, Web 技術仍然是當前移動 App 的架構中必備的組成部分。 我們依賴 Web 技術的地方正是 Web 架構的優勢:

1.可連結。Web 在技術分類上屬於分散式文件,這些文件通過 URL 相互連結。無論是單個網站內的不同頁面還是跨網站的頁面之間,都可以直接開啟而無需下載安裝。這裡要強調一個隱含的功能:Deep Linking,即從一個 App 跳轉進入另一個 App 內的指定頁面,甚至還可以定位到特定的瀏覽位置。

2.可訪問。HTML 是 Web 的基石之一,一方面提供了內容和樣式的分離,使得機器和人都可以閱讀也便於開發更復雜的樣式和互動;另一方面統一的標記語言有更好的可訪問性,這是其他平臺很難建設的,比如可以選擇和複製,盲人可以啟用螢幕閱讀器,甚至可以找命令列中檢視一個 Web 頁面。

3.零門檻。你不需要任何許可或付費就可以參與開發和提供 Web 服務。這意味著同時存在無數種方式來開發一個網站,在一定程度上促成了 Web 技術的繁榮。

4.獨立部署。不同的 Web 服務之間,甚至同一 Web 服務的各部分,都可以獨立地部署和演化。新舊網站可以同時執行在這一平臺上,這一點也是 HTML5 標準的迭代原則。

5.健壯性。Web頁面擁有分散式系統特有的健壯性。Web頁面和它所依賴的圖片、視訊、指令碼、樣式等資源沒有硬性依賴:一方面部分資源掛掉頁面的其他功能仍然可用;另一方面Web App可以一邊下載一邊執行,這是其他平臺很難具有的健壯性。

在 SPA 大行其道之後廣泛討論的相容性、響應式設計、可訪問性(或稱無障礙)、頁面效能等問題, 本來都是 Web 體系結構的優勢,這是一個略帶調侃的示例頁面:https://motherfuckingwebsite.com

這個只有 81 行的網頁,不僅傳遞了相當多的內容,而且它在相容性、響應式設計、可訪問性、頁面效能方面都表現優異。

重要的是這個頁面使用的技術都來自Web早期,換句話說這些非功能需求正是Web與生俱來的優勢。既然我們正在費力解決的這些問題不來自於 Web 本身,那麼這些問題到底來自哪裡? 是重 JavaScript 框架的問題,還是元件化方案的問題,還是掉進開發者體驗陷阱?

SPA 方案的困難

本文不去討論某個具體的 SPA 框架的成敗或優缺點,只討論採用 SPA 方案來實現我們想要的 Web App 存在哪些困難,以及 SPA 方案對既有 Web 頁面的影響。 下面列舉 SPA 方案對架構產生的一些比較重要的影響,從可連結性(URL)、可訪問性,服務的獨立性等方面具體分析。

SPA 是一組高度耦合的頁面(頁面耦合)

SPA 方案要求 App 內所有頁面位於同一服務例項上, 也就是說處理 SPA 頁面請求的每個例項都必須擁有 App 內所有頁面的資訊, 這一資訊通常是頁面元件的宣告。

這是因為 SPA 要求頁面切換不發生瀏覽器跳轉。設想操作流程『開啟頁面A -\u0026gt; pushState 到頁面 B -\u0026gt; 重新整理 -\u0026gt; 返回』,這時瀏覽器不會重新載入 A,而只是觸發 popstate 事件給 B。 因此對於任意頁面 A,點出到的任意頁面 B,B 頁面反過來都需要知道頁面 A 的資訊,當然頁面 A 也知道頁面 B 的資訊,因此任意兩個有跳轉關係的頁面,都需要相互瞭解對方的資訊,或引用對方元件。

這樣相互耦合的一組頁面,就構成了一個 SPA 方案的 Web App。 這樣的 App 內所有頁面都不再能夠『獨立部署』,因此也不能獨立迭代演化。

這往往意味著它們的開發除錯、前端編譯、部署過程都是耦合在一起的, 這些都是 SPA 方案帶來的成本:

  • 開發依賴:因為要能夠開啟一個頁面必須引用對應的元件,這些元件在開發和除錯階段一定需要綁在一起。如果兩個頁面涉及到業務會跨團隊,無疑會增加很多成本。

  • 編譯依賴:考慮使用 MD5 戳的編譯方法,相互引用的一組檔案必須一起編譯上線,這會降低協作效率因為它們本屬於不同的業務或團隊。當然也可以不使用 MD5 戳並分別上線,動態調整引用關係,這樣的問題在於無法平衡 HTTP 快取和快速生效的矛盾。

此外,由於瀏覽器的同源策略,一個 Web App 被限制共享一個域名。 否則在富互動的場景下跨域將會是一個非常複雜的問題, 當然如果你願意使用 JSONP 這麼不安全的介面另當別論。

強元件化容易陷入技術豎井(技術封閉)

SPA 方案伴隨著強元件化方案,容易陷入封閉的技術豎井。 換句話說就是容易一條路走到黑,失去 Web 應有的架構優勢。 這是因為非同步頁面擁有非同步的天性。 瀏覽器重新渲染一個頁面時, 全域性變數、定時器、事件監聽器都會初始化為全新的,這是『重新整理』的含義。 而非同步頁面卻不然:

  • 非同步頁面間,全域性變數、定時器是共享的,沒有託管很容易亂掉。

  • 非同步頁面的\u0026lt;script\u0026gt;之間,執行順序是不保證的,沒有託管極易出錯。

因此絕大多數 SPA 方案都不會讓你直接插入\u0026lt;script\u0026gt;來編寫業務程式碼, 與此相反,會提供類似模組、元件之類的概念來託管一切。 你可能需要儲存、需要網路、需要路由、需要通訊,你需要把所有 Web API 都封裝一遍。

這是各種 SPA 框架全家桶背後的邏輯。 最終業務的執行環境不再是瀏覽器,而是這套元件化方案。 而社群的元件化方案不會像 Web 標準一樣去迭代,也不一定向下相容,這在版本升級或框架遷移時會產生非常大的成本。

URL 不再能定位資源(URL 弱化,可訪問性差)

對於原始 Web 頁面,URL 不僅能定位資源的頁面,甚至還能定位到頁面種的具體瀏覽位置。 但是在 SPA 裡頁面由 SPA 框架渲染,經典的配置是對於所有 URL 都返回同一個資源, 瀏覽器端指令碼通過 location.href 渲染不同的頁面。所以這有啥問題?

1.首屏效能差。瀏覽器端渲染,在頁面下載過程中是白屏的;瀏覽器直接渲染頁面是流式的,下載多少渲染多少。

2.機器不可讀。搜尋引擎、CLI 使用者代理等不支援指令碼的使用者代理無法解析頁面,因為不同 URL 頁面內容是一樣的。

3.無法定位瀏覽位置。因為瀏覽器不再託管整頁渲染也無法記錄和恢復瀏覽位置。

可以看到不僅連結(URL)的概念被弱化,而且可訪問性天生就很差。 比較先進的 SPA 框架會提供伺服器端渲染(SSR)來補救,但對架構有額外的要求:前後端都可以進行頁面渲染,通常會要求前後端同構。

既然瀏覽器不再記錄瀏覽位置,就需要 SPA 框架來實現。 但由於 Web App 內可以區域性地渲染任何一塊內容,因此頁面的概念在 SPA 中就變得很模糊, 而樹狀 DOM 結構確實無法對映到線性的 URL 結構(除非你打算繼續破壞 REST 把資料塞到 URL 裡)。

因此即使花費大力氣去做,也無法實現完美的瀏覽位置記錄。

History API 不完備(體驗不穩定)

History API 是指瀏覽器提供的瀏覽歷史相關的 BOM API, 包括 pushState 方法,popstate 事件,history.state 屬性等。先不提在某些瀏覽器下 API 缺失的問題,在當前標準和主流瀏覽器如 Safari 和 Chrome 中的表現就有許多問題。

這些問題會導致非常不穩定的體驗,例如前進後退無效,URL 與頁面內容不對應、甚至出現互動沒有響應的情況。

總之對於一個追求極致體驗的 Web App 來講是無法接受的。下面羅列一些筆者遇到過的:

  • 同步渲染的頁面資源 載入會延遲 popstate 事件。這使得頁面未載入完時可以通過 pushState 點出但無法返回。

  • PopStateEvent.state 總是等於 history.state。因此當 popstate 事件發生時,誰都無法獲取被 pop 出的 state,這讓 state 幾乎不可用。

  • popstate 事件處理函式中無法區分是前進還是後退。考慮重新整理頁面的場景不能只儲存為變數,只能儲存在 sessionStorage 中,但這是同步呼叫會增加路由的延遲,而且需要維護配額不是一個簡單可靠的方案。

  • 有些高階瀏覽器(比如某些華為內建瀏覽器)不支援 history.state,但支援 pushState 和 popstate。

  • iOS 下所有瀏覽器中,設定 scrollRestoration 為 manual 會使得手勢返回時頁面卡 1s,這讓恢復瀏覽位置也不存在簡單可靠的方案。

  • 沒有 URL 變化事件。在 pushState/replaceState 時不會觸發 popstate 事件。因此沒有統一的 URL 變化事件,通常需要路由工具來封裝。

  • 手勢前進/返回的行為在標準中沒有定義。這意味著有些瀏覽器會做動畫,有些不會。因為這些動畫沒有定義任何 API 所以 SPA 框架接管頁面切換動畫無法保證一致的體驗。

Referer 的語義不再是來源(日誌錯誤)

在 Web 時代,Referer HTTP Header 用來標識一個請求的來源,主要用於日誌、統計和快取優化。 典型的 SPA 框架會破壞 Referer 的語義。
SPA 中頁面跳轉分兩種情況:一種是使用者與 DOM 互動由指令碼 pushState 來改變 URL; 另一種是使用者與瀏覽器互動比如前進後退按鈕或手勢,此時瀏覽器觸發 popstate 事件 來通知指令碼。

對於後一種情況,popstate 事件發生時頁面 URL 已經發生變化,此時才會通知到 SPA 框架載入下一頁內容。 因此這時發出的請求 Referer 頭的值一定 是當前頁的 URL 而不是來源頁的 URL。

PWA 帶來的機會

還不瞭解 PWA 的同學建議先去閱讀筆者在 2017 年給的調研:PWA 初探:基本特性與標準現狀, 除了目前 PWA 已經得到所有主流瀏覽器的支援外,其他內容仍然有效。

此外Harttle Land 也在年初支援了 PWA,你現在就可以把它新增到桌面,或新增到主屏, 也可以離線瀏覽(比如現在切斷網路,重新整理本頁面)。

PWA 一詞出自 Alex Russell 的 Progressive Web Apps: Escaping Tabs Without Losing Our Soul, 從這篇文章標題也可以看到 PWA 的精髓:在實現 App 體驗的同時不丟失 Web 架構的優勢。 因此可以規避上述 SPA 的問題,同時能夠充分發揮 Web 的優勢。

漸進式改善

Progressive 是指 PWA 的構建過程。構成 PWA 的標準都來自 Web 技術, 它們都是瀏覽器提供的、向下相容的、沒有額外執行時代價的技術。 因此可以把任何現有的框架開發的 Web 頁面改造成 PWA,而且與 SPA 方案不同, 沒有強元件化機制,因此不需要一把重構可以逐步地遷移和改善。

效能的提升

PWA 對效能的提升主要靠 Service Worker,它是在傳統的 Client 和 Server 之間新增的一層。效能提升程度取決於這一層的具體策略。例如:

1.如果使用快取優先策略。載入時間必然明顯更短。但使用者可能看到過時的內容。

2.如果使用網路優先策略。載入時間必然更長,因為增加了額外的快取查詢時間。

當然還可以應用 Race 策略,總之效能如何取決於我們怎樣控制。 PWA 使得我們有機會來定製這個策略,當然是值得探索的。

體驗的增強

PWA 方案更接近於 Web 的方式,它是 Web 的增強而不是替代。 因此 Web 應該有的互動體驗會得到保證,此外 PWA 還提供了一些 App 方面的增強。 具體地,相比於 SPA,PWA 可達到的體驗效果主要表現在:

  • 穩定的互動反饋。頁面切換直接由瀏覽器託管,這就可以避免使用 history API,尤其是前進後退等涉及瀏覽歷史棧的操作會更加穩定,互動反饋也更加可預期。

  • 離線可用。這或許是 PWA 最明顯的體驗優勢,可以明顯提升媒體時長和互動次數。

  • 裝置整合度更好。PWA 有一些新的瀏覽器能力,比如新增到桌面、推送通知等,是 SPA 所不具有的。

  • 頁面瀏覽位置。相比 SPA 省去了龐大的實現程式碼,但瀏覽位置保持卻更穩定、更健壯。

PWA 的不足之處在於無法託管頁面切換,這一互動必須由瀏覽器實現。 PWA 對速度的收益也需要額外說明:如果既有系統可能已經做過更激進的優化(例如此前已經做過資源打包或本地儲存)。

  • PWA 方案對載入時間可能並沒有提升,但對於TTI和真實使用者的感受應當有可感知的提升。因為 PWA 更接近瀏覽器容易理解的原始 Web 頁面,因此可以更好地利用瀏覽器優化,比如 HTTP 快取、檔案為單位的編譯快取等。

  • 另一方面 PWA 方案的架構更簡單和解耦。長期來看頁面傾向於比 SPA 體積更小,載入更快速。這方面建議多從架構的長期演化上考慮,見下一節的討論。

架構上的優勢

筆者更看好 PWA 是因為它在架構上的優勢,這對軟體的迭代效率和長期演化都有好處。 選擇好的架構可能沒有立竿見影的收益,但是卻會有利於軟體的演化和團隊的發展,反過來也能更好地支援業務需求。

1.獨立部署和演化

PWA 方案不要求頁面元件之間存在引用關係,甚至不要求頁面之間有相同的元件抽象。 這意味著頁面之間是解耦的。

因此服務/頁面仍然可以獨立部署和演化,不同的頁面仍然可以選擇適合自身業務的技術棧去開發。不僅可以減輕團隊管理的複雜度,也有利於各業務線的迭代效率。

2.業務開發更加輕量

為了應付日漸龐大的 Web 頁面,經過優化的 JavaScript 引擎已經可以和一些編譯語言的速度相提並論。但今天的 Web 頁面指令碼都大的離譜,龐大的指令碼不僅會影響載入速度,過度依賴指令碼還會讓頁面的可訪問性變得很差,互動也變得不可預期。

採用 PWA 方案有利於減小頁面體積,提升頁面的載入效能。 比如省去了龐大的 SPA 框架,更重要的是頁面的解耦讓頁面開發更加輕量。

3.更多可能性

Service Worker 使得除了客戶端、伺服器、中間代理之外,還可以存在一層定製的策略。 Service Worker 可以用於效能優化,甚至實現客戶端容災。 這是 Web 體系結構上新的架構元素,可能大有所為。

4.架構更加簡單

對架構而言,簡單性是穩定性的前提。充分利用瀏覽器和已有 Web 架構,能夠讓前端更加簡單。

不去託管資源載入,把它完全交給瀏覽器,請求 Referer 也就自然不會錯了。

不去操作瀏覽歷史,把頁面切換互動交給瀏覽器,不僅頁面間可以解耦,互動效果也更穩定。

獲取簡單性的關鍵在於不要和瀏覽器對著幹,而是著力於改進瀏覽器。 使用 Web 的方式解決問題,就仍然走在 Web 的道路上,就不會損失 Web 應有的體驗和架構優勢。

參與標準建設

對一個大型網站來講,無論是業界的 SPA 方案還是 PWA 系列技術,都會存在不足和缺陷。 重要的是這二者的改進方式完全不同:

  • 改進 SPA 方案往往意味著在 Web 前端(即瀏覽器端)建設更復雜的抽象和全站統一的元件化;

  • 改進 PWA 方案則意味著從瀏覽器端入手,通過與端的協作來解決問題,同時保持 Web 前端架構的簡單。

前端與瀏覽器端的協作在業界已經有很完善的實踐方式, 包括興趣收集、准入和評審等環節都有現成的方法。 Web 前端和私有平臺(自有端)的協作也應當採用這樣的方式。

不僅可以通過與標準化組織的協作來維持架構的先進性,也可以通過緊密的社群協作來確保技術的包容性, 這樣自有端才能有自己的技術生態,也更容易融入標準的迭代。這也是私有平臺技術影響力的一個來源。

結論和建議

我們想要的只是一個快速的、流暢的、功能豐富的 Web App。 SPA 方案和 PWA 方案的區別在於解決問題的方式。

  • SPA 的思路是封裝一切,讓開發者面向框架而非 Web 本身。架構足夠複雜以至於沒有明顯的問題。

  • PWA 是網頁的漸進增強,技術上是中立的讓開發者仍然面對 Web。架構足夠簡單以至於明顯沒有問題。

SPA 的複雜性在於業務之間因為框架技術(尤其是元件化)而產生耦合, 技術棧深而且封閉,重 JavaScript 的頁面可訪問性和穩定性也會變差, 而且 JavaScript 框架替代瀏覽器託管頁面載入這樣新的互動方式,也會在使用者互動、日誌統計等方面產生誤差和麻煩。

與此相反,PWA 概念涉及的技術是 Web 標準迭代的產物, 不強制任何元件模組框架,可以在任何已有 Web 頁面上漸進增強, 也允許不同的業務可以獨立迭代,因此更容易產出體積小的、載入速度快的頁面。 同時新的 Service Worker 技術也使 Web 架構有更多的技術可能。

因此對於大型 Web App 建議先上 PWA 方案。 因為 PWA 是 Web 標準的一部分,是 JavaScript 框架中立的, 不強制任何元件化方案也沒有引入額外的架構約束, 因此不會給後續架構迭代造成負擔。 如果擁有自有端,大可按照 Web 標準的方式去迭代。以 Web 方式提供的 API 也更加便於參與 W3C 標準,可以保持不落後於社群。

參考文獻

以下是本文參考和引用的資源,感謝 MDN、Infreqently Noted、W3C、Wikipedia、React、Vue、HarttleLand 等。

作者簡介

楊珺,百度前端技術部資深研發工程師,碩士畢業於北京大學計算機應用技術專業。曾負責百度搜尋 Web 極速瀏覽框架的研發工作,2019 年春晚百度紅包期間整體負責搜尋前端技術。HarttleLand 的站長,liquidjs 模板引擎的作者,HTML5 標準的貢獻者,HTML5 標準簡體中文翻譯專案的發起人。

更多內容,請關注前端之巔。

\"\"

相關文章