PWA开发记录-vue
PWA是渐进式web应用程序,其实就是类似于小程序,h5APP,UniApp那样子的程序,通过本地系统的一些接口,结合web技术从而建立的一种web本地化的应用。
安装过程
因为我是通过vue来作为web设计的框架,所以就通过vue来安装
vue create xxx[项目名字]
这是vue通用的项目创建方式,虽然与pwa没什么关系,但是这其实就是写一个前端页面,最后安装pwa的模块,在本地生成一个manifest.json的配置文件,浏览器会自己识别这种web application,然后添加到主屏幕就好,当然不仅仅是manifest.json,这个文件主要是配置web app的应用名称,图标,前景色,背景色之类的,而安装pwa的模块还会为你配置service worker这样的本地缓存功能等待。
下面安装@ionic/vue
npm install @ionic/vue
记得要cd进入项目目录,这个@ionic/vue模块是ionic这个框架结合vue设计的模块,可以契合vue进行开发,里面像vue有一样,有自己写好的好看的组件,以及ionic-vue-router,这个也是结合vue-router封装的,所以项目中必须包含vue-router
我还是用ionic framework的框架结合vue来搭建我的pwa应用吧!这个ionic框架既可以在移动端的web跨平台还是很友好的,可以通过插件编译出ios,android平台的应用,以及设计适合以pwa形式的移动端app。
npm i -g @ionic/cli
安装ionic的脚手架
ionic start xxx项目名称
创建ionic的项目,当然它也可以跟一些参数如“--vue”,表示以vue的形式创建应用,不跟参数的话,回车后会有应用前端框架以及app形式的选择,按照提示进行即可。
vue add pwa
因为ionic是适配了三大前端框架,所以我选择了结合vue进行编写,所用再通过vue的脚手架安装vue所包含的pwa模块,安装这个模块后,会在项目src的目录下生成registerServiceWorker.ts的文件,类似于综合了一些serviceWorker的生命周期函数,可以在这个文件里可以缓存一些自己自定义的数据。而项目里vue的打包静态文件都会自动缓存,无需自行管理。当打包后,会在打包根目录下生成service-worker.js文件,用来注册使用serviceWorker。
当然对于service-worker这个API里面有好几个概念理解了两三天才有点了解,所以需要记录一下。
不过需要注意一点,需要在public目录下自己新建一个manifest.json的配置文件,pwa应用主要就是看这个配置文件,里面可以配置pwa应用的icon各种大小的配置,app名称,显示样式等等,不配置的话打包后会有一个默认的manifest.json配置生成,所以应当自己去手动创建!
更新缓存过程
这个更新过程是指项目静态资源打包后更新serviceWorker的意思。
首先我在自己的探索中,发现vue的pwa模块会自动缓存vue打包后所有的静态文件,但是在assets文件夹下似乎经支持一级目录,也就是assets/icon/*.png是支持serviceWorker缓存的,而二级目录以及多级目录是不支持缓存的,多级目录下的文件均不会缓存。
在src目录下,如果你自己创建了一些目录,如自己封装的请求模块或者单独的模块引入文件的话,如src/plugin/elementUI.js又或者src/request/myaxios.js这样的封装的文件,打包后也是可以进行serviceWorker缓存的。
vue打包后会对文件生成一个hash值作为标识,当你第一次将pwa项目上传后,用户第一次浏览时会自动缓存,至此用户再次打开项目网站时就会走缓存,离线状态下也可以显示,但是它是怎么对源站点文件进行对比的我还不是很了解,可能是对比打包文件的hash值,可是我在开发者工具中的网络里并没有发现哪儿对话进行了网络访问,因为所有的资源请求都走了serviceWorker。当你第二次上传新的打包好的项目后,用户再次打开你的项目网站,serviceWorker就会对新的文件进行下载和缓存,至此就要更新了!更新什么?更新缓存,更新页面,这里问题就来了,用户访问后serviceWorker会自己更新缓存,但是页面却不会更新,即使你刷新页面也是无济于事,除非使用ctrl F5进行强制刷新,或者关闭用户打开的所有关于你项目站点的标签页,然后新建标签页输入项目地址后,新更新的页面就可以正常显示了,这样的体验是很不好的,到底是什么原因呢?
原因就在于,已经打开的标签页一直使用的是旧的serviceWorker,而serviceWorker是独立的线程存在的,当serviceWorker在用户进入旧页面并更新了新页面的缓存,而此时页面的渲染还是使用旧的serviceWorker并且处于waiting的状态,而此时你的新serviceWorker并没有激活使用,只用当你关闭了所有项目的标签页,旧的serviceWorker就是自动结束并结束waiting状态,当你下一次打开网页时,浏览器就会使用新的serviceWorker,此时你的页面就更新了,这样的体验也是非常不好的,需要手动关闭,而在手机上操作问题会更麻烦一些,有什么办法能解决呢?
这里就是比较重要且有意思的地方了,并且还帮我又了解了一些javascript的面貌。
首先在registerServiceWorker.ts文件中有一个updated()函数,当serviceWorker更新后就会触发这个函数,这个函数是被封装过的,(这里还没有但需要了解一些workbox这个东西是什么)看一看源文件就可以发现这个函数是默认传入一个ServiceWorkerRegistration对象,所以代码如下:
updated (registration) {
console.log('New content is available; please refresh.')
document.dispatchEvent(
new CustomEvent('swUpdated', {detail: registration})
)
}
这个函数传入了registration,并且在document中添加了一个自定义事件swUpdated,以便于在我们的页面文件中进行监听是否有更新。
在App.vue或者在项目首页.vue中进行更新事件的监听以及页面的更新,代码如下:
const registration = ref(null) //用于接收registration
const updateExists = ref(false) //标记更新是否存在
const refreshing = ref(false) //标记是否刷新
function updateAvailable(event) {
console.log(event)
registration.value = event.detail
updateExists.value = true
}
document.addEventListener('swUpdated', updateAvailable, {once: true}) //对自定义swUpdated事件进行监听,如果有更新就调用上面的函数对是否更新的标识进行改变。
updateAvailable函数用于改变是否更新的变量,为了执行下面刷新的函数,
function refreshApp() {
updateExists.value = false
if (!registration.value || !registration.value.waiting) return
registration.value.waiting.postMessage({type: 'SKIP_WAITING'})
}
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing.value) return
refreshing.value = true
window.location.reload()
})
可以将上面的refreshApp绑定在页面更新的按钮上,这个函数里会检查registration是否存在并且跳过registration的wating状态,即更新serviceWorker,不再使用旧的serviceWorker,进而使用新的serviceWorker,而下面的serviceWorker.addEventListener函数用于监听“controllerchange”,只要serviceWorker有变化就会执行里面的函数,页面就会进行刷新,而此时刷新使用的是新的serviceWorker,所以页面刷新后就是新的页面,至此,pwa就实现了用户自行更新,且为热更新!
哦对了,上面的函数有一个registration.value.waiting.postMessage({type: 'SKIP_WAITING'})
其中postMessage是发送消息,在registration可以监听“message”,如果有"SKIP_WAITING"进行比对,成功就可以进行self.skipwating()函数进行serviceWorker的waiting状态跳转,进而刷新页面就好,之所以可以直接写“type: 'SKIP_WAITING'”是因为pwa模块或者workbox对serviceWorker进行了封装,就不需要自己进行配置了,没有的话可以在service-worker.js中进行添加(因为用pwa模块这个文件只有打包后才会生成,所以不知道在registerServiceWorker中能不能使用)
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting()
}
})
大功告成!