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