参考文档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中沟通。