文章來自 個人博客 。html
生命週期是 Service Worker 中比較複雜的一部分,若是你不知道它什麼時間將要作什麼,以及它帶來的好處,那麼你可能會有一種感受:就是它一直在和你較勁。若是理解它的工做機制,你就能夠給用戶提供完美的,無感知的更新體驗。 這篇文章是 Chrome 團隊最近總結的一片文章,配合例子講述生命週期,讓咱們更容易理解,也解決了我以前開發中遇到的一些困惑,因此決定翻譯出來。此處 閱讀原文 。git
本文中介紹利用生命週期能夠實現的功能大概有以下幾點:github
實現緩存優先(offline-first)web
在不打斷現有 SW 的狀況下,準備好一個新的 SW瀏覽器
讓註冊 SW 的頁面同一時間只歸屬同一個 SW 控制緩存
確保你的網站只有一個版本在運行app
最後一點尤爲重要,通常狀況下(沒有 SW 的狀況),用戶瀏覽你的網站時可能先打開一個 tab,過了一下子又打開了一個 tab,結果就是在同一時間,你的頁面運行了兩個版本,大部分時候,這樣是沒問題的,可是若是你使用了緩存,那麼兩個 tab 就要面臨如何管理緩存的問題,若是處理很差,它可能會形成異常,嚴重的形成數據丟失。svg
用戶很是不喜歡數據丟失,這會讓他們很是憂桑。工具
install
事件是 SW 觸發的第一個事件,而且僅觸發一次。fetch
installEvent.waitUntil()
接收一個 Promise 參數,用它來表示 SW 安裝的成功與否。
SW 在安裝成功並激活以前,不會響應 fetch
或push
等事件。
默認狀況下,頁面的請求(fetch)不會經過 SW,除非它自己是經過 SW 獲取的,也就是說,在安裝 SW 以後,須要刷新頁面纔能有效果。
clients.claim()
能夠改變這種默認行爲。
舉個例子:
<!DOCTYPE html> 3秒後將出現一張圖片: <script> navigator.serviceWorker.register('/sw.js') .then(reg => console.log('SW registered!', reg)) .catch(err => console.log('Boo!', err)); setTimeout(() => { const img = new Image(); img.src = '/dog.svg'; document.body.appendChild(img); }, 3000); </script>
這裏註冊了一個 SW,而且在 3 秒後在頁面上添加一個圖片。
這是 SW 的代碼:
self.addEventListener('install', event => { console.log('V1 installing…'); // 這裏緩存一個 cat.svg event.waitUntil( caches.open('static-v1').then(cache => cache.add('/cat.svg')) ); }); self.addEventListener('activate', event => { console.log('V1 now ready to handle fetches!'); }); self.addEventListener('fetch', event => { const url = new URL(event.request.url); //若是是同域而且請求的是 dog.svg 的話,那麼返回 cat.svg if (url.origin == location.origin && url.pathname == '/dog.svg') { event.respondWith(caches.match('/cat.svg')); } });
這裏 SW 緩存了 cat.svg,並當請求 dog.svg 的時候返回它。然而,當你運行 這個例子 的時候,你會發現第一次出現一隻狗,刷新後,貓纔會出現。
SW 的默認做用域爲基於當前文件 URL 的 ./
。意思就是若是你在//example.com/foo/bar.js
裏註冊了一個 SW,那麼它默認的做用域爲 //example.com/foo/
。
咱們把頁面,workers,shared workers 叫作clients
。SW 只能對做用域內的clients
有效。一旦一個client
被「控制」了,那麼它的請求都會通過這個 SW。咱們能夠經過查看navigator.serviceWorker.controller
是否爲 null 來查看一個client
是否被 SW 控制。
當你調用.register()
的時候,第一個 SW 被下載下來,這過程當中若是下載,解析或者在初始化中有錯誤的話,那麼 register
的Promise 會返回 reject,而後 SW 會被銷燬。
Chrome 開發者工具會展現出來錯誤,在 Application 中的 Service Workers Tab:
SW 首先會觸發install
,每一個 SW 只會被觸發一次,當你修改你的 SW 後,瀏覽器會認爲這是一個新的 SW,從而會再觸發這個新 SW 的install
事件,在後面會詳細說到。
install
是在 SW 控制 clients
以前處理緩存很好的時機。在 event.waitUntil()
傳入的 Promise 會讓瀏覽器知道 SW 何時安裝成功以及是否成功。
當 Promise reject 的時候,表明着安裝失敗,瀏覽器將這個 SW 廢棄掉,不會控制任何 clients。
安裝成功後並激活(activate)成功後,SW 就能夠處理「功能性的事件「了,好比push
,sync
。但這並不表明調用.register()
的頁面會當即生效。
第一次你請求 這個demo 的時候,雖然在 SW 被激活後好久才請求了dog.svg
(由於這裏等待了三秒),但 SW 也並無處理這個請求,結果你看見了一隻狗。當你第二次請求的時候,也就是刷新頁面,這時請求被處理了,當前頁面和圖片都通過了 SW 的 fetch
事件,因此你看見了一隻貓。
你能夠在activate
事件中經過調用clients.claim()
來讓沒被控制的 clients 受控。
好比 這個demo ,可能第一次你就會看見一隻貓,這裏我說「可能」,是由於這時時間敏感的,僅當 SW 激活而且clients.claim()
被調用成功在圖片請求以前的時候才能夠。
因此,可想而知,當你用 SW 加載與正常請求不一樣資源的時候(好比上面的例子),那用clients.claim()
可能會遇到一些問題,這時有些資源可能不會經過你的SW。
我見過不少人在代碼中把
clients.claim()
當作了必選項,但我本身不多這樣作,由於僅僅是第一次加載不會經過 SW,並且頁面仍是都會正常運行的。
簡單來講:
觸發更新的幾種狀況:
第一次導航到做用域範圍內頁面的時候
當在24小時內沒有進行更新檢測而且觸發功能性時間如push
或sync
的時候
SW 的 URL 發生變化並調用.register()
時
當 SW 代碼發生變化,SW 會作更新(還將包括引入的腳本)
更新後的 SW 會和原始的 SW 共同存在,並運行它的install
若是新的 SW 不是成功狀態,好比 404,解析失敗,執行中報錯或者在 install 過程當中被 reject,它將會被廢棄,以前版本的 SW 仍是激活狀態不變。
一旦新 SW 安裝成功,它會進入wait
狀態直到原始 SW 不控制任何 clients。
self.skipWaiting()
能夠阻止等待,讓新 SW 安裝成功後當即激活。
<div style="max-width:600px;">
<iframe id="iframe-2" onload="iframeLoaded('iframe-2')" style="width:100%;border:none;" src="https://www.zhengqingxin.com/static/demo/5-lifecycle/iframe2.html"> </iframe>
</div>
下面咱們來舉個 SW 更新的例子,這是一個將貓變成馬的故事:
const expectedCaches = ['static-v2']; self.addEventListener('install', event => { console.log('V2 installing…'); // 將 horse.svg 緩存在新的緩存 static-v2 中 event.waitUntil( caches.open('static-v2').then(cache => cache.add('/horse.svg')) ); }); self.addEventListener('activate', event => { // 刪除額外的緩存,static-v1 將被刪掉 event.waitUntil( caches.keys().then(keys => Promise.all( keys.map(key => { if (!expectedCaches.includes(key)) { return caches.delete(key); } }) )).then(() => { console.log('V2 now ready to handle fetches!'); }) ); }); self.addEventListener('fetch', event => { const url = new URL(event.request.url); //若是是同域而且請求的是 dog.svg 的話,那麼返回 horse.svg if (url.origin == location.origin && url.pathname == '/dog.svg') { event.respondWith(caches.match('/horse.svg')); } });
若是你打開 這個 demo ,你會看到一隻喵,緣由是這樣的...
注意這裏咱們將緩存從 static-v1 換到了 static-v2,這表明了我用了一個新的緩存空間覆蓋了以前 SW 正在使用的緩存。
這裏新建了一塊緩存的作法相似於原生 app 中將每塊資源打包到一塊指定的執行空間的作法,有時候結合實際狀況,你也能夠不這麼作。
一旦新 SW 安裝成功,它會進入wait
狀態直到原始 SW 不控制任何 clients。這個狀態是waiting
,這也是瀏覽器確保在同一時間只有一個版本的 SW 運行的機制。
若是你再次打開 這個 demo ,你仍是會看到一隻喵,由於新的 SW 仍是沒有被激活,在開發者工具裏你依然看到它是 waiting 狀態。
儘管這個例子中你僅打開了一個 tab,但刷新頁面並無用,這是因爲瀏覽器自己的機制,當你刷新的時候,當前頁面不會離開,直到收到了一個響應頭,並且即便這樣,若是響應中包含Content-Disposition
的話,當前頁面仍是不會離開。因爲這個時間上的重疊,在刷新的時候當前的 SW 老是控制了一個 client。
爲了讓 SW 更新,你須要把全部用原始 SW 的頁面 tab 關閉或者跳轉走,這時你再訪問 這個 demo ,你就會看到了一匹野馬。
這種機制相似於 Chrome 自己的更新機制,Chrome 在後臺更新,只有當你重啓瀏覽器的時候纔會生效,在這期間你不會被打擾,能夠繼續使用當前版本。然而,這樣可能會使咱們開發者比較痛苦,好在開發者工具幫咱們解決了這個事情,後面會說到。
Activate 在舊的 SW 離開時會被觸發,這時新的 SW 能夠控制 clients。這時候你能夠作一些在老 SW 運行時不能作的事情,好比清理緩存。
在上面的例子中,以前保留的緩存,在activate
時間執行的時候被清理掉。
這裏最好不要更新之前的版本,而是直接分配新的緩存空間。
若是你在event.waitUntil()
中傳入了一個 Promise,SW 將會緩存住功能性事件(fetch
,push
,sync
等等),直到 Promise 返回 resolve 的時候再觸發,也就是說,當你的fetch
事件被觸發的時候,SW 已經被徹底激活了。
cache storage API 和 localStorage,IndexedDB 同樣是「同域」的。若是你在一個父域下運行多個網站,好比
yourname.github.io/myapp
,這就要當心你不要把別的網站的緩存刪掉了。避免這個問題,你能夠將 cache 的 key 設的具備惟一性,好比 myapp-static-v1 而且約束不要碰不以 myapp- 開頭的緩存。
waiting 意在讓你的網站同一時間只有一個 SW 在運行,但若是你不想要這樣的話,你能夠經過調用self.skipWaiting()
來讓新 SW 當即激活。
這麼作會讓你的新 SW 踢掉舊的,而後當它變爲 waiting 狀態時當即激活,注意這裏不會跳過 installing,只會跳過 waiting。
在 waiting 以前或者以後調用skipWaiting()
均可以,通常狀況咱們在 install
事件中調用:
self.addEventListener('install', event => { self.skipWaiting(); event.waitUntil( // caching etc ); });
這個例子中,你可能直接能夠看到一隻奶牛,和clients.claim()
同樣,這是一場賽跑,僅當你的新 SW 安裝,激活等早於你請求圖片時,奶牛纔會出現。
skipWaiting()
意味着新 SW 控制了以前用舊 SW 獲取的頁面,也就是說你的頁面有一部分資源是經過舊 SW 獲取,剩下一部分是經過新 SW 獲取的,若是這樣作會給你帶來麻煩,那就不要用skipWaiting()
,這點咱們應該根據具體狀況評估。
像我以前說的,當頁面刷新或者執行功能性事件時,瀏覽器會自動檢查更新,其實咱們也能夠手動的來觸發更新:
navigator.serviceWorker.register('/sw.js').then(reg => { // sometime later… reg.update(); });
若是你但願你的用戶訪問頁面很長時間並且不用刷新,那麼你能夠每一個一段時間調用一次update()
。
若是你看過個人文章緩存最佳實踐,你可能會考慮給每一個 SW 不一樣的 URL。千萬不要這麼作!在 SW 中這麼作是「最差實踐」,要在原地址上修改 SW。
舉個例子來講明爲何:
1.index.html
註冊了sw-v1.js
做爲SW。
2.sw-v1.js
對index.html
作了緩存,也就是緩存優先(offline-first)。
3.你更新了index.html
從新註冊了在新地址的 SW sw-v2.js
.
若是你像上面那麼作,用戶永遠也拿不到sw-v2.js
,由於index.html
在sw-v1.js
緩存中,這樣的話,若是你想更新爲sw-v2.js
,還須要更改原來的sw-v1.js
。
在上面的 demo 裏,我給每一個 SW 用了不一樣的 URL,這只是爲了作演示,不要在生產環境中這麼作。
SW 的生命週期是爲了用戶構建的,但這樣不免讓咱們開發帶來一些煩惱,幸好與一些工具來幫助咱們。
這是我最喜歡的:
這樣把生命週期變得對開發友好了,每次跳轉將會:
1.從新獲取 SW
2.儘管字節一致,也會從新安裝,也就是說install
事件被執行而且更新緩存。
3.跳過 waiting,激活新的 SW。
4.導航到這個頁面。
這就是說你每次操做都會更新而不用刷新頁面或者關閉 tab。
若是你有個 SW 在等待狀態,你能夠點擊 skipWaiting 讓它當即變爲激活狀態。
若是你強制刷新頁面,那麼會繞過 SW,變成不受控,這個功能已被定爲規範,因此在其餘支持 SW 的瀏覽器中也適用。
Service Worker 是 可擴展web 的一部分。想法初衷是,做爲瀏覽器開發者,有時候咱們可能不如 web 開發者更瞭解 web,因此,咱們其實不該該提供僅僅能夠解決具體問題的 API,而是應該給 web 開發者更多的權限從而更好的解決問題。
因此,咱們儘量的開放更多,SW 整個生命週期都是可查看的:
navigator.serviceWorker.register('/sw.js').then(reg => { reg.installing; // 安裝中的 SW,或者是undefined reg.waiting; // 等待中的 SW,或者是undefined reg.active; // 激活中的 SW,或者是undefined reg.addEventListener('updatefound', () => { // 正在安裝的新的 SW const newWorker = reg.installing; newWorker.state; // "installing" - 安裝事件被觸發,但還沒完成 // "installed" - 安裝完成 // "activating" - 激活事件被觸發,但還沒完成 // "activated" - 激活成功 // "redundant" - 廢棄,多是由於安裝失敗,或者是被一個新版本覆蓋 newWorker.addEventListener('statechange', () => { // newWorker 狀態發生變化 }); }); }); navigator.serviceWorker.addEventListener('controllerchange', () => { // 當 SW controlling 變化時被觸發,好比新的 SW skippedWaiting 成爲一個新的被激活的 SW });