Web離線應用解決方案——ServiceWorker

什麼是ServiceWorker

  在介紹ServiceWorker以前,咱們先來談談PWA。PWA (Progressive Web Apps) 是一種 Web App 新模型,並非具體指某一種前沿的技術或者某一個單一的知識點,,這是一個漸進式的 Web App,是經過一系列新的 Web 特性,配合優秀的 UI 交互設計,逐步的加強 Web App 的用戶體驗。css

  • Https環境部署
  • 響應式設計,一次部署,能夠在移動設備和 PC 設備上運行 在不一樣瀏覽器下可正常訪問。
  • 瀏覽器離線和弱網環境可極速訪問
  • 能夠把 App Icon 入口添加到桌面。
  • 點擊 Icon 入口有相似 Native App 的動畫效果。
  • 靈活的熱更新

  在PWA要求的各類能力上,關於離線環境的支持咱們就須要仰賴ServiceWorker。Service workers 本質上充當Web應用程序與瀏覽器之間的代理服務器,也能夠在網絡可用時做爲瀏覽器和網絡間的代理。它們旨在(除其餘以外)使得可以建立有效的離線體驗,攔截網絡請求並基於網絡是否可用以及更新的資源是否駐留在服務器上來採起適當的動做。因爲PWA是谷歌提出,那麼對ServiceWorker,一樣也提出一些能力要求:html

  • 後臺消息傳遞
  • 網絡代理,轉發請求,僞造響應
  • 離線緩存
  • 消息推送

  在目前階段,ServiceWorker的主要能力集中在網絡代理和離線緩存上。具體的實現上,能夠理解爲ServiceWorker是一個能在網頁關閉時仍然運行的WebWorker。算法

ServiceWorker的生命週期

  剛纔講到ServiceWorker擁有離線能力的WebWorker,既然這麼強的能力,那就須要好好管理起來。因此咱們要明白ServiceWorker的生命週期,也就是它從建立到銷燬的過程。在全部介紹ServiceWorker生命週期的文章中最多見的就是下面這張圖。json

  整個過程當中一個ServiceWorker會經歷:安裝、激活、等待、銷燬的階段。但實際上這張圖我感受並無清晰的解釋ServiceWorker的聲明週期,因此我製做了下面這張圖。瀏覽器

  這張圖把ServiceWorker的聲明週期分爲了兩部分,主線程中的狀態和ServiceWorker子線程中的狀態。子線程中的代碼處在一個單獨的模塊中,當咱們須要使用ServiceWorker時,按照以下的方式來加載:緩存

if (navigator.serviceWorker != null) {
      // 使用瀏覽器特定方法註冊一個新的service worker
      navigator.serviceWorker.register('sw.js')
      .then(function(registration) {
        window.registration = registration;
        console.log('Registered events at scope: ', registration.scope);
      });
    }

  這個時候ServiceWorker處於Parsed解析階段。當解析完成後ServiceWorker處於Installing安裝階段,主線程的registration的installing屬性表明正在安裝的ServiceWorker實例,同時子線程中會觸發install事件,並在install事件中指定緩存資源服務器

var cacheStorageKey = 'minimal-pwa-3';

var cacheList = [
  '/',
  "index.html",
  "main.css",
  "e.png",
  "pwa-fonts.png"
]

// 當瀏覽器解析完sw文件時,serviceworker內部觸發install事件
self.addEventListener('install', function(e) {
  console.log('Cache event!')
  // 打開一個緩存空間,將相關須要緩存的資源添加到緩存裏面
  e.waitUntil(
    caches.open(cacheStorageKey).then(function(cache) {
      console.log('Adding to Cache:', cacheList)
      return cache.addAll(cacheList)
    })
  )
})

  這裏使用了Cache API來將資源緩存起來,同時使用e.waitUntil接手一個Promise來等待資源緩存成功,等到這個Promise狀態成功後,ServiceWorker進入installed狀態,意味着安裝完畢。這時候主線程中返回的registration.waiting屬性表明進入installed狀態的ServiceWorker。微信

/* In main.js */
navigator.serviceWorker.register('./sw.js').then(function(registration) {  
    if (registration.waiting) {
        // Service Worker is Waiting
    }
})

  然而這個時候並不意味着這個ServiceWorker會立馬進入下一個階段,除非以前沒有新的ServiceWorker實例,若是以前已有ServiceWorker,這個版本只是對ServiceWorker進行了更新,那麼須要知足以下任意一個條件,新的ServiceWorker纔會進入下一個階段:網絡

  • 在新的ServiceWorker線程代碼裏,使用了self.skipWaiting() 
  • 或者當用戶導航到別的網頁,所以釋放了舊的ServiceWorker時候
  • 或者指定的時間過去後,釋放了以前的ServiceWorker

  這個時候ServiceWorker的生命週期進入Activating階段,ServiceWorker子線程接收到activate事件:框架

// 若是當前瀏覽器沒有激活的service worker或者已經激活的worker被解僱,
// 新的service worker進入active事件
self.addEventListener('activate', function(e) {
  console.log('Activate event');
  console.log('Promise all', Promise, Promise.all);
  // active事件中一般作一些過時資源釋放的工做
  var cacheDeletePromises = caches.keys().then(cacheNames => {
    console.log('cacheNames', cacheNames, cacheNames.map);
    return Promise.all(cacheNames.map(name => {
      if (name !== cacheStorageKey) { // 若是資源的key與當前須要緩存的key不一樣則釋放資源
        console.log('caches.delete', caches.delete);
        var deletePromise = caches.delete(name);
        console.log('cache delete result: ', deletePromise);
        return deletePromise;
      } else {
        return Promise.resolve();
      }
    }));
  });

  console.log('cacheDeletePromises: ', cacheDeletePromises);
  e.waitUntil(
    Promise.all([cacheDeletePromises]
    )
  )
})

  這個時候一般作一些緩存清理工做,當e.waitUntil接收的Promise進入成功狀態後,ServiceWorker的生命週期則進入activated狀態。這個時候主線程中的registration的active屬性表明進入activated狀態的ServiceWorker實例

/* In main.js */
navigator.serviceWorker.register('./sw.js').then(function(registration) {  
    if (registration.active) {
        // Service Worker is Active
    }
})

  到此一個ServiceWorker正式進入激活狀態,能夠攔截網絡請求了。若是主線程有fetch方式請求資源,那麼就能夠在ServiceWorker代碼中觸發fetch事件:

fetch('./data.json')

  這時在子線程就會觸發fetch事件:

self.addEventListener('fetch', function(e) {
  console.log('Fetch event ' + cacheStorageKey + ' :', e.request.url);
  e.respondWith( // 首先判斷緩存當中是否已有相同資源
    caches.match(e.request).then(function(response) {
      if (response != null) { // 若是緩存中已有資源則直接使用
        // 不然使用fetch API請求新的資源
        console.log('Using cache for:', e.request.url)
        return response
      }
      console.log('Fallback to fetch:', e.request.url)
      return fetch(e.request.url);
    })
  )
})

  那麼若是在install或者active事件中失敗,ServiceWorker則會直接進入Redundant狀態,瀏覽器會釋放資源銷燬ServiceWorker。

  如今若是沒有網絡進入離線狀態,或者資源命中緩存那麼就會優先讀取緩存的資源:

緩存資源更新

  那麼若是咱們在新版本中更新了ServiceWorker子線程代碼,當訪問網站頁面時瀏覽器獲取了新的文件,逐字節比對 /sw.js 文件發現不一樣時它會認爲有更新啓動 更新算法open_in_new,因而會安裝新的文件並觸發 install 事件。可是此時已經處於激活狀態的舊的 Service Worker 還在運行,新的 Service Worker 完成安裝後會進入 waiting 狀態。直到全部已打開的頁面都關閉,舊的 Service Worker 自動中止,新的 Service Worker 纔會在接下來從新打開的頁面裏生效。若是想要當即更新須要在新的代碼中作一些處理。首先在install事件中調用self.skipWaiting()方法,而後在active事件中調用self.clients.claim()方法通知各個客戶端。

// 當瀏覽器解析完sw文件時,serviceworker內部觸發install事件
self.addEventListener('install', function(e) {
  debugger;
  console.log('Cache event!')
  // 打開一個緩存空間,將相關須要緩存的資源添加到緩存裏面
  e.waitUntil(
    caches.open(cacheStorageKey).then(function(cache) {
      console.log('Adding to Cache:', cacheList)
      return cache.addAll(cacheList)
    }).then(function() {
      console.log('install event open cache ' + cacheStorageKey);
      console.log('Skip waiting!')
      return self.skipWaiting();
    })
  )
})

// 若是當前瀏覽器沒有激活的service worker或者已經激活的worker被解僱,
// 新的service worker進入active事件
self.addEventListener('activate', function(e) {
  debugger;
  console.log('Activate event');
  console.log('Promise all', Promise, Promise.all);
  // active事件中一般作一些過時資源釋放的工做
  var cacheDeletePromises = caches.keys().then(cacheNames => {
    console.log('cacheNames', cacheNames, cacheNames.map);
    return Promise.all(cacheNames.map(name => {
      if (name !== cacheStorageKey) { // 若是資源的key與當前須要緩存的key不一樣則釋放資源
        console.log('caches.delete', caches.delete);
        var deletePromise = caches.delete(name);
        console.log('cache delete result: ', deletePromise);
        return deletePromise;
      } else {
        return Promise.resolve();
      }
    }));
  });

  console.log('cacheDeletePromises: ', cacheDeletePromises);
  e.waitUntil(
    Promise.all([cacheDeletePromises]
    ).then(() => {
      console.log('activate event ' + cacheStorageKey);
      console.log('Clients claims.')
      return self.clients.claim();
    })
  )
})

  注意這裏說的是瀏覽器獲取了新版本的ServiceWorker代碼,若是瀏覽器自己對sw.js進行緩存的話,也不會獲得最新代碼,因此對sw文件最好配置成cache-control: no-cache或者添加md5。

  實際過程當中像咱們剛纔把index.html也放到了緩存中,而在咱們的fetch事件中,若是緩存命中那麼直接從緩存中取,這就會致使即便咱們的index頁面有更新,瀏覽器獲取到的永遠也是都是以前的ServiceWorker緩存的index頁面,因此有些ServiceWorker框架支持咱們配置資源更新策略,好比咱們能夠對主頁這種作策略,首先使用網絡請求獲取資源,若是獲取到資源就使用新資源,同時更新緩存,若是沒有獲取到則使用緩存中的資源。代碼以下:

self.addEventListener('fetch', function(e) {
  console.log('Fetch event ' + cacheStorageKey + ' :', e.request.url);
  e.respondWith( // 該策略先從網絡中獲取資源,若是獲取失敗則再從緩存中讀取資源
    fetch(e.request.url)
    .then(function (httpRes) {

      // 請求失敗了,直接返回失敗的結果
      if (!httpRes || httpRes.status !== 200) {
          // return httpRes;
          return caches.match(e.request)
      }

      // 請求成功的話,將請求緩存起來。
      var responseClone = httpRes.clone();
      caches.open(cacheStorageKey).then(function (cache) {
          return cache.delete(e.request)
          .then(function() {
              cache.put(e.request, responseClone);
          });
      });

      return httpRes;
    })
    .catch(function(err) { // 無網絡狀況下從緩存中讀取
      console.error(err);
      return caches.match(e.request);
    })
  )
})

 

注意事項

  ServiceWorker是一項新能力,目前IOS平臺對他的支持性並不友好,可是在安卓側已經沒有大問題。而微信平臺對它的支持也不錯。

  依賴項:

  • 依賴Cache API
  • 依賴Fetch API Promise API
  • Https環境

   錯誤排查:

  • install或active事件失敗
  • 非Https環境
  • sw.js安裝路徑問題
  • scope設置

  同時這裏我也爲你們錄製視頻,能夠更清晰的看到這些細節。

相關文章
相關標籤/搜索