使用 React.js 的漸進(jìn)式 Web 應(yīng)用程序:第 3 部分 - 離線支持和網(wǎng)絡(luò)恢復(fù)能力

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

一個(gè)好的漸進(jìn)式 Web 應(yīng)用,不論網(wǎng)絡(luò)狀況如何都能立即加載,并且在不需要網(wǎng)絡(luò)請(qǐng)求的情況下也能展示 UI (即離線時(shí))。

再次訪問 Housing.com 漸進(jìn)式 Web 應(yīng)用(使用 React 和 Redux 構(gòu)建)能夠立即加載離線緩存的 UI。

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

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

Service worker 實(shí)際上依賴兩個(gè) API:Fetch (通過網(wǎng)絡(luò)重新獲取內(nèi)容的標(biāo)準(zhǔn)方式) 和 Cache(應(yīng)用數(shù)據(jù)的內(nèi)容存儲(chǔ),此緩存獨(dú)立于瀏覽器緩存和網(wǎng)絡(luò)狀態(tài))。

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

高級(jí)特性基礎(chǔ)

Service worker 也設(shè)計(jì)作為基礎(chǔ) API,讓 web 應(yīng)用更像 native 應(yīng)用。具體包括:

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

Service Worker 生命周期

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

注冊(cè)

如果要安裝 Service Worker,你需要在腳本里注冊(cè)它。注冊(cè)后會(huì)通知瀏覽器定位你的 Service Worker 文件,并啟動(dòng)后臺(tái)安裝。在 index.html 中的基本注冊(cè)方法如下:

// 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 注冊(cè),注冊(cè)成功后返回一個(gè) resolve 狀態(tài)的 Promise 對(duì)象。作用域是 registration.scope。

作用域

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

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

安裝和激活

Service workers 是事件驅(qū)動(dòng)的。安裝和激活方法由對(duì)應(yīng)的安裝和激活事件觸發(fā),由 Service Worker 響應(yīng)。

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

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

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() 傳入一個(gè) URL 數(shù)組,請(qǐng)求并獲取文件,然后添加到緩存中去。如果任一步驟獲取/寫入失敗,整個(gè)操作失敗,并且緩存回退到它的上一個(gè)狀態(tài)。

攔截和緩存請(qǐng)求

當(dāng) Service Worker 控制頁面時(shí),它能夠攔截頁面發(fā)起的每個(gè)請(qǐng)求,并且決定如何處理。這使得它有點(diǎn)像后臺(tái)代理。我們用它來攔截到 urlsToCache 列表的請(qǐng)求,接著返回資源的本地版本,而不是走網(wǎng)絡(luò)獲取資源。這通過在 fetch 事件上綁定處理方法實(shí)現(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() 方法傳入一個(gè) promise 對(duì)象,這個(gè)能夠監(jiān)聽請(qǐng)求和從 Service Worker 創(chuàng)建的條目中發(fā)現(xiàn)緩存。如果有匹配的緩存響應(yīng),返回對(duì)應(yīng)的值。

這就是 Service Worker。以下是學(xué)習(xí) Service Worker 可用的免費(fèi)資源。

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

探索 - 自定義離線頁面

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

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

Lighthouse

如果你的應(yīng)用在離線時(shí)有充分的用戶體驗(yàn),在遇到 Lighthouse 檢測(cè)的如下條件時(shí),就會(huì)全部通過。

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

Chrome 開發(fā)工具

開發(fā)工具通過應(yīng)用選項(xiàng)卡支持 「調(diào)試 Service Worker」 和 「模擬脫機(jī)連通性」。

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

應(yīng)用外殼架構(gòu)

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

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

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

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

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

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

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

利用 Service Worker 啟動(dòng)低成本緩存

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

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

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

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

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

React app 中的離線緩存

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

了解使用這些 SW 庫構(gòu)建一個(gè) React 應(yīng)用的討論也是大有裨益的:

sw-precache 對(duì)比 offline-plugin

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

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

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

迷你案例學(xué)習(xí):為 ReactHN 添加離線緩存

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

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

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

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

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

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

注意:如果現(xiàn)在開始做離線緩存功能,我會(huì)只用 sw-precache-webpack-plugin 從標(biāo)準(zhǔn) 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
      }
    ),

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

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

從 API 端點(diǎn)(例如一個(gè) appspot.com 上的應(yīng)用引擎)緩存數(shù)據(jù)請(qǐng)求,類似如下:

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

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

第三步:仔細(xì)想一想對(duì)你的用戶來說,什么是最有幫助的離線體驗(yàn)。每個(gè)應(yīng)用都有所不同。

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

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

離線 Google 分析

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

IndexedDB 中的離線 Google 分析事件隊(duì)列

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

普遍問題(和答案)

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

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

根據(jù)其他內(nèi)容集結(jié)了資料如下:

其他資源:

最后結(jié)語!

在這個(gè)系列的第四部分,我們會(huì)重點(diǎn)關(guān)注使用全局渲染來漸進(jìn)增強(qiáng) React.js 漸進(jìn)式 Web 應(yīng)用

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容