ServiceWorker 快取離線化

栗子喲發表於2019-02-26

概述

Service Worker 是HTML5 的一個新特性,主要用來做持久的離線快取

為什麼要使用ServiceWorker

在公司業務中,因為經常要處理效能優化方面的需求,使用傳統的效能優化手段,滿足了大多數業務場景。但是,如果目標使用者手機效能以及網路普遍較差的情況下(例如東南亞、印度等海外市場),瓶頸就在於DNS查詢,TCP的建立時間,採用常規的優化手段就顯得捉襟見肘。此時,我們專案組有嘗試採用離線快取方案,即將靜態資源快取到本地,通過攔截代理請求,讀取本地檔案,加快訪問速度。

ServiceWorker的目的

這個 API 的唯一目的就是解放主執行緒,Web Worker 是脫離在主執行緒之外的,將一些複雜的耗時的活交給它幹,完成後通過 postMessage 方法告訴主執行緒,而主執行緒通過 onMessage 方法得到 Web Worker 的結果反饋。

功能和特性

  • Service Worker擁有自己獨立的 worker 執行緒,獨立於當前網頁執行緒
  • 離線快取靜態資源
  • 攔截代理請求和響應
  • 可自定義響應內容
  • 可以通過postMessage向主執行緒傳送訊息
  • 無法直接操作DOM
  • 必須在HTTPS環境下工作或 localhost / 127.0.0.1 (自身安全機制)
  • 通過Promise非同步實現
  • Service Worker安裝(installing)完成後,就會一直存在,除非手動解除安裝(unregister)

生命週期

Service Worker 的生命週期完全獨立於網頁

  • 註冊 (register)
  • 安裝 (install)
  • 啟用 (activate)

在這裡插入圖片描述

通常使用 service worker 只需要以下幾個步驟:

  • 1. 檢測是否支援serivceworker

首先,檢測當前環境是否支援 service worker,可以使用 'serviceWorker' in navigator 進行檢測。

  • 2. 註冊(register)

如果支援,可以使用 navigator.serviceWorker.register('./sw.js'),在當前主執行緒中註冊 service worker。如果註冊成功,service worker 則在 ServiceWorkerGlobalScope環境中執行; 需要注意的是: 當前環境無法操作DOM,且和主執行緒之間相互獨立(即執行緒之間不會相互阻塞)。

  • 3. 安裝(install)

然後,後臺開始安裝service worker,一般在此過程中,開始快取一些靜態資原始檔。

  • 4. 啟用(active)

安裝成功之後,準備進行啟用 service worker,通常在啟用狀態下,主要進行快取清理,更新service worker等操作。

  • 5. 使用(activing)

啟用成功後,,service worker 就可以控制當前頁面了。需要注意的是,只有在service worker成功啟用後,才具有控制頁面的能力,一般在第一次訪問頁面時,service worker第一次建立成功,並沒有啟用,只有當重新整理頁面,再次訪問之後,才具有控制頁面的能力。

  • 6. 解除安裝(unregister)

快取顆粒化

  1. 快取 *.html 靜態資原始檔

快取收益和成本

  • 收益
    1. 省去建立tcp的連線時長,加快首屏載入速度
    2. 減少靜態資源伺服器的負載
  • 成本
    1. 資料不一致問題(更新策略)
    2. 程式碼維護成本(快取檔案)

專案演示

本專案在第一次安裝serverworker之後,可以在控制檯看到以下資訊:

在這裡插入圖片描述

檢視對應請求的靜態資源資訊:

可以看到,第一次安裝serviceworker時,讀取到的靜態資源並沒有快取。

在這裡插入圖片描述
重新整理瀏覽器之後,我們再看一下這些靜態資源:

可以看到,靜態資源以及被serviceworker快取起來了。

在這裡插入圖片描述

我們再來檢視當前serviceworker安裝情況:

可以看到,serviceworker已經處於啟用狀態。

在這裡插入圖片描述

最後,看一下離線功能的效果:

是不是很神奇,在離線狀態下我們的頁面也是能夠展示出資料的。 其中,離線的原理就是利用了serviceworker中,fetchcacheStorage這兩個介面,將請求進行攔截,將響應進行快取

在這裡插入圖片描述

原始碼實現

該原始碼實現了以下幾個功能:

  • 強制更新 通過self.skipWaiting(),如果檢測到新的service worker檔案,就會立即替換掉舊的。
  • 快取靜態資源 cache.addAll(cacheFiles) 通過這個介面實現
  • 攔截請求 通過監聽fetch事件,可以攔截當前頁所有請求self.addEventListener('fetch',function(e){})
  • 快取響應 將響應內容加入快取cache.put(evt.request, response)
// 快取靜態資原始檔列表
let cacheFiles = [
  './test.js',
  './index.html',
  './src/img/yy.png'
]
// serviceworker使用版本
let __version__ = 'cache-v2'

// 快取靜態資源
self.addEventListener('install', function (evt) {
  // 強制更新sw.js
  self.skipWaiting()
  evt.waitUntil(
    caches.open(version).then(function (cache) {
      return cache.addAll(cacheFiles)
    })
  )
})

// 快取更新
self.addEventListener('active', function (evt) {
  evt.waitUntil(
    caches.keys().then(function (cacheNames) {
      return Promise.all(
        cacheNames.map(function (cacheName) {
          if (cacheName !== version) {
            return caches.delete(cacheName)
          }
        })
      )
    })
  )
})

// 請求攔截
self.addEventListener('fetch', function (evt) {
  console.log('處理fetch事件:', evt.request.url)
  evt.respondWith(
    caches.match(evt.request).then(function (response) {
      if (response) {
        console.log('快取匹配到res:', response)
        return response
      }
      console.log('快取未匹配對應request,準備從network獲取', caches)
      return fetch(evt.request).then(function (response) {
        console.log('fetch獲取到的response:', response)
        caches.open(version).then(function (cache) {
          cache.put(evt.request, response)
          return response
        })
      })
    }).catch(function (err) {
      console.error('fetch 介面錯誤', err)
      throw err
    })
  )
})

複製程式碼

請參考: 原始碼地址

相關文章