2023-04-23|閱讀時間 ‧ 約 16 分鐘

【Web微知識系列】 Service Workers

Service Workers

Service worker與Web workers相同,也都是一段運行在瀏覽器後台的腳本,提供一些不需要與頁面直接交互的功能(操作dom),主要處理網路相關的問題,可以攔截網路請求進行相對應的優化動作,我們把它想像成與伺服器之間的代理服務器可能會比較容易理解,當網路環境不佳時便回應快取資源,待網路順暢後同步最新資料,因此能提高更好的離線體驗,我們可能會想說為什麼有了Web workers、AppCache這類的API還需要Service worker呢?因為這些既有的功能主要都由我們自己去handle一些細緻的操作,過程非常繁瑣,因此發展出Service worker,背後幫我們解決掉許多事情(error handler、http request listener…)

功能

  • 資源快取
  • 離線應用
  • 多頁面傳遞(Post Message)
  • 推播通知
  • 後台自動更新

生命週期

  • 註冊Service worker
  • 註冊之後瀏覽器會在背景啟動Service Worker的安裝
  • 安裝過程中會將設定的靜態資源進行緩存,待所有靜態資源緩存成功後進入Activated狀態
  • 如果過程中任何一個資源不能成功緩存則代表安裝失敗,進入error,待重新安裝
  • 進入Activated狀態後進行監聽,當request或post message發生時則觸發相對應動作
  • Terminated狀態由瀏覽器決定是否銷毀,如果長期不使用或者記憶體不足時,則可能銷毀這個worker

Worker中常使用的事件

簡單實作Service Worker

註冊Service Worker

<html>
              <body>
                 <script>
                    if ('serviceWorker' in navigator) {
                       navigator.serviceWorker.register('./sw.js')
                          .then(reg => console.log(reg))
                          .catch(err => console.log(err));
                    }
                 </script>
              </body></html>

撰寫sw.js腳本檔

const cacheUrl = [
              './index.html',
              './script.js',
              './car.svg'
           ];
           const cacheName = 'precache' + (self.registration ? self.registration.scope: '');
           self.addEventListener('install', (event) => {
              event.waitUntil(
                 caches.open(cacheName)
                    .then((cache) => {
                       console.log('open cache');
                       return cache.addAll(cacheUrl);
                    });
              );
           });
           //clean cached files
           self.addEventListener('activate', (event) => {
              event.waitUntil(
                 caches.keys().then(function(cacheNames) {
                                   var promiseArr = cacheNames.map(function(item) {
                                           if (item !== cacheName) {
                                                   return caches.delete(item);
                                           }
                                   })
                                   return Promise.all(promiseArr);
                           })
              )
           });
           self.addEventListener('fetch', (event) => {
               //cache first
               event.respondWith(caches.match(event.request).then(res => {
                 if (res) {
                    console.log('match');
                    return res;
                 }
                 return fetch(event.request);
              }));
           });

cache的策略

Cache only

這種方式下任何請求都會從Cache storage取得
self.addEventListener('fetch', function(event) {
     event.respondWith(caches.match(event.request));
   });

Network only

這種方式下任何請求都不會跟Cache storage打交道,直接向後端發送
self.addEventListener('fetch', function(event) {
     event.respondWith(fetch(event.request));
     // or simply don't call event.respondWith, which
     // will result in default browser behaviour
   });

Cache first

頁面發送request時會先從Cache storage中存取若發現該請求尚未被緩存到則會改為Network請求
self.addEventListener('fetch', function(event) {
     event.respondWith(
       caches.match(event.request).then(function(response) {
         return response || fetch(event.request);
       })
     );
   });

Network first

頁面發送request時會先由Network向後端請求,若請求失敗則改由Cache storage請求
self.addEventListener('fetch', function(event) {
     event.respondWith(
       fetch(event.request).catch(function() {
         return caches.match(event.request);
       })
     );
   });

Cache & network race

頁面發送request時同時向Cache及Network請求,哪一個請求先回來則使用該response
// Promise.race is no good to us because it rejects if// a promise rejects before fulfilling. Let's make a proper// race function:function promiseAny(promises) {
     return new Promise((resolve, reject) => {
       // make sure promises are all promises
       promises = promises.map(p => Promise.resolve(p));
       // resolve this promise as soon as one resolves
       promises.forEach(p => p.then(resolve));
       // reject if all promises reject
       promises.reduce((a, b) => a.catch(() => b))
         .catch(() => reject(Error("All failed")));
     });
   };    self.addEventListener('fetch', function(event) {
     event.respondWith(
       promiseAny([
         caches.match(event.request),
         fetch(event.request)
       ])
     );
   });

Cache then network

頁面發送request時先由Cache storage取得顯示給user,然後再由Network取得後更新資料。
Code in Page
var networkDataReceived = false;    startSpinner();    // fetch fresh data
   var networkUpdate = fetch('/data.json').then(function(response) {
     return response.json();
   }).then(function(data) {
     networkDataReceived = true;
     updatePage();
   });    // fetch cached data
   caches.match('/data.json').then(function(response) {
     if (!response) throw Error("No data");
     return response.json();
   }).then(function(data) {
     // don't overwrite newer network data
     if (!networkDataReceived) {
       updatePage(data);
     }
   }).catch(function() {
     // we didn't get cached data, the network is our last hope:
     return networkUpdate;
   }).catch(showErrorMessage).then(stopSpinner);
Code in the ServiceWorker: 當網路請求資源成功時,更新緩存內容
self.addEventListener('fetch', function(event) {
     event.respondWith(
       caches.open('mysite-dynamic').then(function(cache) {
         return fetch(event.request).then(function(response) {
           cache.put(event.request, response.clone());
           return response;
         });
       })
     );
   });

相關概念

  • Service worker與Web worker一樣不能直接對dom結構進行操作,僅透過postMessage相互溝通
  • 基於安全考量Service worker只能運行在https的環境之上,畢竟修改網路請求的能力是相對危險的
  • 使用Service worker來進行cache優於一般worker的原因是Service worker能夠細緻的控制,例如發生error時直接於worker內部進行相對應處理,而一般worker僅能於產生worker的那個頁面進行,且API相對較少

實際操作

Service worker相關的library

sw-precache主要根據配置來產生service worker腳本檔的工具,而sw-toolbox則是sw-precache的加強工具,可以針對動態請求進行細部控制

各家瀏覽器支援狀況

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.