科普 | Service Worker 入門指南
Service Worker 簡介
Service Workers 本質(zhì)上是一種能在瀏覽器后臺運(yùn)行的獨(dú)立線程,它能夠在網(wǎng)頁關(guān)閉后持續(xù)運(yùn)行,能夠攔截網(wǎng)絡(luò)請求并根據(jù)網(wǎng)絡(luò)是否可用來采取適當(dāng)?shù)膭幼?、更新來自服?wù)器的的資源,從而實現(xiàn)攔截和加工網(wǎng)絡(luò)請求、消息推送、靜默更新、事件同步等一系列功能,是 PWA 應(yīng)用的核心技術(shù)之一。
與普通 JS 運(yùn)行環(huán)境相比,Service Workers 有如下特點(diǎn):
- 無法直接訪問 DOM , 可通過 postMessage 發(fā)送消息與頁面通信。
- 能夠控制頁面發(fā)送網(wǎng)絡(luò)請求。
- 必須在 HTTPS 協(xié)議下運(yùn)行。
- 開發(fā)過程中可以通過 locakhost 使用 service worker。
本文將從應(yīng)用角度,簡單匯總下 Service Workers 幾個核心概念,包括:API、生命周期、waitUntil 機(jī)制、調(diào)試等。
生命周期
Service Worker 的生命周期完全獨(dú)立于網(wǎng)頁。生命周期 (install -> waiting -> activate -> fetch):
其中, install 事件是 Service Worker 獲取的第一個事件,并且只發(fā)生一次。
主要邏輯 & API
- register
- install
- activate
- fetch
- skipWaiting
// register
if ('serviceWorker' in navigator) {
// 由于 127.0.0.1:8000 是所有測試 Demo 的 host
// 為了防止作用域污染,將安裝前注銷所有已生效的 Service Worker
navigator.serviceWorker.getRegistrations()
.then(regs => {
for (let reg of regs) {
reg.unregister()
}
navigator.serviceWorker.register('./sw.js')
})
}
// sw.js
console.log('service worker 注冊成功')
self.addEventListener('install', () => {
// 安裝回調(diào)的邏輯處理
console.log('service worker 安裝成功')
})
self.addEventListener('activate', () => {
// 激活回調(diào)的邏輯處理
console.log('service worker 激活成功')
})
self.addEventListener('fetch', event => {
console.log('service worker 抓取請求成功: ' + event.request.url)
})
「waitUntil 機(jī)制」
參考:??https://developer.mozilla.org/zh-CN/docs/Web/API/ExtendableEvent/waitUntil。??
ExtendableEvent.waitUntil() 方法告訴事件分發(fā)器該事件仍在進(jìn)行。這個方法也可以用于檢測進(jìn)行的任務(wù)是否成功。在服務(wù)工作線程中,這個方法告訴瀏覽器事件一直進(jìn)行,直至 promise resolve,瀏覽器不應(yīng)該在事件中的異步操作完成之前終止服務(wù)工作線程。
「skipWaiting」
Service Worker 一旦更新,需要等所有的終端都關(guān)閉之后,再重新打開頁面才能激活新的 Service Worker,這個過程太復(fù)雜了。通常情況下,開發(fā)者希望當(dāng) Service Worker 一檢測到更新就直接激活新的 Service Worker。如果不想等所有的終端都關(guān)閉再打開的話,只能通過 skipWaiting 的方法了。
Service Worker 在全局提供了一個 skipWaiting() 方法,skipWaiting() 在 waiting 期間調(diào)用還是在之前調(diào)用并沒有什么不同。一般情況下是在 install 事件中調(diào)用它。
「clients.claim() 方法」
如果使用了 skipWaiting 的方式跳過 waiting 狀態(tài),直接激活了 Service Worker,可能會出現(xiàn)其他終端還沒有受當(dāng)前終端激活的 Service Worker 控制的情況,切回其他終端之后,Service Worker 控制頁面的效果可能不符合預(yù)期,尤其是如果 Service Worker 需要動態(tài)攔截第三方請求的時候。
為了保證 Service Worker 激活之后能夠馬上作用于所有的終端,通常在激活 Service Worker 后,通過在其中調(diào)用 self.clients.claim() 方法控制未受控制的客戶端。self.clients.claim() 方法返回一個 Promise,可以直接在 waitUntil() 方法中調(diào)用,如下代碼所示:
self.addEventListener('activate', event => {
event.waitUntil(
self.clients.claim()
.then(() => {
// 返回處理緩存更新的相關(guān)事情的 Promise
})
)
})
如何處理 Service Worker 的更新
- 如果目前尚未有活躍的 SW ,那就直接安裝并激活。
- 如果已有 SW 安裝著,向新的 swUrl 發(fā)起請求,獲取內(nèi)容和和已有的 SW 比較。如沒有差別,則結(jié)束安裝。如有差別,則安裝新版本的 SW(執(zhí)行 install 階段),之后令其等待(進(jìn)入 waiting 階段)。
- 如果老的 SW 控制的所有頁面 「全部關(guān)閉」,則老的 SW 結(jié)束運(yùn)行,轉(zhuǎn)而激活新的 SW(執(zhí)行 activated 階段),使之接管頁面。
方法一:skipWaiting
問題:同一個頁面,前半部分的請求是由 sw.v1.js 控制,而后半部分是由 sw.v2.js 控制。這兩者的不一致性很容易導(dǎo)致問題,甚至網(wǎng)頁報錯崩潰。
方法二:skipWaiting + 刷新
let refreshing = false
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) {
return
}
refreshing = true;
window.location.reload();
});
問題:毫無征兆的刷新頁面的確不可接受,影響用戶體驗。
方法三:給用戶一個提示
大致的流程是:
- 瀏覽器檢測到存在新的(不同的)SW 時,安裝并讓它等待,同時觸發(fā) updatefound 事件。
- 我們監(jiān)聽事件,彈出一個提示條,詢問用戶是不是要更新 SW。
- 如果用戶確認(rèn),則向處在等待的 SW 發(fā)送消息,要求其執(zhí)行 skipWaiting 并取得控制權(quán)。
- 因為 SW 的變化觸發(fā) controllerchange 事件,我們在這個事件的回調(diào)中刷新頁面即可。
問題:
- 弊端一:過于復(fù)雜。
- 弊端二:刷新邏輯的實現(xiàn)必須通過 JS 完成更新。
如何調(diào)試
為了更熟練的運(yùn)用 Chrome Devtools 調(diào)試 Service Worker,首先需要熟悉以下這些選項:
- 「Offline」:復(fù)選框可以將 DevTools 切換至離線模式。它等同于 Network 窗格中的離線模式。
- 「Update on reload」:復(fù)選框可以強(qiáng)制 Service Worker 線程在每次頁面加載時更新。
- 「Bypass for network」:復(fù)選框可以繞過 Service Worker 線程并強(qiáng)制瀏覽器轉(zhuǎn)至網(wǎng)絡(luò)尋找請求的資源。
- 「Update」:按鈕可以對指定的 Service Worker 線程執(zhí)行一次性更新。
- 「Push」:按鈕可以在沒有負(fù)載的情況下模擬推送通知。
- 「Sync」:按鈕可以模擬后臺同步事件。
- 「Unregister」:按鈕可以注銷指定的 Service Worker 線程。
- 「Source」:告訴當(dāng)前正在運(yùn)行的 Service Worker 線程的安裝時間,鏈接是 Service Worker 線程源文件的名稱。點(diǎn)擊鏈接會將定向并跳轉(zhuǎn)至 Service Worker 線程來源。
- 「Status」:告訴 Service Worker 線程的狀態(tài)。此行上的數(shù)字指示 Service Worker 線程已被更新的次數(shù)。如果啟用 update on reload 復(fù)選框,接下來會注意到每次頁面加載時此數(shù)字都會增大。在狀態(tài)旁邊會看到 start 按鈕(如果 Service Worker 線程已停止)或 stop 按鈕(如果 Service Worker 線程正在運(yùn)行)。Service Worker 線程設(shè)計為可由瀏覽器隨時停止和啟動。使用 stop 按鈕明確停止 Service Worker 線程可以模擬這一點(diǎn)。停止 Service Worker 線程是測試 Service Worker 線程再次重新啟動時的代碼行為方式的絕佳方法。它通??梢越沂居捎趯Τ掷m(xù)全局狀態(tài)的不完善假設(shè)而引發(fā)的錯誤。
- 「Clients」:告訴 Service Worker 線程作用域的原點(diǎn)。如果已啟用 show all 復(fù)選框,focus 按鈕將非常實用。在此復(fù)選框啟用時,系統(tǒng)會列出所有注冊的 Service Worker 線程。如果這時候點(diǎn)擊正在不同標(biāo)簽中運(yùn)行的 Service Worker 線程旁的 focus 按鈕,Chrome 會聚焦到該標(biāo)簽。
總結(jié)
完整流程
應(yīng)用場景
基于service worker 可以實現(xiàn)攔截和處理網(wǎng)絡(luò)請求、消息推送、靜默更新、事件同步等服務(wù)。
- 離線緩存:配合 CacheStorage 可以將應(yīng)用中不變化的資源或者很少變化的資源長久的存儲在用戶端,提升加載速度、降低流量消耗、降低服務(wù)器壓力,提高請求速度,讓用戶體驗更加絲滑。
- 消息推送:激活沉睡的用戶,推送即時消息、公告通知,激發(fā)更新等。如web資訊客戶端、web即時通訊工具、h5游戲等運(yùn)營產(chǎn)品。
- 事件同步:確保web端產(chǎn)生的任務(wù)即使在用戶關(guān)閉了web頁面也可以順利完成。如web郵件客戶端、web即時通訊工具等。
- 定時同步:周期性的觸發(fā)Service Worker腳本中的定時同步事件,可借助它提前刷新緩存內(nèi)容。
- 結(jié)合CacheStorage、 Push API 和 Notification API。
參考鏈接: