使用 React.js 的漸進式 Web 應用程序:第 3 部分 - 離線支持和網(wǎng)絡恢復能力

本期是新系列的第三部分,將介紹使用 Lighthouse 優(yōu)化移動 web 應用傳輸?shù)募记伞?并看看如何使你的 React 應用離線工作。

一個好的漸進式 Web 應用,不論網(wǎng)絡狀況如何都能立即加載,并且在不需要網(wǎng)絡請求的情況下也能展示 UI (即離線時)。

再次訪問 Housing.com 漸進式 Web 應用(使用 React 和 Redux 構建)能夠立即加載離線緩存的 UI。

我們可以用 Service Worker 實現(xiàn)這一需求。Service Worker 是一個后臺 worker,可以看做是可編程的代理,允許開發(fā)者控制 request 執(zhí)行其他操作。使用 Service Worker,React 應用得以(部分或全部)離線工作。

你能夠掌控離線時 UX 的可用程度。你可以只離線緩存應用的外殼,全部數(shù)據(jù)(就像 ReactHN 緩存 stories 一樣),或者像 Housing.com 和 Flipkart 那樣,提供有限但有幫助的靜態(tài)舊數(shù)據(jù)。并且均通過置灰 UI 蒙層來暗示已離線,這樣就能夠感知“實時”價格還未同步。

Service worker 實際上依賴兩個 API:Fetch (通過網(wǎng)絡重新獲取內容的標準方式) 和 Cache(應用數(shù)據(jù)的內容存儲,此緩存獨立于瀏覽器緩存和網(wǎng)絡狀態(tài))。

注意:Service worker 能夠應用于漸進式增強。盡管瀏覽器支持程度還有待提升,但只要網(wǎng)絡暢通,不支持此特性的用戶也能充分體驗 PWA (漸進式 Web 應用程序)。

高級特性基礎

Service worker 也設計作為基礎 API,讓 web 應用更像 native 應用。具體包括:

  • 推送 API - 啟用 web 應用消息推送服務。服務器能夠任意發(fā)送消息,即使 web 應用或瀏覽器不在工作狀態(tài)。
  • 后臺同步 - 延遲處理直到用戶網(wǎng)絡連接穩(wěn)定為止。這能方便保證用戶消息的正確發(fā)送。應用下次在線時能夠啟動自動定期更新。

Service Worker 生命周期

每個 Service Worker 的生命周期有三步:注冊,安裝和激活。Jake Archibald 的這篇文章有更詳細的說明

注冊

如果要安裝 Service Worker,你需要在腳本里注冊它。注冊后會通知瀏覽器定位你的 Service Worker 文件,并啟動后臺安裝。在 index.html 中的基本注冊方法如下:

// Check for browser support of service worker
if ('serviceWorker' in navigator) {

 navigator.serviceWorker.register('service-worker.js')
 .then(function(registration) {
   // Successful registration
   console.log('Hooray. Registration successful, scope is:', registration.scope);
 }).catch(function(err) {
   // Failed registration, service worker won’t be installed
   console.log('Whoops. Service worker registration failed, error:', error);
 });

}

使用 navigator.serviceWorker.register 注冊,注冊成功后返回一個 resolve 狀態(tài)的 Promise 對象。作用域是 registration.scope。

作用域

Service Worker 的作用域由攔截請求的路徑?jīng)Q定。默認作用域是 Service Worker 文件所在路徑。如果 service-worker.js 在根目錄下,則 Service Worker 將控制該域名下所有文件的訪問請求。你可以通過在注冊時傳入其他參數(shù)來改變作用域。

navigator.serviceWorker.register('service-worker.js', {
 scope: '/app/'
});

安裝和激活

Service workers 是事件驅動的。安裝和激活方法由對應的安裝和激活事件觸發(fā),由 Service Worker 響應。

Service Worker 注冊之后,用戶第一次訪問 PWA 時,install 事件觸發(fā),此時確定頁面需要緩存的靜態(tài)資源。當 Service Worker 被認為是的時才會觸發(fā)該事件,即要么是頁面第一次加載 Service Worker 文件,要么是當前文件與之前安裝的文件不同,哪怕是一個字節(jié)不同,都會被認為是新的。如果你想在有機會控制客戶端之前緩存東西,那么 install 是關鍵所在。

我們可以使用以下代碼為靜態(tài)應用添加最基本的緩存:

var CACHE_NAME = 'my-pwa-cache-v1';
var urlsToCache = [
  '/',
  '/styles/styles.css',
  '/script/webpack-bundle.js'
];

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        // Open a cache and cache our files
        return cache.addAll(urlsToCache);
      })
  );
});

addAll() 傳入一個 URL 數(shù)組,請求并獲取文件,然后添加到緩存中去。如果任一步驟獲取/寫入失敗,整個操作失敗,并且緩存回退到它的上一個狀態(tài)。

攔截和緩存請求

當 Service Worker 控制頁面時,它能夠攔截頁面發(fā)起的每個請求,并且決定如何處理。這使得它有點像后臺代理。我們用它來攔截到 urlsToCache 列表的請求,接著返回資源的本地版本,而不是走網(wǎng)絡獲取資源。這通過在 fetch 事件上綁定處理方法實現(xiàn):

self.addEventListener('fetch', function(event) {
    console.log(event.request.url);
    event.respondWith(
        caches.match(event.request).then(function(response) {
            return response || fetch(event.request);
        })
    );
});

在 fetch 監(jiān)聽器中(具體的說是 event.respondWith),向 caches.match() 方法傳入一個 promise 對象,這個能夠監(jiān)聽請求和從 Service Worker 創(chuàng)建的條目中發(fā)現(xiàn)緩存。如果有匹配的緩存響應,返回對應的值。

這就是 Service Worker。以下是學習 Service Worker 可用的免費資源。

如果第三方 API 想要部署他們自己的 Service Worker 來處理其他域傳來的請求,Foreign Fetch 可以幫忙。這對于網(wǎng)絡化邏輯自定義和單個緩存實例響應定義都有幫助。

探索 - 自定義離線頁面

基于 React 的 mobile.twitter.com 用 Service Worker 在網(wǎng)絡不可達時提供自定義離線頁面。

為用戶提供有意義的離線體驗(例如:可讀內容)是一個很好的目標。也就是說,在早期的 Service Worker 實驗中,你會發(fā)現(xiàn)設置自定義離線頁面是很小但正確的決定。這里有許多優(yōu)秀的 案例 展示如何實現(xiàn)它。

Lighthouse

如果你的應用在離線時有充分的用戶體驗,在遇到 Lighthouse 檢測的如下條件時,就會全部通過。

start_url 便于檢查用戶從主界面打開 PWA 時使用離線緩存的體驗情況,這項檢查能夠發(fā)現(xiàn)許多的問題,所以要確保 start_url 在你的 Web 應用的 manifest 中。

Chrome 開發(fā)工具

開發(fā)工具通過應用選項卡支持 「調試 Service Worker」 和 「模擬脫機連通性」。

強烈推薦使用 3G 節(jié)流(和 Timeline 面板的 CPU 節(jié)流)開發(fā),模擬低端硬件上應用在脫機和網(wǎng)絡差的情況下的表現(xiàn)。

應用外殼架構

應用程序外殼(或者應用外殼)架構是構建可靠的和在客戶機立即加載的漸進式 Web 應用的一個方法,與 native 應用類似。

應用“外殼” 是最小化的 HTML,CSS 和 JavaScript,要求為用戶接口賦能(想想 toolbars,drawers 等等),確保用戶重復訪問時即時可靠的性能表現(xiàn)。這意味著應用程序外殼不需要每次都下載,只需要網(wǎng)絡獲取少量必要內容即可。

Housing.com 使用了內容占位符的應用外殼。一旦全部下載完成,立即填充占位,此舉有助于提升感官性能。

對于富 JavaScript 架構的 單頁應用 來說,應用外殼是首選方法。這個方法依賴外殼的緩存(利用 Service Worker)來運行程序。其次,用 JavaScript 加載每個頁面的動態(tài)內容。在無網(wǎng)絡情況下,應用外殼有助于更快的獲取屏幕的起始 HTML 頁面。外殼可以使用 Material UI 或是自定義風格。

注意:參考 第一個漸進式 Web 應用 學習設計和實現(xiàn)第一個應用外殼程序,以天氣應用為樣例。用應用外殼模型實現(xiàn)立即加載 同樣探討了這個模式。

我們利用 Cache Storage API(通過 Service Worker)離線緩存外殼,目的是當重復訪問時,應用外殼能夠立即加載,這樣就能在無網(wǎng)絡情況下快速獲取屏幕信息,即使內容最終還是來自網(wǎng)絡。

記住你可以使用更簡單的 SSR 或者 SPA 架構開發(fā) PWA,但它沒有同樣的性能優(yōu)勢并且更依賴全頁緩存。

利用 Service Worker 啟動低成本緩存

這里列舉兩個用于不同離線場景的庫:sw-precache 會自動事先緩存靜態(tài)資源,sw-toolbox 處理運行時緩存以及回退策略。這兩個庫一起使用能達到互補的效果,需要提供靜態(tài)內容外殼的性能策略時,總是從緩存中直接獲取,而動態(tài)的或遠程的資源則通過網(wǎng)絡請求提供,需要時回退到緩存或靜態(tài)響應里。

應用外殼緩存:靜態(tài)資源(HTML, JavaScript, CSS 和 images)提供 web 應用的核心外殼。Sw-precache 確保絕大多數(shù)這類靜態(tài)資源都被緩存下來,并且保持更新。預緩存一個網(wǎng)站離線工作需要的所有資源顯然是不現(xiàn)實的。

運行時緩存:一些過于龐大或者很少使用的資源,還有一些動態(tài)資源,像來自遠程 API 或服務的響應。沒有預緩存的請求并不一定要響應網(wǎng)絡錯誤。sw-toolbox 讓我們得以靈活實現(xiàn)請求的處理,這能夠處理某些資源的運行時緩存和其他資源的自定義回退。

sw-toolbox 支持大多數(shù)不同緩存策略,包括網(wǎng)絡優(yōu)先(確保可用數(shù)據(jù)是最新的,而不是讀取緩存),緩存優(yōu)先(匹配請求與緩存列表,如果資源不存在則發(fā)起網(wǎng)絡請求),速度優(yōu)先(同時從緩存和網(wǎng)絡請求資源,響應最快的返回結果)。了解這些方法的 優(yōu)劣 十分重要。

許多網(wǎng)站都在各自的漸進式 Web 應用里利用 sw-toolbox 和 sw-precache 進行離線緩存,例如 Housing.com,the NFL,F(xiàn)lipkart,Alibaba,the Washington Post 等等。也就是說,我們能夠一直關注反饋和優(yōu)化方案。

React app 中的離線緩存

利用 Service Worker 和 Cache Storage API 緩存 URL 的可訪問內容能夠通過以下這些不同的方式:

了解使用這些 SW 庫構建一個 React 應用的討論也是大有裨益的:

sw-precache 對比 offline-plugin

正如上文提到,offline-plugin 是另一個庫,用于添加 Service Worker 緩存到頁面。它設計理念是最小化配置(目標是零配置) 和 Webpack的深度整合。當 Webpack 的 publicPath 配置了,它能夠自動為緩存生成 relativePaths,而不需要再指定其他配置。對靜態(tài)網(wǎng)站來說,offline-plugin 是一個很好的 sw-precache 的替代品。如果你用的是 HtmlWebpackPlugin,offline-plugin 還能緩存 .html 頁面。

module.exports = {
  plugins: [
    // ... other plugins
    new OfflinePlugin()
  ]
}

我在 漸進式 Web 應用的離線緩存 中講了其他類型數(shù)據(jù)的離線存儲策略。尤其是 React,如果你正關注添加數(shù)據(jù)倉庫到緩存或正使用 Redux,你會對 堅持 ReduxRedux 復制本地搜索 感興趣的(后者壓縮后約 8 KB)。

迷你案例學習:為 ReactHN 添加離線緩存

ReactHN 一開始是沒有離線緩存的單頁應用。我們按步驟添加離線緩存:

第一步:用 sw-precache 為應用 “外殼” 離線緩存靜態(tài)資源。通過調用 package.json 里 script 域的 sw-precache CLI 工具,每次構建完成時產(chǎn)生一個 Service Worker 用于預緩存外殼

"precache": "sw-precache — root=public — config=sw-precache-config.json"

這份預緩存配置文件通過上面的命令傳遞,可以控制引入的文件和 helper 腳本:

{
  "staticFileGlobs": [
    "app/css/**.css",
    "app/**.html",
    "app/js/**.js",
    "app/images/**.*"
  ],
  "verbose": true,
  "importScripts": [
    "sw-toolbox.js",
    "runtime-caching.js"
  ]
}

sw-precache 在輸出結果中列出將離線緩存的靜態(tài)資源總大小。這有利于明白多大的應用外殼和資源能夠保證良好的交互體驗。

注意:如果現(xiàn)在開始做離線緩存功能,我會只用 sw-precache-webpack-plugin 從標準 Webpack 配置中直接配置:

plugins: [
    new SWPrecacheWebpackPlugin(
      {
        cacheId: "react-hn",
        filename: "my-service-worker.js",
        staticFileGlobs: [
          "app/css/**.css",
          "app/**.html",
          "app/js/**.js",
          "app/images/**.*"
        ],
       verbose: true
      }
    ),

第二步:我們還想緩存運行時/動態(tài)請求。為了實現(xiàn)這一功能,我們需要引入 sw-toolbox 和上面的運行時緩存配置。應用使用了 Google Fonts 網(wǎng)絡字體,所以我們添加一個簡單的規(guī)則,緩存所有 google.com 的 fonts 子域下的請求。

global.toolbox.router.get('/(.+)', global.toolbox.fastest, {
   origin: /https?:\/\/fonts.+/
});

從 API 端點(例如一個 appspot.com 上的應用引擎)緩存數(shù)據(jù)請求,類似如下:

global.toolbox.router.get('/(.*)', global.toolbox.fastest, {
   origin: /\.(?:appspot)\.com$/
})

注意:sw-toolbox 支持許多有用的選項,包括能夠設置緩存條目的最大失效時長(借助 maxAgeSeconds)。要了解更多支持細節(jié),請閱讀 API docs。

第三步:仔細想一想對你的用戶來說,什么是最有幫助的離線體驗。每個應用都有所不同。

ReactHN 依賴服務器返回的實時新聞報道和評論數(shù)據(jù)。一番實驗之后,我們發(fā)現(xiàn) UX 和性能之間的一個平衡點是用 稍微 老舊的數(shù)據(jù)提供離線體驗。

從其他已經(jīng)發(fā)布的 PWA 上可以學到很多東西,鼓勵大家盡可能地研究和分享學習成果。?

離線 Google 分析

一旦在你的 PWA 使用 Service Worker 提升離線體驗,你的關注點就會移向別處,比如,確保 Google 分析離線可用,如果你嘗試離線 GA,請求會失敗,你也不能得到有用的數(shù)據(jù)狀態(tài)。

IndexedDB 中的離線 Google 分析事件隊列

我們可以用 離線 Google 分析庫 解決這一問題(sw-offline-google-analytics)來解決這一問題。當用戶離線時,入隊所有 GA 請求,并且一旦網(wǎng)絡再次可用,就嘗試重連。我們今年的 Google I/O web app
就成功使用了相似的技術,鼓勵大家都去試一試。

普遍問題(和答案)

對我來說,Service Worker 最難搞的部分就是調試。但去年開始,Chrome DevTools 顯著降低了調試難度。為了節(jié)約你的時間和減少稍后踩的大坑,我強烈推薦在 SW debugging codelab 上做開發(fā)。??

記錄你發(fā)現(xiàn)的技巧或者新知識也可以幫助別人。Rich Harris 就寫了 Service Worker 早知道

根據(jù)其他內容集結了資料如下:

其他資源:

最后結語!

在這個系列的第四部分,我們會重點關注使用全局渲染來漸進增強 React.js 漸進式 Web 應用

如果你剛了解 React,Wes Bos 的 React 入門 很適合你。

感謝 Gray Norton, Sean Larkin, Sunil Pai, Max Stoiber, Simon Boudrias, Kyle Mathews, Arthur Stolyar 和 Owen Campbell-Moore 的評論。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容