“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第 1 篇文章,点击查看活动详情”
本文只涉及 ServiceWorker 和 Manifest 的讲解,其他部分请参考官方文档。
应用场景
首先我们需要明白 PWA 主要作为离线应用来缓存资源,所以对于数据更新不频繁的场景是非常适用的,可以加速用户访问,提高用户体验,比如文档网站:
- 个人博客网站
- 官网
其他网站也可以用作缓存一些静态资源,加速网站访问等等。
ServiceWorker
ServiceWorker 概述
ServiceWorker 是 PWA 的基石,它本身相当于一个浏览器和服务器之间的代理服务器,但是本质上它也是一个Worker,所以它不能访问 DOM 等等。
不同的状态的 ServiceWorker
- waiting
等待被安装 - installing
安装中 - active
激活状态
ServiceWorker 的状态
- installing
安装中 - installed
已安装 - activating
激活中 - activated
已激活 - redundant
已销毁
ServiceWorker 的事件
- install
安装回调 - activate
激活回调 - fetch
获取回调 - sync
后台同步回调 - push
服务器推送回调
ServiceWorker 使用
在外部,我们进行ServiceWorker 的注册,卸载以及版本更新逻辑。__SW_ENABLE__ 是通过 DefinePlugin 插件定义的一个变量,控制ServiceWorker 的开启和关闭
// registerSW.js
const __sw_enable__ = window.__SW_ENABLE__
function emitUpdate() {
const event = document.createEvent('Event')
event.initEvent('sw:update', true, true)
document.dispatchEvent(event)
}
window.addEventListener('load', () => {
if ('serviceWorker' in navigator) {
if (!__sw_enable__) {
navigator.serviceWorker.register(`/sw.js`, {
scope: '/'
}).then((res) => {
if (res.waiting) {
emitUpdate()
return
}
res.onupdatefound = function() {
const installingWorker = res.installing
installingWorker.onstatechange = function() {
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
emitUpdate()
}
break
}
}
}
}).catch((error) => {
console.error(`sw register error, it caused by error: ${error}`)
})
} else {
navigator.serviceWorker.getRegistrations().then(function(regs) {
for (const reg of regs) {
reg.unregister()
}
})
}
let reload = true
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (navigator.serviceWorker.controller) {
if (reload) {
window.location.reload()
reload = false
}
}
})
}
})
// main.js
document.addEventListener('sw:update', () => {
if (window.confirm(`是否更新 ${window.__VERSION__}?`)) {
try {
navigator.serviceWorker.getRegistration().then(reg => {
reg?.waiting?.postMessage?.({ type: 'SKIP_WAITING' })
})
} catch (e) {
window.location.reload()
}
}
})
编写 ServiceWorker 内部脚本我们有两种方式:
- 原生
- Workbox
如果原生自己写的话,我们需要写很多代码来控制不同资源的更新策略,相较比较麻烦;所以一般推荐使用 Workbox 来控制。
原生使用
const version = 'v1'
const cacheTargetList = []
// install
this.addEventListener('install', (event) => {
event.waitUntil(
caches.open(version).then((cache) => {
return cache.addAll(
cacheTargetList
)
})
)
})
// updated
// 缓存策略
// 不在同一个域的任何资源一定不能使用 CacheFirst | CacheOnly
// 1、StaleWhileRevalidate:如果缓存存在响应,那么取缓存,同时发起请求更新缓存,后续每次请求相当于最近一次更新数据
// /\.(?:js|css)/ CDN
// 2、CacheFirst:如果缓存存在响应,那么取缓存;如果缓存没有响应,则发起网络请求,完成响应,并将结果缓存
// /\.(?:js|css)/ hash 同域名
// /\.(?:png|jpg|jpeg|webp)/ 设置一定失效时间
// 3、NetworkFirst:对于频繁更新的请求,网络优先策略是理想的解决方案,默认情况下,它尝试从网络请求响应,成功后将结果缓存,失败则取缓存
// /\.html$/
// 4、NetworkOnly:仅使用网络来响应
// 5、CacheOnly:仅使用缓存来响应,如果你有自己的预先缓存的步骤,可能很有用
this.addEventListener('fetch', (event) => {
if (event.request.url.startsWith('http')) {
event.respondWith(
caches.match(event.request).then((resp) => {
return resp || fetch(event.request).then((response) => {
return caches.open(version).then((cache) => {
// 由于 Cache API 不支持 POST 方法,没有不能缓存 POST 请求
if (event.request.method === 'GET') {
cache.put(event.request, response.clone())
}
return response
})
})
}).catch(() => {
return caches.match('error')
})
)
}
})
// remove old cache
this.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keyList) => {
return Promise.all(keyList.map(key => {
if (key !== version) {
return caches.delete(key)
}
}))
})
)
self.clients.claim()
})
// receive message
self.addEventListener('message', (event) => {
const port = event.ports[0]
if (port) {
port.postMessage(event.data)
}
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting()
}
})
// post message
// self.clients.matchAll().then((clients) => {
// clients.forEach((client) => {
// client.postMessage({
// action: 'post message'
// })
// })
// })
Workbox 使用
由于项目采用 Webpack 打包,所以需要安装 workbox-webpack-plugin 依赖。 如果你不采用 Webpack 打包,可以使用 workbox-build 自定义构建。Workbox 有两种方式配置 sw:GenerateSW 和 InjectManifest。
GenerateSW 属于配置型,不需要编写 sw 脚本,相较比较便捷,InjectManifest 需要自行编写 sw 脚本,相较比较灵活。
GenerateSW
const { GenerateSW } = require('workbox-webpack-plugin')
plugins:[
new GenerateSW({
cacheId: `sw-${version}`,
swDest: 'sw.js',
inlineWorkboxRuntime: false,
skipWaiting: false,
clientsClaim: false,
navigateFallback: '/index.html',
cleanupOutdatedCaches: true,
// 'CacheFirst' | 'CacheOnly' | 'NetworkFirst' | 'NetworkOnly' | 'StaleWhileRevalidate'
runtimeCaching: [
{
urlPattern: /.*\.html/,
handler: 'NetworkFirst',
options: {
cacheName: `html-cache`,
cacheableResponse: {
statuses: [0, 200]
}
}
},
{
urlPattern: /\.js[\?]?/,
handler: 'CacheFirst',
options: {
cacheName: `js-cache`,
cacheableResponse: {
statuses: [0, 200]
}
}
},
{
urlPattern: /\.css[\?]?/,
handler: 'CacheFirst',
options: {
cacheName: `css-cache`,
cacheableResponse: {
statuses: [0, 200]
}
}
},
{
urlPattern: new RegExp(`^${process.env.VUE_APP_BASE_API}`),
handler: 'StaleWhileRevalidate',
options: {
cacheName: `api-get-cache`,
cacheableResponse: {
statuses: [0, 200]
},
fetchOptions: {
mode: 'cors',
method: 'GET',
credentials: 'omit'
}
}
},
{
urlPattern: new RegExp(`^${process.env.VUE_APP_BASE_API}`),
handler: 'NetworkOnly',
method: 'POST',
options: {
cacheName: `api-post-cache`,
cacheableResponse: {
statuses: [0, 200]
}
}
},
{
urlPattern: /\.(?:png|gif|jpg|jpeg|svg)[\?]?/,
handler: 'CacheFirst',
options: {
cacheName: `image-cache`,
cacheableResponse: {
statuses: [0, 200]
},
expiration: {
maxEntries: 60, // 最大的缓存数,超过之后则走 LRU 策略清除最老最少使用缓存
maxAgeSeconds: 30 * 24 * 60 * 60 // 最长缓存时间为 30 天
}
}
}
]
}),
]
InjectManifest
const { InjectManifest } = require('workbox-webpack-plugin')
plugins:[
new InjectManifest({
swSrc: './src/sw.js',
swDest: './dist/sw.js',
compileSrc: true
})
]
Mainfest 配置
配置 PWA 的 图标,这样可以被安装成一个应用。
{
"name": "XX项目",
"short_name": "XX",
"icons": [
{
"src": "/static/img/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/img/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "/index.html",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#4DBA87"
}
<!--manifest配置-->
<link rel="manifest" href="/static/manifest.json" />
<!--主题颜色-->
<meta name="theme-color" content="#4DBA87" />
<!--是否启用 WebApp 全屏模式,删除苹果默认的工具栏和菜单栏-->
<meta name="apple-mobile-web-app-capable" content="yes" />
<!--设置苹果工具栏颜色-->
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<!--添加到主屏后的标题(iOS 6 新增)-->
<meta
name="apple-mobile-web-app-title"
content="<%= webpackConfig.name %>"
/>
<!--在iPhone,iPad,iTouch的safari浏览器上可以使用添加到主屏按钮将网站添加到主屏幕上,方便用户以后访问-->
<link
rel="apple-touch-icon"
href="/static/img/icons/apple-touch-icon-152x152.png"
/>
<!--Safari 10开始支持固定书签页的SVG favicons-->
<link
rel="mask-icon"
href="/static/img/icons/safari-pinned-tab.svg"
color="#4DBA87"
/>
<!--Windows 8 磁贴图标-->
<meta
name="msapplication-TileImage"
content="/static/img/icons/msapplication-icon-144x144.png"
/>
<!--Windows 8 磁贴颜色-->
<meta name="msapplication-TileColor" content="#000000" />
总结
本文主要探讨了 PWA 的两个核心部分:ServiceWorker 和 Mainfest。讲解了ServiceWorker 两种实现方式,并介绍通过 Workbox 打造一个简略可靠的 ServiceWorker 应用,最后介绍了 Mainfest 配置部分,实现应用的注册。