service-worker的踩坑實踐

參考文檔javascript

這是和文章內容基本一致的一個demo,你們能夠測試下訪問後的service-worker的狀況,還有離線的訪問能力
demo地址
項目地址html

簡介

筆者使用service-worker在項目中的實踐前端

解決的問題vue

  • sw文件自身的緩存問題
  • sw的更新的交互形式
  • sw更新失敗的兜底策略

生命週期

一個service worker在啓動前經歷了三步:java

  • 註冊(Registration)
  • 安裝(Installation)
  • 激活(Activation)
  • 更新(updated)

配置

用到的依賴webpack

service-worker註冊

let path = '/sw-test/sw.js'
let scope = '/sw-test/'
navigator.serviceWorker.register(path, { scope }).then(function(reg) {
    // registration worked
    console.log('Registration succeeded. Scope is ' + reg.scope);
  }).catch(function(error) {
    // registration failed
    console.log('Registration failed with ' + error);
  });
複製代碼
  1. path一般理解爲路徑,就是service-worker存放的位置,也能夠理解爲請求service-worker的url地址。能夠經過後端請求將service-worker從其餘目錄展現到你須要的Url地址下。
  2. scope 理解爲做用域,意義是在該做用域以及其下級目錄下發起的fetch請求都受當前的service-worker控制,在做用域之外地址下發起的請求,sw是沒法進行代理的。
  3. 在不填寫scope的狀況下,默認的scope就是path的父級目錄,上圖的path是/sw-test/sw.js,默認scope就是/sw-test/。
  4. 配置scope只能在默認做用域,也就是path的範圍內再自定義,至關於只能縮小做用域,不能擴大做用域的範圍。假如默認scope爲/a/b/,能夠經過傳入{scope: '/a/b/c/'}來指定本身的scope,自定義爲/d/e/就不行。

service-worker更新和緩存

  • service-worker.js也會受http的緩存策略控制
  • 若是新的worker未被成功下載,或者解析錯誤,或者在運行時出錯,或者在安裝階段不成功,新的worker會被丟棄,舊的會被保留
  • 一旦新的worker被成功安裝,更新的worker會進入等待狀態,新的worker會等待舊的worker下線纔會激活,新的worker和舊的會並存
  • self.skipWaiting()會強制跳過等待狀態,直接讓新的worker在安裝後進入激活狀態,這樣可能會有緩存問題
  • 瀏覽器會 diff 當前打開頁面的 service-worker.js,並判斷是否更新,若是 diff 結果爲更新,則從新安裝最新的 service-wroker.js,而且全量更新緩存
  • 任何靜態資源包括 service-worker.js 都會被 HTTP 緩存
  • 服務器對某個資源進行 no-cache 設置能夠避免 HTTP 緩存

針對上述的狀況,service-worker的更新就是必須解決的問題。
下面分兩種方法git

  1. 在服務器端配置service-worker的header,Cache-control: no-cache,使其不被緩存
  2. 前端進行service-worker的版本控制,每次註冊都添加版本號進行改寫
  • 下面是一種簡單粗暴的解決方法,缺點就是每次會從新請求service-worker
// sw-register.js
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js' + Date.now()).then(function (reg) {
    
  })
}
複製代碼
  • 利用 sw-register-webpack-plugin  插件,能夠自動生成版本號,github地址
// npm install sw-register-webpack-plugin --save-dev

const SwRegisterWebpackPlugin = require('sw-register-webpack-plugin')

webpack({
    // ...
    plugins: [
        new SwRegisterWebpackPlugin(/* options */);
    ]
    // ...
});
複製代碼
  • 折中的方法,用webpack.DefinePlugin插件,將版本號替
// webpack.config.jg
const webpack = require('webpack')

function getVersion () {
  var d = new Date()
  return '' + d.getFullYear() + d.getMonth() + 1 + d.getDate() + d.getHours() + d.getMinutes() + d.getSeconds()
}

webpack({
    // ...
  plugins: [
    new webpack.DefinePlugin({
      __SW_VERSION__: getVersion()
    })
  ]
  // ...
});
複製代碼
// sw-register.js
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js?version=' + __SW_VERSION__)
    .then(function (reg) {
      
    })
    .catch(function (e) {
      
    })
}
複製代碼

serivice-worker激活

1 skipWaiting跳過等待階段
2 頁面提示
3 添加加載動畫,等待sw下載github

因爲瀏覽器的內部實現原理,當頁面切換或者自身刷新時,瀏覽器是等到新的頁面完成渲染以後再銷燬舊的頁面。這表示新舊兩個頁面中間有共同存在的交叉時間,所以簡單的切換頁面或者刷新是不能使得 service worker 進行更新的。web

既然service-worker的激活沒法經過刷新解決,那麼還有個skipWaiting能夠用。vue-cli

可是最好不要直接skipWaiting(跳過等待階段), 推薦的作法應該是在瀏覽器發現更新後,給用戶彈出提示。而後用戶點擊從新加載時,一方面刷新頁面 (location.reload()),一方面讓新的 SW 接管頁面 (skipWaiting)。

具體的流程:

  • 在註冊service-worker時就監聽sw的更新情況
  • 若是有更新,而且安裝完成後,就發送自定義事件sw.update
  • 自定義事件被觸發,顯示更新按鈕
  • 用戶點擊更新按鈕觸發更新

function emitUpdate () {
  var event = document.createEvent('Event')
  event.initEvent('sw.update', true, true)
  window.dispatchEvent(event)
}

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/zhangyi/sw')
    .then(function (reg) {
      if (reg.waiting) {
        emitUpdate()
        return
      }
      reg.onupdatefound = function () {
        var installingWorker = reg.installing
        installingWorker.onstatechange = function () {
          switch (installingWorker.state) {
            case 'installed':
              if (navigator.serviceWorker.controller) {
                // 自定義的更新事件
                emitUpdate()
              }
              break
          }
        }
      }
    })
    .catch(function (e) {
      console.error('Error during service worker registration:', e)
    })
}
複製代碼
let refreshing = false

export default {
  name: 'SWUpdatePopup',
  data () {
    return {
      showSwUpdate: false
    }
  },
  mounted () {
    this.addListener()
  },
  methods: {
    addListener () {
      window.addEventListener('sw.update', this.handleUpdate)
      this.$once('hook:beforeDestroy', function () {
        window.removeEventListener('sw.update', this.handleUpdate)
      })
    },
    handleUpdate () {
      this.showSwUpdate = true
    },
    handleSkipWaiting () {
      navigator.serviceWorker.getRegistration()
        .then(reg => this.skipWaiting(reg))
        .then(() => {
          window.location.reload(true)
        })
    },
    handleSWChange () {
      if (refreshing) {
        return
      }
      refreshing = true
      window.location.reload()
    },
    skipWaiting (registration) {
      const worker = registration.waiting
      if (!worker) {
        return Promise.resolve()
      }
      return new Promise((resolve, reject) => {
        const channel = new MessageChannel()
        channel.port1.onmessage = (event) => {
          if (event.data.error) {
            reject(event.data.error)
          } else {
            resolve(event.data)
          }
        }
        worker.postMessage({ type: 'skip-waiting' }, [channel.port2])
      })
    },
    handleRefresh () {
      window.location.reload(true)
    }
  }
}
複製代碼

配置主要基於 vue-cli 的 pwa 插件和 workbox-webpack-plugin

workbox-webpack-plugin主要提供兩種模式:

**GenerateSW **模式根據配置生成sw文件,適用場景:

  • 簡單的運行時配置需求
  • 不涉及Web Push

**InjectManifest **模式經過既有sw文件再加工,適用場景;

  • 涉及Web Push
  • 更復雜的自定義配置

這裏使用的GenerateSW模式

// vue.config.js
const { InjectManifest } = require('workbox-webpack-plugin')

module.exports = {
	configureWebpack: config => {
    config.plugins.push(
      new InjectManifest({
        swSrc: './src/service-worker.js',
        importsDirectory: 'js',
        importWorkboxFrom: 'disabled', // 不使用谷歌workerbox的cdn
        exclude: [/\.map$/, /^manifest.*\.js$/, /\.html$/]
      })
    )
  }
}
複製代碼

serivice-worker卸載

當service-worker新版本的更新出現問題,那麼就要考慮如何保證用戶看到的版本是最新的
我選擇的策略是卸載當前的sw,用線上的文件,而且再也不安裝當前錯誤版本的。

// sw-register.js
const version = Number(__SW_VERSION__)
const project = __PROJECT_NAME__

function emitUpdate () {
  var event = document.createEvent('Event')
  event.initEvent('sw.update', true, true)
  window.dispatchEvent(event)
}

function emitUnregister () {
  var event = document.createEvent('Event')
  event.initEvent('sw.unregister', true, true)
  window.dispatchEvent(event)
}

function unregister () {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.getRegistration()
      .then(function (registration) {
        if (registration) {
          registration.unregister().then(function () {
            emitUnregister()
          })
        }
      })
  }
}

const failSwName = 'fail:' + project + '-sw-version'

function getFailVersion () {
  const version = window.localStorage.getItem(failSwName)
  if (version) {
    return Number(version)
  }
  return ''
}

function setFailVersion () {
  window.localStorage.setItem(failSwName, version)
}

if (getFailVersion() !== version && 'serviceWorker' in navigator) {
  // 若是是新的版本,那就嘗試註冊安裝
  navigator.serviceWorker.register(`/${project}/service-worker.js?version=${version}`) // eslint-disable-line
    .then(function (reg) {
      if (reg.waiting) {
        emitUpdate()
        return
      }
      reg.onupdatefound = function () {
        var installingWorker = reg.installing
        installingWorker.onstatechange = function () {
          if (installingWorker.state === 'installed') {
            if (navigator.serviceWorker.controller) {
              emitUpdate()
            }
          }
        }
      }
    })
    .catch(function (e) {
      console.error('Error during service worker registration:', e)
      // 註冊失敗後,在session中寫入失敗的版本,並直接卸載
      setFailVersion()
      unregister()
    })
} else {
  // 直接卸載
  unregister()
}
複製代碼
// service-worker.js
//...
self.addEventListener('message', event => {
  const replyPort = event.ports[0]
  const message = event.data
  if (replyPort && message && message.type === 'skip-waiting') {
    event.waitUntil(
      self.skipWaiting()
        .then(() => replyPort.postMessage({ error: null }))
        .catch(error => replyPort.postMessage({ error }))
    )
  }
})
//...
複製代碼

更新的彈窗

<template>
  <div>
    <div class="sw-update-dialog" v-if="showSwUpdate" >
      <button @click="handleSkipWaiting">
        更新
      </button>
    </div>
    <div class="sw-update-dialog" v-if="showSwUnregister" >
      <button @click="handleRefresh">
        更新
      </button>
    </div>
  </div>
</template>

<script> let refreshing = false export default { name: 'SWUpdatePopup', data () { return { showSwUpdate: false, showSwUnregister: false } }, mounted () { this.addListener() }, methods: { addListener () { window.addEventListener('sw.update', this.handleUpdate) window.addEventListener('sw.unregister', this.handleUnregister) this.$once('hook:beforeDestroy', function () { window.removeEventListener('sw.update', this.handleUpdate) window.removeEventListener('sw.unregister', this.handleUnregister) }) }, handleUpdate () { this.showSwUpdate = true }, handleSkipWaiting () { navigator.serviceWorker.getRegistration() .then(reg => this.skipWaiting(reg)) .then(() => { window.location.reload(true) }) }, handleSWChange () { if (refreshing) { return } refreshing = true window.location.reload() }, skipWaiting (registration) { const worker = registration.waiting if (!worker) { return Promise.resolve() } // 這裏是參考vue-press的寫法 // 利用MessageChannel返回一個promise return new Promise((resolve, reject) => { const channel = new MessageChannel() channel.port1.onmessage = (event) => { if (event.data.error) { reject(event.data.error) } else { resolve(event.data) } } worker.postMessage({ type: 'skip-waiting' }, [channel.port2]) }) }, handleUnregister () { this.showSwUnregister = true }, handleRefresh () { window.location.reload(true) } } } </script>
複製代碼

項目示例

這是和文章內容基本一致的一個demo,你們能夠測試下訪問後的service-worker的狀況,還有離線的訪問能力。
demo地址
項目地址

原文的語雀地址

因爲筆者水平有限,文中不免有所錯誤,但願讀者朋友不吝賜教,歡迎斧正。 有更好的解決方案可在評論中說明或直接在項目issue中溝通。

相關文章
相關標籤/搜索