React 同構應用 PWA 升級指南

發表於2018-05-25

前言

最近在給我的部落格網站 PWA 升級,順便就記錄下 React 同構應用在使用 PWA 時遇到的問題,這裡不會從頭開始介紹什麼是 PWA,如果你想學習 PWA 相關知識,可以看下下面我收藏的一些文章:

PWA 特性

PWA 不是單純的某項技術,而是一堆技術的集合,比如:Service Worker,manifest 新增到桌面,push、notification api 等。

而就在前不久時間,IOS 11.3 剛剛支援 Service worker 和類似 manifest 新增到桌面的特性,所以這次 PWA 改造主要還是實現這兩部分功能,至於其它的特性,等 iphone 支援了再升級吧。

Service Worker

service worker 在我看來,類似於一個跑在瀏覽器後臺的執行緒,頁面第一次載入的時候會載入這個執行緒,線上程啟用之後,通過對 fetch 事件,可以對每個獲取的資源進行控制快取等。

明確哪些資源需要被快取?

那麼在開始使用 service worker 之前,首先需要清楚哪些資源需要被快取?

快取靜態資源

首先是像 CSS、JS 這些靜態資源,因為我的部落格裡引用的指令碼樣式都是通過 hash 做持久化快取,類似於:main.ac62dexx.js 這樣,然後開啟強快取,這樣下次使用者下次再訪問我的網站的時候就不用重新請求資源。直接從瀏覽器快取中讀取。對於這部分資源,service worker 沒必要再去處理,直接放行讓它去讀取瀏覽器快取即可。

我認為如果你的站點載入靜態資源的時候本身沒有開啟強快取,並且你只想通過前端去實現快取,而不需要後端在介入進行調整,那可以使用 service worker 來快取靜態資源,否則就有點畫蛇添足了。

快取頁面

快取頁面顯然是必要的,這是最核心的部分,當你在離線的狀態下載入頁面會之後出現:

究其原因就是因為你在離線狀態下沒辦法載入頁面,現在有了 service worker,即使你在沒網路的情況下,也可以載入之前快取好的頁面了。

快取後端介面資料

快取介面資料是需要的,但也不是必須通過 service worker 來實現,前端存放資料的地方有很多,比如通過 localstorage,indexeddb 來進行儲存。這裡我也是通過 service worker 來實現快取介面資料的,如果想通過其它方式來實現,只需要注意好 url 路徑與資料對應的對映關係即可。

快取策略

明確了哪些資源需要被快取後,接下來就要談談快取策略了。

頁面快取策略

因為是 React 單頁同構應用,每次載入頁面的時候資料都是動態的,所以我採取的是:

  1. 網路優先的方式,即優先獲取網路上最新的資源。當網路請求失敗的時候,再去獲取 service worker 裡之前快取的資源
  2. 當網路載入成功之後,就更新 cache 中對應的快取資源,保證下次每次載入頁面,都是上次訪問的最新資源
  3. 如果找不到 service worker 中 url 對應的資源的時候,則去獲取 service worker 對應的 /index.html 預設首頁

為什麼存在命中不了快取頁面的情況?

  1. 首先需要明確的是,使用者在第一次載入你的站點的時候,載入頁面後才會去啟動 sw,所以第一次載入不可能通過 fetch 事件去快取頁面
  2. 我的部落格是單頁應用,但是使用者並不一定會通過首頁進入,有可能會通過其它頁面路徑進入到我的網站,這就導致我在 install 事件中根本沒辦法指定需要快取那些頁面
  3. 最終實現的效果是:使用者第一次開啟頁面,馬上斷掉網路,依然可以離線訪問我的站點

結合上面三點,我的方法是:第一次載入的時候會快取 /index.html 這個資源,並且快取頁面上的資料,如果使用者立刻離線載入的話,這時候並沒有快取對應的路徑,比如 /archives 資源訪問不到,這返回 /index.html 走非同步載入頁面的邏輯。

在 install 事件快取 /index.html,保證了 service worker 第一次載入的時候快取預設頁面,留下退路。

在頁面載入完後,在 React 元件中立刻快取資料:

這樣就保證了使用者第一次載入頁面,立刻離線訪問站點後,雖然無法像第一次一樣能夠服務端渲染資料,但是之後能通過獲取頁面,非同步載入資料的方式構建離線應用。

使用者第一次訪問站點,如果在不重新整理頁面的情況切換路由到其他頁面,則會非同步獲取到的資料,當下次訪問對應的路由的時候,則退化到非同步獲取資料。

當使用者第二次載入頁面的時候,因為 service worker 已經控制了站點,已經具備了快取頁面的能力,之後在訪問的頁面都將會被快取或者更新快取,當使用者離線訪問的的時候,也能訪問到服務端渲染的頁面了。

介面快取策略

談完頁面快取,再來講講介面快取,介面快取就跟頁面快取很類似了,唯一的不同在於:頁面第一次載入的時候不一定有快取,但是會有介面快取的存在(因為偽造了 cache 中的資料),所以快取策略跟頁面快取類似:

  1. 網路優先的方式,即優先獲取網路上介面資料。當網路請求失敗的時候,再去獲取 service worker 裡之前快取的介面資料
  2. 當網路載入成功之後,就更新 cache 中對應的快取介面資料,保證下次每次載入頁面,都是上次訪問的最新介面資料

所以程式碼就像這樣(程式碼類似,不再贅述):

這裡其實可以再進行優化的,比如在獲取資料介面的時候,可以先讀取快取中的介面資料進行渲染,當真正的網路介面資料返回之後再進行替換,這樣也能有效減少使用者的首屏渲染時間。當然這可能會發生頁面閃爍的效果,可以新增一些動畫來進行過渡。

其它問題

到現在為止,已經基本上可以實現 service worker 離線快取應用的效果了,但是還有仍然存在一些問題:

快速啟用 service worker

預設情況下,頁面的請求(fetch)不會通過 sw,除非它本身是通過 sw 獲取的,也就是說,在安裝 sw 之後,需要重新整理頁面才能有效果。sw 在安裝成功並啟用之前,不會響應 fetch或push等事件。

因為站點是單頁面應用,這就導致了你在切換路由(沒有重新整理頁面)的時候沒有快取介面資料,因為這時候 service worker 還沒有開始工作,所以在載入 service worker 的時候需要快速地啟用它。程式碼如下:

有的文章說還需要在 install 事件中新增 self.skipWaiting(); 來跳過等待時間,但是我在實踐中發現即使不新增也可以正常啟用 service worker,原因不詳,有讀者知道的話可以交流下。

現在當你第一次載入頁面,跳轉路由,立刻離線訪問的頁面,也可以順利地載入頁面了。

不要強快取 sw.js

使用者每次訪問頁面的時候都會去重新獲取 sw.js,根據檔案內容跟之前的版本是否一致來判斷 service worker 是否有更新。所以如果你對 sw.js 開啟強快取的話,就將陷入死迴圈,因為每次頁面獲取到的 sw.js 都是一樣,這樣就無法升級你的 service worker。

另外對 sw.js 開啟強快取也是沒有必要的:

  1. 本身 sw.js 檔案本身就很小,浪費不了多少頻寬,覺得浪費可以使用協商快取,但額外增加開發負擔
  2. sw.js 是在頁面空閒的時候才去載入的,並不會影響使用者首屏渲染速度

避免改變 sw 的 URL

在 sw 中這麼做是“最差實踐”,要在原地址上修改 sw。

舉個例子來說明為什麼:

  1. index.html 註冊了 sw-v1.js 作為 sw
  2. sw-v1.js 對 index.html 做了快取,也就是快取優先(offline-first)
  3. 你更新了 index.html 重新註冊了在新地址的 sw sw-v2.js

如果你像上面那麼做,使用者永遠也拿不到 sw-v2.js,因為 index.html 在 sw-v1.js 快取中,這樣的話,如果你想更新為 sw-v2.js,還需要更改原來的 sw-v1.js。

測試

自此,我們已經完成了使用 service worker 對頁面進行離線快取的功能,如果想體驗功能的話,訪問我的部落格:https://lindongzhou.com

隨意瀏覽任意的頁面,然後關掉網路,再次訪問,之前你瀏覽過的頁面都可以在離線的狀態下進行訪問了。

IOS 需要 11.3 的版本才支援,使用 Safari 進行訪問,Android 請選擇支援 service worker 的瀏覽器

manifest 桌面應用

前面講完了如何使用 service worker 來離線快取你的同構應用,但是 PWA 不僅限於此,你還可以使用設定 manifest 檔案來將你的站點新增到移動端的桌面上,從而達到趨近於原生應用的體驗。

使用 webpack-pwa-manifest 外掛

我的部落格站點是通過 webpack 來構建前端程式碼的,所以我在社群裡找到 webpack-pwa-manifest 外掛用來生成 manifest.json。

首先安裝好 webpack-pwa-manifest 外掛,然後在你的 webpack 配置檔案中新增:

簡單地闡述下配置資訊:

  1. name: 應用名稱,就是圖示下面的顯示名稱
  2. short_name: 應用名稱,但 name 無法顯示完全時候則顯示這個
  3. background_color、theme_color:顧名思義,相應的顏色
  4. publicPath: 設定 cdn 路徑,跟 webpack 裡的 publicPath 一樣
  5. icons: 設定圖示,外掛會自動幫你生成不同 size 的圖片,但是圖片大小必須大於最大 sizes
  6. ios: 設定在 safari 中如何去新增桌面應用

設定完之後,webpack 會在構建過程中生成相應的 manifest 檔案,並在 html 檔案中引用,下面就是生成 manifest 檔案:

html 中會引用這個檔案,並且加上對 ios 新增桌面應用的支援,就像這樣。

就這麼簡單,你就可以使用 webpack 來新增你的桌面應用了。

測試

新增完之後你可以通過 chrome 開發者工具 Application – Manifest 來檢視你的 mainfest 檔案是否生效:

這樣說明你的配置生效了,安卓機會自動識別你的配置檔案,並詢問使用者是否新增。

結尾

講到這差不多就完了,等以後 IOS 支援 PWA 的其它功能的時候,到時候我也會相應地去實踐其它 PWA 的特性的。現在 IOS 11.3 也僅僅支援 PWA 中的 service worker 和 app manifest 的功能,但是相信在不久的將來,其它的功能也會相應得到支援,到時候相信 PWA 將會在移動端綻放異彩的。

相關文章