Service Worker —這應該是一個挺全面的整理

前段日子有空,粗略學習了一下 Service Worker ;最近從新複習了下,而且把內容整理後寫在這裏,但願對你們有所幫助。css

同時,若是文章中有錯誤或者描述不當的地方,歡迎你們可以幫我指正,謝謝。react

PS:文章很長,含有大量示例代碼。你們能夠慢慢看:)json


介紹

做爲一個比較新的技術,你們能夠把 Service Worker 理解爲一個介於客戶端和服務器之間的一個代理服務器。在 Service Worker 中咱們能夠作不少事情,好比攔截客戶端的請求、向客戶端發送消息、向服務器發起請求等等,其中最重要的做用之一就是離線資源緩存。瀏覽器

首先,做爲一個新技術,咱們須要關注的是它在不一樣瀏覽器的兼容性,下面是來自於 caniuse.com 的一張圖: 緩存

各大瀏覽器對 Service Worker 的 兼容性
從這張圖中,咱們能夠看到 IE 和 Opera Mini 全面撲街,而主流瀏覽器中 Edge 17如下不支持,Safair 和 IOS Safair 剛剛開始支持,而火狐和 Chrome 支持良好。因此你們能夠放心的使用,不過最好仍是作一下判斷。

對於 Service Worker ,瞭解過 Web Worker 的同窗可能會比較好理解。它和 Web Worker 相比,有相同的點,也有不一樣的地方。安全

相同:bash

  1. Service Worker 工做在 worker context 中,是沒有訪問 DOM 的權限的,因此咱們沒法在 Service Worker 中獲取 DOM 節點,也沒法在其中操做 DOM 元素;
  2. 咱們能夠經過 postMessage 接口把數據傳遞給其餘 JS 文件;
  3. Service Worker 中運行的代碼不會被阻塞,也不會阻塞其餘頁面的 JS 文件中的代碼;

不一樣的地方在於,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。

Chrome 開發者工具
在默認狀況下,Service Worker 一定會 每24小時被下載一次,若是下載的文件是最新文件,那麼它就會被從新註冊和安裝,但不會被激活,當再也不有頁面使用舊的 Service Worker 的時候,它就會被激活。
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 sw
  event.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 sw
  event.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 sw2
  event.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 中能夠看到咱們緩存的資源。

Cache Stroage
這種方法只能緩存指定的資源,無疑是不實用的,因此咱們須要針對用戶發起的每個請求進行緩存。

動態緩存靜態資源

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 方法複製一份存儲到緩存中,而原版則會被返回給用戶

在這裏有幾點須要注意:

  1. 當用戶第一次訪問頁面的時候,資源的請求是早於 Service Worker 的安裝的,因此靜態資源是沒法緩存的;只有當 Service Worker 安裝完畢,用戶第二次訪問頁面的時候,這些資源纔會被緩存起來;
  2. Cache Stroage 只能緩存靜態資源,因此它只能緩存用戶的 GET 請求;
  3. Cache Stroage 中的緩存不會過時,可是瀏覽器對它的大小是有限制的,因此須要咱們按期進行清理;

對於用戶發起的 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 Stroage
      return 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 這個屬性,但願有知道的同窗幫我答疑解惑,謝謝。


感謝閱讀,未經容許,請勿轉載:)

相關文章
相關標籤/搜索