Service Worker學習與實踐(一)——離線快取

counterxing發表於2018-09-18

什麼是Service Worker

Service Worker本質上充當Web應用程式與瀏覽器之間的代理伺服器,也可以在網路可用時作為瀏覽器和網路間的代理。它們旨在(除其他之外)使得能夠建立有效的離線體驗,攔截網路請求並基於網路是否可用以及更新的資源是否駐留在伺服器上來採取適當的動作。他們還允許訪問推送通知和後臺同步API

  • Service Worker的本質是一個Web Worker,它獨立於JavaScript主執行緒,因此它不能直接訪問DOM,也不能直接訪問window物件,但是,Service Worker可以訪問navigator物件,也可以通過訊息傳遞的方式(postMessage)與JavaScript主執行緒進行通訊。
  • Service Worker是一個網路代理,它可以控制Web頁面的所有網路請求。
  • Service Worker具有自身的生命週期,使用好Service Worker的關鍵是靈活控制其生命週期。

Service Worker的作用

  • 用於瀏覽器快取
  • 實現離線Web APP
  • 訊息推送

Service Worker相容性

Service Worker是現代瀏覽器的一個高階特性,它依賴於fetch APICache StoragePromise等,其中,Cache提供了Request / Response物件對的儲存機制,Cache Storage儲存多個Cache

Service Worker學習與實踐(一)——離線快取

示例

在瞭解Service Worker的原理之前,先來看一段Service Worker的示例:

self.importScripts('./serviceworker-cache-polyfill.js');

var urlsToCache = [
  '/',
  '/index.js',
  '/style.css',
  '/favicon.ico',
];

var CACHE_NAME = 'counterxing';

self.addEventListener('install', function(event) {
  self.skipWaiting();
  event.waitUntil(
    caches.open(CACHE_NAME)
    .then(function(cache) {
      return cache.addAll(urlsToCache);
    })
  );
});

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
    .then(function(response) {
      if (response) {
        return response;
      }
      return fetch(event.request);
    })
  );
});


self.addEventListener('activate', function(event) {
  var cacheWhitelist = ['counterxing'];

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

複製程式碼

下面開始逐段逐段地分析,揭開Service Worker的神祕面紗:

polyfill

首先看第一行:self.importScripts('./serviceworker-cache-polyfill.js');,這裡引入了Cache API的一個polyfill,這個polyfill支援使得在較低版本的瀏覽器下也可以使用Cache Storage API。想要實現Service Worker的功能,一般都需要搭配Cache API代理網路請求到快取中。

Service Worker執行緒中,使用importScripts引入polyfill指令碼,目的是對低版本瀏覽器的相容。

Cache Resources List And Cache Name

之後,使用一個urlsToCache列表來宣告需要快取的靜態資源,再使用一個變數CACHE_NAME來確定當前快取的Cache Storage Name,這裡可以理解成Cache Storage是一個DB,而CACHE_NAME則是DB名:

var urlsToCache = [
  '/',
  '/index.js',
  '/style.css',
  '/favicon.ico',
];

var CACHE_NAME = 'counterxing';
複製程式碼

Lifecycle

Service Worker獨立於瀏覽器JavaScript主執行緒,有它自己獨立的生命週期。

如果需要在網站上安裝Service Worker,則需要在JavaScript主執行緒中使用以下程式碼引入Service Worker

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then(function(registration) {
    console.log('成功安裝', registration.scope);
  }).catch(function(err) {
    console.log(err);
  });
}
複製程式碼

此處,一定要注意sw.js檔案的路徑,在我的示例中,處於當前域根目錄下,這意味著,Service Worker和網站是同源的,可以為當前網站的所有請求做代理,如果Service Worker被註冊到/imaging/sw.js下,那隻能代理/imaging下的網路請求。

可以使用Chrome控制檯,檢視當前頁面的Service Worker情況:

Service Worker學習與實踐(一)——離線快取

安裝完成後,Service Worker會經歷以下生命週期:

  1. 下載(download
  2. 安裝(install
  3. 啟用(activate
  • 使用者首次訪問Service Worker控制的網站或頁面時,Service Worker會立刻被下載。之後至少每24小時它會被下載一次。它可能被更頻繁地下載,不過每24小時一定會被下載一次,以避免不良指令碼長時間生效。

  • 在下載完成後,開始安裝Service Worker,在安裝階段,通常需要快取一些我們預先宣告的靜態資源,在我們的示例中,通過urlsToCache預先宣告。

  • 在安裝完成後,會開始進行啟用,瀏覽器會嘗試下載Service Worker指令碼檔案,下載成功後,會與前一次已快取的Service Worker指令碼檔案做對比,如果與前一次的Service Worker指令碼檔案不同,證明Service Worker已經更新,會觸發activate事件。完成啟用。

如圖所示,為Service Worker大致的生命週期:

Service Worker學習與實踐(一)——離線快取

install

在安裝完成後,嘗試快取一些靜態資源:

self.addEventListener('install', function(event) {
  self.skipWaiting();
  event.waitUntil(
    caches.open(CACHE_NAME)
    .then(function(cache) {
      return cache.addAll(urlsToCache);
    })
  );
});
複製程式碼

首先,self.skipWaiting()執行,告知瀏覽器直接跳過等待階段,淘汰過期的sw.jsService Worker指令碼,直接開始嘗試啟用新的Service Worker

然後使用caches.open開啟一個Cache,開啟後,通過cache.addAll嘗試快取我們預先宣告的靜態檔案。

監聽fetch,代理網路請求

頁面的所有網路請求,都會通過Service Workerfetch事件觸發,Service Worker通過caches.match嘗試從Cache中查詢快取,快取如果命中,則直接返回快取中的response,否則,建立一個真實的網路請求。

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
    .then(function(response) {
      if (response) {
        return response;
      }
      return fetch(event.request);
    })
  );
});
複製程式碼

如果我們需要在請求過程中,再向Cache Storage中新增新的快取,可以通過cache.put方法新增,看以下例子:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
    .then(function(response) {
      // 快取命中
      if (response) {
        return response;
      }

      // 注意,這裡必須使用clone方法克隆這個請求
      // 原因是response是一個Stream,為了讓瀏覽器跟快取都使用這個response
      // 必須克隆這個response,一份到瀏覽器,一份到快取中快取。
      // 只能被消費一次,想要再次消費,必須clone一次
      var fetchRequest = event.request.clone();

      return fetch(fetchRequest).then(
        function(response) {
          // 必須是有效請求,必須是同源響應,第三方的請求,因為不可控,最好不要快取
          if (!response || response.status !== 200 || response.type !== 'basic') {
            return response;
          }

          // 消費過一次,又需要再克隆一次
          var responseToCache = response.clone();
          caches.open(CACHE_NAME)
            .then(function(cache) {
              cache.put(event.request, responseToCache);
            });
          return response;
        }
      );
    })
  );
});
複製程式碼

在專案中,一定要注意控制快取,介面請求一般是不推薦快取的。所以在我自己的專案中,並沒有在這裡做動態的快取方案。

activate

Service Worker總有需要更新的一天,隨著版本迭代,某一天,我們需要把新版本的功能釋出上線,此時需要淘汰掉舊的快取,舊的Service WorkerCache Storage如何淘汰呢?

self.addEventListener('activate', function(event) {
  var cacheWhitelist = ['counterxing'];

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});
複製程式碼
  1. 首先有一個白名單,白名單中的Cache是不被淘汰的。
  2. 之後通過caches.keys()拿到所有的Cache Storage,把不在白名單中的Cache淘汰。
  3. 淘汰使用caches.delete()方法。它接收cacheName作為引數,刪除該cacheName所有快取。

sw-precache-webpack-plugin

sw-precache-webpack-plugin是一個webpack plugin,可以通過配置的方式在webpack打包時生成我們想要的sw.jsService Worker指令碼。

一個最簡單的配置如下:

var path = require('path');
var SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');

const PUBLIC_PATH = 'https://www.my-project-name.com/';  // webpack needs the trailing slash for output.publicPath

module.exports = {

  entry: {
    main: path.resolve(__dirname, 'src/index'),
  },

  output: {
    path: path.resolve(__dirname, 'src/bundles/'),
    filename: '[name]-[hash].js',
    publicPath: PUBLIC_PATH,
  },

  plugins: [
    new SWPrecacheWebpackPlugin(
      {
        cacheId: 'my-project-name',
        dontCacheBustUrlsMatching: /\.\w{8}\./,
        filename: 'service-worker.js',
        minify: true,
        navigateFallback: PUBLIC_PATH + 'index.html',
        staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
      }
    ),
  ],
}
複製程式碼

在執行webpack打包後,會生成一個名為service-worker.js檔案,用於快取webpack打包後的靜態檔案。

一個最簡單的示例

Service Worker Cache VS Http Cache

對比起Http Header快取,Service Worker配合Cache Storage也有自己的優勢:

  1. 快取與更新並存:每次更新版本,藉助Service Worker可以立馬使用快取返回,但與此同時可以發起請求,校驗是否有新版本更新。
  2. 無侵入式:hash值實在是太難看了。
  3. 不易被沖掉:Http快取容易被沖掉,也容易過期,而Cache Storage則不容易被沖掉。也沒有過期時間的說法。
  4. 離線:藉助Service Worker可以實現離線訪問應用。

但是缺點是,由於Service Worker依賴於fetch API、依賴於PromiseCache Storage等,相容性不太好。

後話

本文只是簡單總結了Service Worker的基本使用和使用Service Worker做客戶端快取的簡單方式,然而,Service Worker的作用遠不止於此,例如:藉助Service Worker做離線應用、用於做網路應用的推送(可參考push-notifications)等。

甚至可以藉助Service Worker,對介面進行快取,在我所在的專案中,其實並不會做的這麼複雜。不過做介面快取的好處是支援離線訪問,對離線狀態下也能正常訪問我們的Web應用。

Cache StorageService Worker總是分不開的。Service Worker的最佳用法其實就是配合Cache Storage做離線快取。藉助於Service Worker,可以輕鬆實現對網路請求的控制,對於不同的網路請求,採取不同的策略。例如對於Cache的策略,其實也是存在多種情況。例如可以優先使用網路請求,在網路請求失敗時再使用快取、亦可以同時使用快取和網路請求,一方面檢查請求,一方面有檢查快取,然後看兩個誰快,就用誰。

相關文章