- 原文作者:Addy Osmani
- 譯文出自:掘金翻譯計劃
- 譯者:Jiang Haichao
- 校對者:Gocy, David Lin
本期是新系列的第三部分,將介紹使用 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 可用的免費資源。
- 基于 Web 基本原理的 Service Worker 入門
- 你的第一個離線 webapp,web 基本原理編程實驗室
- Udacity 基于 Service Worker 的離線 Web 應用教程
- 推薦 Jake Archibald 的離線小書。
- 基于 Webpack 的漸進式 Web 應用 也是一個很棒的指南,學h會如何用基礎 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 的可訪問內容能夠通過以下這些不同的方式:
- 使用 Service Worker 基礎 API。GoogleChrome 樣例 和 Jake Archibald 的 離線小書 上有許多使用不同緩存策略的樣例.
- 在 package.json 腳本域中用一行代碼就能啟用 sw-precache 和 sw-toolbox。ReactHN 的例子在這里
- 在 Webpack 配置中使用類似 sw-precache-webpack-plugin 或者 offline-plugin 的插件。 react-boilerplate 這個啟動工具包已經(jīng)默認包含它了。
- 使用 create-react-app 和 Service Worker 庫 僅幾行代碼就能添加離線緩存支持(類似上一條)。
了解使用這些 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,你會對 堅持 Redux 和 Redux 復制本地搜索 感興趣的(后者壓縮后約 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ù)其他內容集結了資料如下:
- 如何刪除一個多 bug 的 Service Worker 或者實現(xiàn)一個終止開關?
- 測試 Service Worker 代碼有哪些方法?
- Service Worker 可以緩存 POST 請求嗎?
- 如何多個頁面注冊同一個 sw ?
- Service Worker 內部能夠讀取 cookie 嗎? (敬請期待)
- 如何處理 Service Worker 的全局錯誤?
其他資源:
- Service Worker 準備好了嗎??—?瀏覽器實現(xiàn)狀態(tài)和資源
- 立即加載:構建離線優(yōu)先的漸進式 Web 應用?—?Jake
- 漸進式 Web 應用的離線支持?—?完全工具指南
- 使用 Service Worker 實現(xiàn)立即加載?—?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 的評論。