參考文檔javascript
這是和文章內容基本一致的一個demo,你們能夠測試下訪問後的service-worker的狀況,還有離線的訪問能力
demo地址
項目地址html
筆者使用service-worker在項目中的實踐前端
解決的問題vue
一個service worker在啓動前經歷了三步:java
用到的依賴webpack
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);
});
複製代碼
- 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
// 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.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) {
})
}
複製代碼
1 skipWaiting跳過等待階段
2 頁面提示
3 添加加載動畫,等待sw下載github
因爲瀏覽器的內部實現原理,當頁面切換或者自身刷新時,瀏覽器是等到新的頁面完成渲染以後再銷燬舊的頁面。這表示新舊兩個頁面中間有共同存在的交叉時間,所以簡單的切換頁面或者刷新是不能使得 service worker 進行更新的。web
既然service-worker的激活沒法經過刷新解決,那麼還有個skipWaiting
能夠用。vue-cli
可是最好不要直接skipWaiting
(跳過等待階段), 推薦的作法應該是在瀏覽器發現更新後,給用戶彈出提示。而後用戶點擊從新加載時,一方面刷新頁面 (location.reload()
),一方面讓新的 SW 接管頁面 (skipWaiting
)。
具體的流程:
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$/]
})
)
}
}
複製代碼
當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中溝通。