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

  • 原文地址:Progressive Web Apps with React.js: Part 3?—?Offline support and network resilience
  • 原文作者:Addy Osmani
  • 譯文出自:掘金翻譯計劃
  • 譯者:Jiang Haichao
  • 校對者:Gocy, David Lin

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

本期是新系列的第三部分,將介紹使用 Lighthouse 優化移動 web 應用傳輸的技巧。 並看看如何使你的 React 應用離線工作。

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

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

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

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

Service worker 實際上依賴兩個 API:Fetch (通過網路重新獲取內容的標準方式) 和 Cache(應用數據的內容存儲,此緩存獨立於瀏覽器緩存和網路狀態)。

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

高級特性基礎

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

  • 推送 API - 啟用 web 應用消息推送服務。伺服器能夠任意發送消息,即使 web 應用或瀏覽器不在工作狀態。
  • 後台同步 - 延遲處理直到用戶網路連接穩定為止。這能方便保證用戶消息的正確發送。應用下次在線時能夠啟動自動定期更新。

Service Worker 生命周期

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

註冊

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

// Check for browser support of service workerif ("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 狀態的 Promise 對象。作用域是 registration.scope。

作用域

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

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

安裝和激活

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

Service Worker 註冊之後,用戶第一次訪問 PWA 時,install 事件觸發,此時確定頁面需要緩存的靜態資源。當 Service Worker 被認為是的時才會觸發該事件,即要麼是頁面第一次載入 Service Worker 文件,要麼是當前文件與之前安裝的文件不同,哪怕是一個位元組不同,都會被認為是新的。如果你想在有機會控制客戶端之前緩存東西,那麼 install 是關鍵所在。

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

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 數組,請求並獲取文件,然後添加到緩存中去。如果任一步驟獲取/寫入失敗,整個操作失敗,並且緩存回退到它的上一個狀態。

攔截和緩存請求

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

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 監聽器中(具體的說是 event.respondWith),向 caches.match() 方法傳入一個 promise 對象,這個能夠監聽請求和從 Service Worker 創建的條目中發現緩存。如果有匹配的緩存響應,返回對應的值。

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

  • 基於 Web 基本原理的 Service Worker 入門
  • 你的第一個離線 webapp,web 基本原理編程實驗室
  • Udacity 基於 Service Worker 的離線 Web 應用教程
  • 推薦 Jake Archibald 的離線小書。
  • 基於 Webpack 的漸進式 Web 應用 也是一個很棒的指南,學h會如何用基礎 Service Worker 代碼啟用離線緩存(如果你不喜歡用庫的話)。

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

探索 - 自定義離線頁面

基於 React 的 mobile.twitter.com 用 Service Worker 在網路不可達時提供自定義離線頁面。

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

Lighthouse

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

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

Chrome 開發工具

開發工具通過應用選項卡支持 「調試 Service Worker」 和 「模擬離線連通性」。

強烈推薦使用 3G 節流(和 Timeline 面板的 CPU 節流)開發,模擬低端硬體上應用在離線和網路差的情況下的表現。

應用外殼架構

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

應用「外殼」 是最小化的 HTML,CSS 和 JavaScript,要求為用戶介面賦能(想想 toolbars,drawers 等等),確保用戶重複訪問時即時可靠的性能表現。這意味著應用程序外殼不需要每次都下載,只需要網路獲取少量必要內容即可。

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

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

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

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

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

利用 Service Worker 啟動低成本緩存

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

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

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

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

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

React app 中的離線緩存

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

  • 使用 Service Worker 基礎 API。GoogleChrome 樣例 和 Jake Archibald 的 離線小書 上有許多使用不同緩存策略的樣例.
  • 在 package.json 腳本域中用一行代碼就能啟用 sw-precache 和 sw-toolbox。ReactHN 的例子在這裡
  • 在 Webpack 配置中使用類似 sw-precache-webpack-plugin 或者 offline-plugin 的插件。 react-boilerplate 這個啟動工具包已經默認包含它了。
  • 使用 create-react-app 和 Service Worker 庫 僅幾行代碼就能添加離線緩存支持(類似上一條)。

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

  • 面向 Lighthouse (PWA 提交)
  • 跨框架的漸進式 Web 應用

sw-precache 對比 offline-plugin

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

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

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

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

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

第一步:用 sw-precache 為應用 「外殼」 離線緩存靜態資源。通過調用 package.json 里 script 域的 sw-precache CLI 工具,每次構建完成時產生一個 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 在輸出結果中列出將離線緩存的靜態資源總大小。這有利於明白多大的應用外殼和資源能夠保證良好的交互體驗。

注意:如果現在開始做離線緩存功能,我會只用 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 } ),

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

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

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

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

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

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

ReactHN 依賴伺服器返回的實時新聞報道和評論數據。一番實驗之後,我們發現 UX 和性能之間的一個平衡點是用 稍微 老舊的數據提供離線體驗。

從其他已經發布的 PWA 上可以學到很多東西,鼓勵大家儘可能地研究和分享學習成果。?

離線 Google 分析

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

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

我們可以用 離線 Google 分析庫 解決這一問題(sw-offline-google-analytics)來解決這一問題。當用戶離線時,入隊所有 GA 請求,並且一旦網路再次可用,就嘗試重連。我們今年的 Google I/O web app

就成功使用了相似的技術,鼓勵大家都去試一試。

普遍問題(和答案)

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

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

根據其他內容集結了資料如下:

  • 如何刪除一個多 bug 的 Service Worker 或者實現一個終止開關?
  • 測試 Service Worker 代碼有哪些方法?
  • Service Worker 可以緩存 POST 請求嗎?
  • 如何多個頁面註冊同一個 sw ?
  • Service Worker 內部能夠讀取 cookie 嗎? (敬請期待)
  • 如何處理 Service Worker 的全局錯誤?

其他資源:

  • Service Worker 準備好了嗎??—?瀏覽器實現狀態和資源
  • 立即載入:構建離線優先的漸進式 Web 應用?—?Jake
  • 漸進式 Web 應用的離線支持?—?完全工具指南
  • 使用 Service Worker 實現立即載入?—?Jeff Posnick
  • Mozilla Service Worker 小書
  • 開始使用 Service Worker 工具箱—?Dean Hume
  • Service Worker 單元測試相關資源?—?Matt Gaunt

最後結語!

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

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

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

推薦閱讀:

基於 Webpack 的應用包體尺寸優化
React 實現一個漂亮的 Table
React Conf 2017 不能錯過的大起底——Day 1!
解析 Redux 源碼

TAG:React | 前端开发 | JavaScript |