Service Worker ——这应该是一个挺全面的整理
我在之前有空的時候粗略學習了一下 Service Worker ;最近有空,所以花時間再去學習了下。我在這里整理了下,希望對大家有幫助。
同時,如果文章中有錯誤或者描述不當的地方,歡迎大家能夠幫我指正,謝謝。
PS:文章很長,含有大量示例代碼。大家可以慢慢看:)
介紹
作為一個比較新的技術,大家可以把 Service Worker 理解為一個介于客戶端和服務器之間的一個代理服務器。在 Service Worker 中我們可以做很多事情,比如攔截客戶端的請求、向客戶端發送消息、向服務器發起請求等等,其中最重要的作用之一就是離線資源緩存。
首先,作為一個新技術,我們需要關注的是它在不同瀏覽器的兼容性,下面是來自于 caniuse.com 的一張圖:
從這張圖中,我們可以看到 IE 和 Opera Mini 全面撲街,而主流瀏覽器中 Edge 17以下不支持,Safair 和 IOS Safair 剛剛開始支持,而火狐和 Chrome 支持良好。所以大家可以放心的使用,不過最好還是做一下判斷。
對于 Service Worker ,了解過 Web Worker 的同學可能會比較好理解。它和 Web Worker 相比,有相同的點,也有不同的地方。
相同:
不同的地方在于,Service Worker 是一個瀏覽器中的進程而不是瀏覽器內核下的線程,因此它在被注冊安裝之后,能夠被在多個頁面中使用,也不會因為頁面的關閉而被銷毀。因此,Service Worker 很適合被用與多個頁面需要使用的復雜數據的計算——購買一次,全家“收益”。
另外有一點需要注意的是,出于對安全問題的考慮,Service Worker 只能被使用在 https 或者本地的 localhost 環境下。
注冊安裝
下面就讓我們來使用 Service Worker 。
如果當前使用的瀏覽器支持 Service Worker ,則在 window.navigator 下會存在 serviceWorker 對象,我們可以使用這個對象的 register 方法來注冊一個 Service Worker。
這里需要注意的一點是,Service Worker 在使用的過程中存在大量的 Promise ,對于 Promise 不是很了解的同學可以先去看一下相關文檔。 Service Worker 的注冊方法返回的也是一個 Promise 。
// index.js if ('serviceWorker' in window.navigator) {navigator.serviceWorker.register('./sw.js', { scope: './' }).then(function (reg) {console.log('success', reg);}).catch(function (err) {console.log('fail', err);}); } 復制代碼在這段代碼中,我們先使用 if 判斷下瀏覽器是否支持 Service Worker ,避免由于瀏覽器不兼容導致的 bug 。
register 方法接受兩個參數,第一個是 service worker 文件的路徑,請注意:這個文件路徑是相對于 Origin ,而不是當前 JS 文件的目錄的;第二個參數是 Serivce Worker 的配置項,可選填,其中比較重要的是 scope 屬性。按照文檔上描述,它是 Service Worker 控制的內容的子目錄,這個屬性所表示的路徑不能在 service worker 文件的路徑之上,默認是 Serivce Worker 文件所在的目錄;關于這個屬性,文檔中講的不是很清楚,我也有很多疑問,會在接下來的內容中提出。希望有知道的同學能幫我解惑。
register 方法返回一個 Promise 。如果注冊失敗,可以通過 catch 來捕獲錯誤信息;如果注冊成功,可以使用 then 來獲取一個 ServiceWorkerRegistration 的實例,有興趣的同學可以去翻閱文檔。
注冊完 Service Worker 之后,瀏覽器會為我們自動安裝它,因此我們就可以在 service worker 文件中監聽它的 install 事件了。
// sw.js this.addEventListener('install', function (event) {console.log('Service Worker install'); }); 復制代碼同樣的,Service Worker 在安裝完成后會被激活,所以我們也可監聽 activate 事件。
// sw.js this.addEventListener('activate', function (event) {console.log('Service Worker activate'); }); 復制代碼這時,我們可以在 Chorme 的開發者工具中看到我們注冊的 Service Worker。
在默認情況下,Service Worker 必定會每24小時被下載一次,如果下載的文件是最新文件,那么它就會被重新注冊和安裝,但不會被激活,當不再有頁面使用舊的 Service Worker 的時候,它就會被激活。
這對于我們開發來說是很不方便的,因此在這里我勾選了一個名為 Update on reload 的單選框,選中它之后,我們每次刷新頁面都能夠使用最新的 service worker 文件。
在同一個 Origin 下,我們可以注冊多個 Service Worker。但是請注意,這些 Service Worker 所使用的 scope 必須是不相同的。
if ('serviceWorker' in window.navigator) {navigator.serviceWorker.register('./sw/sw.js', { scope: './sw' }).then(function (reg) {console.log('success', reg);})navigator.serviceWorker.register('./sw2/sw2.js', { scope: './sw2' }).then(function (reg) {console.log('success', reg);}) } 復制代碼信息通訊
之前說過,使用 postMessage 方法可以進行 Service Worker 和頁面之間的通訊,下面就讓我們來試一下。
從頁面到 Service Worker
首先是從頁面發送信息到 Serivce Worker 。
// index.js if ('serviceWorker' in window.navigator) {navigator.serviceWorker.register('./sw.js', { scope: './' }).then(function (reg) {console.log('success', reg);navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage("this message is from page");}); } 復制代碼為了保證 Service Worker 能夠接收到信息,我們在它被注冊完成之后再發送信息,和普通的 window.postMessage 的使用方法不同,為了向 Service Worker 發送信息,我們要在 ServiceWorker 實例上調用 postMessage 方法,這里我們使用到的是 navigator.serviceWorker.controller 。
// sw.js this.addEventListener('message', function (event) {console.log(event.data); // this message is from page }); 復制代碼在 service worker 文件中我們可以直接在 this 上綁定 message 事件,這樣就能夠接收到頁面發來的信息了。
對于不同 scope 的多個 Service Worker ,我么也可以給指定的 Service Worker 發送信息。
// index.js if ('serviceWorker' in window.navigator) {navigator.serviceWorker.register('./sw.js', { scope: './sw' }).then(function (reg) {console.log('success', reg);reg.active.postMessage("this message is from page, to sw");})navigator.serviceWorker.register('./sw2.js', { scope: './sw2' }).then(function (reg) {console.log('success', reg);reg.active.postMessage("this message is from page, to sw 2");}) }// sw.js this.addEventListener('message', function (event) {console.log(event.data); // this message is from page, to sw });// sw2.js this.addEventListener('message', function (event) {console.log(event.data); // this message is from page, to sw 2 }); 復制代碼請注意,當我們在注冊 Service Worker 時,如果使用的 scope 不是 Origin ,那么navigator.serviceWorker.controller 會為 null。這種情況下,我們可以使用 reg.active 這個對象下的 postMessage 方法,reg.active 就是被注冊后激活 Serivce Worker 實例。但是,由于 Service Worker 的激活是異步的,因此第一次注冊 Service Worker 的時候,Service Worker 不會被立刻激活, reg.active 為 null,系統會報錯。我采用的方式是返回一個 Promise ,在 Promise 內部進行輪詢,如果 Service Worker 已經被激活,則 resolve 。
// index.js navigator.serviceWorker.register('./sw/sw.js').then(function (reg) {return new Promise((resolve, reject) => {const interval = setInterval(function () {if (reg.active) {clearInterval(interval);resolve(reg.active);}}, 100)})}).then(sw => {sw.postMessage("this message is from page, to sw");})navigator.serviceWorker.register('./sw2/sw2.js').then(function (reg) {return new Promise((resolve, reject) => {const interval = setInterval(function () {if (reg.active) {clearInterval(interval);resolve(reg.active);}}, 100)})}).then(sw => {sw.postMessage("this message is from page, to sw2");}) 復制代碼從 Service Worker 到頁面
下一步就是從 Service Worker 發送信息到頁面了,不同于頁面向 Service Worker 發送信息,我們需要在 WindowClient 實例上調用 postMessage 方法才能達到目的。而在頁面的JS文件中,監聽 navigator.serviceWorker 的 message 事件即可收到信息。
而最簡單的方法就是從頁面發送過來的消息中獲取 WindowClient 實例,使用的是 event.source ,不過這種方法只能向消息的來源頁面發送信息。
// sw.js this.addEventListener('message', function (event) {event.source.postMessage('this message is from sw.js, to page'); });// index.js navigator.serviceWorker.addEventListener('message', function (e) {console.log(e.data); // this message is from sw.js, to page }); 復制代碼如果不想受到這個限制,則可以在 serivce worker 文件中使用 this.clients 來獲取其他的頁面,并發送消息。
// sw.js this.clients.matchAll().then(client => {client[0].postMessage('this message is from sw.js, to page'); }) 復制代碼關于這個方法,我有一些沒有解決的疑問的。在我的試驗中,注冊 Service Worker 時候設置的 scope 的值會對獲取到的 client 產生影響。
如果在注冊 Service Worker 的時候,把 scope 設置為非 origin 目錄,那么在 service worker 文件中,我無法獲取到 Origin 路徑對應頁面的 client。
// index.js navigator.serviceWorker.register('./sw.js', { scope: './sw/' });// sw.js this.clients.matchAll().then(client => {console.log(client); // [] }) 復制代碼我查找了一些資料,但是沒有找到關于 scope 和 client 之間的聯系的明確說明文檔。我的猜測是,Service Worker 是否只能夠獲取到 scope 路徑下的子頁面的 client ,但是我使用 react router 試驗發現似乎又不是,希望有知道的同學能夠幫忙解答,謝謝!
使用 Message Channel 來通信
另外一種比較好用的通信方式是使用 Message Channel 。
// index.js navigator.serviceWorker.register('./sw.js', { scope: './' }).then(function (reg) {const messageChannel = new MessageChannel();messageChannel.port1.onmessage = e => {console.log(e.data); // this message is from sw.js, to page}reg.active.postMessage("this message is from page, to sw", [messageChannel.por2]);})// sw.js this.addEventListener('message', function (event) {console.log(event.data); // this message is from page, to swevent.ports[0].postMessage('this message is from sw.js, to page'); }); 復制代碼使用這種方式能夠使得通道兩端之間可以相互通信,而不是只能向消息源發送信息。舉個例子,兩個 Service Worker 之間的通信。
// index.jsconst messageChannel = new MessageChannel();navigator.serviceWorker.register('./sw/sw.js').then(function (reg) {console.log(reg)return new Promise((resolve, reject) => {const interval = setInterval(function () {if (reg.active) {clearInterval(interval);resolve(reg.active);}}, 100)})}).then(sw => {sw.postMessage("this message is from page, to sw", [messageChannel.port1]);})navigator.serviceWorker.register('./sw2/sw2.js').then(function (reg) {return new Promise((resolve, reject) => {const interval = setInterval(function () {if (reg.active) {clearInterval(interval);resolve(reg.active);}}, 100)})}).then(sw => {sw.postMessage("this message is from page, to sw2", [messageChannel.port2]);})// sw.js this.addEventListener('message', function (event) {console.log(event.data); // this message is from page, to swevent.ports[0].onmessage = e => {console.log('sw:', e.data); // sw: this message is from sw2.js}event.ports[0].postMessage('this message is from sw.js'); });// sw2.js this.addEventListener('message', function (event) {console.log(event.data); // this message is from page, to sw2event.ports[0].onmessage = e => {console.log('sw2:', e.data); // sw2: this message is from sw.js}event.ports[0].postMessage('this message is from sw2.js'); }); 復制代碼首先讓頁面給兩個 Service Worker 發送信息,并且把信息通道的端口發送過去;然后在兩個 service worker 文件中使用端口分別設置接受信息的回調函數,之后它們就能夠互相發送信息并接收到來自通道對面的消息了。
靜態資源緩存
下面要講的就是重頭戲,也是 Service Worker 能夠實現的最主要的功能——靜態資源緩存。
正常情況下,用戶打開網頁,瀏覽器會自動下載網頁所需要的 JS 文件、圖片等靜態資源。我們可以通過 Chrome 開發工具的 Network 選項來查看。
但是如果用戶在沒有聯網的情況下打開網頁,瀏覽器就無法下載這些展示頁面效果所必須的資源,頁面也就無法正常的展示出來。
我們可以使用 Service Worker 配合 CacheStroage 來實現對靜態資源的緩存。
緩存指定靜態資源
// sw.js this.addEventListener('install', function (event) {console.log('install');event.waitUntil(caches.open('sw_demo').then(function (cache) {return cache.addAll(['/style.css','/panda.jpg','./main.js'])})); }); 復制代碼當 Service Worker 在被安裝的時候,我們能夠對制定路徑的資源進行緩存。CacheStroage 在瀏覽器中的接口名是 caches ,我們使用 caches.open 方法新建或打開一個已存在的緩存;cache.addAll 方法的作用是請求指定鏈接的資源并把它們存儲到之前打開的緩存中。由于資源的下載、緩存是異步行為,所以我們要使用事件對象提供的 event.waitUntil 方法,它能夠保證資源被緩存完成前 Service Worker 不會被安裝完成,避免發生錯誤。
從 Chrome 開發工具中的 Application 的 Cache Strogae 中可以看到我們緩存的資源。
這種方法只能緩存指定的資源,無疑是不實用的,所以我們需要針對用戶發起的每一個請求進行緩存。
動態緩存靜態資源
this.addEventListener('fetch', function (event) {console.log(event.request.url);event.respondWith(caches.match(event.request).then(res => {return res ||fetch(event.request).then(responese => {const responeseClone = responese.clone();caches.open('sw_demo').then(cache => {cache.put(event.request, responeseClone);})return responese;}).catch(err => {console.log(err);});})) }); 復制代碼我們需要監聽 fetch 事件,每當用戶向服務器發起請求的時候這個事件就會被觸發。有一點需要注意,頁面的路徑不能大于 Service Worker 的 scope,不然 fetch 事件是無法被觸發的。
在回掉函數中我們使用事件對象提供的 respondWith 方法,它可以劫持用戶發出的 http 請求,并把一個 Promise 作為響應結果返回給用戶。然后我們使用用戶的請求對 Cache Stroage 進行匹配,如果匹配成功,則返回存儲在緩存中的資源;如果匹配失敗,則向服務器請求資源返回給用戶,并使用 cache.put 方法把這些新的資源存儲在緩存中。因為請求和響應流只能被讀取一次,所以我們要使用 clone 方法復制一份存儲到緩存中,而原版則會被返回給用戶
在這里有幾點需要注意:
對于用戶發起的 POST 請求,我們也可以在攔截后,通過判斷請求中攜帶的 body 的內容來進行有選擇的返回。
if(event.request.method === 'POST') {event.respondWith(new Promise(resolve => {event.request.json().then(body => {console.log(body); // 用戶請求攜帶的內容})resolve(new Response({ a: 2 })); // 返回的響應}))} } 復制代碼我們可以在 fetch 事件的回掉函數中對請求的 method 、url 等各項屬性進行判斷,選擇不同的操作。
對于靜態資源的緩存,Cache Stroage 是個不錯的選擇;而對于數據,我們可以使用 IndexedDB 來存儲,同樣是攔截用戶請求后,使用緩存在 IndexDB 中的數據作為響應返回,詳細的內容我就不在這里講了,有興趣的同學可以自己去了解下。
更新 Cache Stroage
前面提到過,當有新的 service worker 文件存在的時候,他會被注冊和安裝,等待使用舊版本的頁面全部被關閉后,才會被激活。這時候,我們就需要清理下我們的 Cache Stroage 了,刪除舊版本的 Cache Stroage 。
this.addEventListener('install', function (event) {console.log('install');event.waitUntil(caches.open('sw_demo_v2').then(function (cache) { // 更換 Cache Stroagereturn cache.addAll(['/style.css','/panda.jpg','./main.js'])})) });const cacheNames = ['sw_demo_v2']; // Cahce Stroage 白名單this.addEventListener('activate', function (event) {event.waitUntil(caches.keys().then(keys => {return Promise.all[keys.map(key => {if (!cacheNames.includes(key)) {console.log(key);return caches.delete(key); // 刪除不在白名單中的 Cache Stroage}})]})) }); 復制代碼首先在安裝 Service Worker 的時候,要換一個 Cache Stroage 來存儲,然后設置一個白名單,當 Service Worker 被激活的時候,將不在白名單中的 Cache Stroage 刪除,釋放存儲空間。同樣使用 event.waitUntil ,在 Service Worker 被激活前執行完刪除操作。
小結
Service Worker 作為一個新的技術,在靜態資源緩存和處理多頁面所需的復雜數據等方面都有很不錯的應用前景。作為實現 PWA 不可或缺的一部分,我相信,不管是他的瀏覽器兼容性、功能的多樣性以及文檔的完整性,都會變的越來越好。
同時,肯定還有很多我沒有學到、講到,或者是我忽略了 Service Worker 的內容存在,所以我希望可以和大家一起學習,特別是 scope 這個屬性,希望有知道的同學幫我答疑解惑,謝謝。
轉載于:https://juejin.im/post/5b06a7b3f265da0dd8567513
總結
以上是生活随笔為你收集整理的Service Worker ——这应该是一个挺全面的整理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Sass-变量计算
- 下一篇: 浅谈 instanceof 和 type