從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 等。
https://harttle.land/2017/01/28/pwa-explore.html
https://infrequently.org/2015/06/progressive-apps-escaping-tabs-without-losing-our-soul/
https://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/
https://www.w3.org/2019/Process-20190301/#Reports
https://harttle.land/2019/03/14/the-developer-experience-bait-and-switch-zh.html
作者簡介
楊珺,百度前端技術部資深研發工程師,碩士畢業于北京大學計算機應用技術專業。曾負責百度搜索 Web 極速瀏覽框架的研發工作,2019 年春晚百度紅包期間整體負責搜索前端技術。HarttleLand 的站長,liquidjs 模板引擎的作者,HTML5 標準的貢獻者,HTML5 標準簡體中文翻譯項目的發起人。
更多內容,請關注前端之巔。