PWA
# 什么是PWA
PWA(渐进式Web App),MDN参考 (opens new window)
# 优点
- 渐进式:可安装到桌面,使用更方便
- 离线使用:网络差、离线也都可以进行访问
- 原生体验:可配置启动页、隐藏地址栏等,拥有app类似的使用体验
- 离线推送:可推送离线通知,增加用户粘性
# PWA现状
# 背景
- 国外
- 手机自带谷歌浏览器与谷歌服务
- 不需要翻墙
- 国内
- 浏览器众多(qq、360、百度浏览器等),导致对h5、es6、css3支持度不统一
- 谷歌浏览器普及度低
- 外网需要翻墙
快应用前几年国内刚兴起,但各浏览器厂商因为利益等各种原因,不能形成一套规范,最终还是小程序胜利✌️
# 应用一:创建桌面应用
由于pc端chrome、firefox浏览器普及度高,所以pc端适用效果还不错,其他浏览器厂商支持不一未进行测试(如:360浏览器、QQ浏览器等)
因为国外手机自带谷歌浏览器与谷歌服务,所以pwa应用在国外非常流行,而在国内恰恰相反,移动端各浏览器厂商支持程度不一,又要涉及各种权限,所以在国内pwa应用基本凉凉。
# 应用二:离线消息通知
离线通知,要借助谷歌服务,先把消息传给谷歌相应消息平台(FCM),然后由谷歌向浏览器派送消息,使得用户可以接收到离线通知。因为谷歌浏览器普及度低,又要借助翻墙,所以离线通知在国内基本没戏。
# 应用三:service worker
有离线需求的话,service worker + caches可以去单独使用,做离线存储效果还不错,主流浏览器几乎都支持
# 谁在用
# pwa
- 抖音 (opens new window)
- 语雀 (opens new window)
- YouTube (opens new window)
- 微博 (opens new window)
- 个人博客 (opens new window)
# service worker
# 技术要点
# 1.manifest.json
应用清单,用来配置应用图标、展示方式、主题、入口页等,应用基本信息 MDN参考 (opens new window)
# 2.service worker
pwa的核心技术,是一个独立的worker线程,不会占用js主线程,可用来做持久化离线缓存。MDN参考 (opens new window)
注意:只能在https或loclhost下使用
# 3.cacheStorage
配合service worker来实现缓存,MDN参考 (opens new window)
# 在vue中使用
# vite中使用
vite-plugin-pwa (opens new window)
yarn add vite-plugin-pwa -D
- vite.config.js增加配置
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
VitePWA({ registerType: 'autoUpdate' })
]
})
1
2
3
4
5
6
2
3
4
5
6
vite-plugin-pwa入门指南 (opens new window)
# vue-cli中使用
终端执行 vue add @vue/pwa
参考 (opens new window)
# 案例完整代码
# manifest.json
{
// 应用名称
"name": "一个神器的应用",
// 应用简称
"short_name": "神应用",
// 应用描述
"description": "一个无所不能的离线应用",
// 启动首页地址
"start_url": "/index.html",
// app显示模式
// 1.standalone 更像app 2.fullscreen 全屏模式 3.minimal-ui 会显示地址栏
"display": "standalone",
// 启动动画背景色
"background_color": "#999",
// 主题颜色
"theme_color": "#fff",
// 应用图标
"icons": [
{
"src": "/image/logo.png",
"sizes": "256x256",
"type": "image/png"
}
],
// 应用语言
"lang":"zh-CN"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="manifest" href="./manifest.json">
<link rel="stylesheet" href="css/index.css">
<title>pwa演示</title>
</head>
<body>
<h3>service worker缓存图片</h3>
<img src="image/1.png" width="500">
<h3>http强缓存图片</h3>
<img src="https://oper.anju.dev.nnzhineng.com:20104/s/cms/e1ac985b-30b5-45f8-a950-e1c534c876da/%E7%AE%80%E7%BA%A6%E7%BB%BF%E8%89%B2%E5%A4%A7%E6%9A%91%E8%8A%82%E6%B0%94%E6%8F%92%E7%94%BB%E5%8D%A1%E9%80%9A%E5%85%AC%E4%BC%97%E5%8F%B7%E9%A6%96%E5%9B%BE_%E5%85%AC%E4%BC%97%E5%8F%B7%E5%B0%81%E9%9D%A2%E9%A6%96%E5%9B%BE_2022-07-14+11_09_09.jpeg" width="500">
<h3>无缓存图片</h3>
<img src="image/2.png" width="500">
</body>
<script>
window.addEventListener('load', () => {
if ('serviceWorker' in navigator) {
console.log('该环境支持 serviceWorker')
navigator.serviceWorker.register('./sw.js').then(sw =>
console.log('serviceWorker启动成功:', sw)
).catch(err => {
console.log('serviceWorker启动失败',err)
});
}
})
// 浏览器无接受消息权限,去获取授权
if(Notification.permission === 'default'){
Notification.requestPermission()
}
window.addEventListener('online', () => {
new Notification('网络提示',{body:'您的设备处于联网状态'})
// 刷新页面
location.reload();
})
window.addEventListener('offline', () => {
new Notification('网络提示',{body:'您的设备处于断网状态'})
})
</script>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# sw.js
// cache缓存名称
const CACHE_NAME = 'cache_v1'
self.addEventListener('install', async event => {
console.log('serviceWorker 安装成功', event)
// 开启一个cache
const cache = await caches.open(CACHE_NAME)
// cache存储资源
await cache.addAll([
'/', // 要缓存/ 而不是缓存 /index.html,因为访问的是localhost/
'/image/logo.png',
'/image/1.png',
'/manifest.json',
'/css/index.css'
])
// 让serviceWorker跳过等待,直接进入activate状态
// event.waitUntil 等待skipWaiting结束后,在往下执行,也可以用await
await self.skipWaiting()
})
self.addEventListener('activate', async event => {
console.log('serviceWorker 重新激活', event)
// 去除旧的缓存资源
const keys = await caches.keys()
keys.forEach(key => {
if(key !== CACHE_NAME){
caches.delete(key)
}
})
// 立即获得控制权,否则要等到页面下次刷新才能拿到控制权
await self.clients.claim()
})
// 处理fetch请求
self.addEventListener('fetch', event => {
console.log('捕获fetch请求', event)
const req = event.request
// 给浏览器响应
event.respondWith(networkFirst(req))
})
async function networkFirst(req){
const res = (await caches.match(req))
if(res){
return res
}else {
return await fetch(req)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 效果
编辑 (opens new window)
上次更新: 2022/08/11