# pwa技术调研
**Repository Path**: sql_yu/pwa
## Basic Information
- **Project Name**: pwa技术调研
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2024-05-16
- **Last Updated**: 2024-05-16
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# PWA
### 介绍核心
> PWA(Progressive Web App)是一种理念,使用多种技术来增强 web app 的功能,可以让网站的体验变得更好,能够模拟一些原生功能,比如通知推送。在移动端利用标准化框架,让网页应用呈现和原生应用相似的体验
PWA 中包含的核心功能及特性如下:
1. Web App Manifest
1. Service Worker
1. Cache API 缓存
1. Push&Notification 推送与通知
1. Background Sync 后台同步
1. 响应式设计
优势:
1. 无需安装,无需下载,只要你输入网址访问一次,然后将其添加到设备桌面就可以持续使用。
2. 发布不需要提交到 app 商店审核
3. 更新迭代版本不需要审核,不需要重新发布审核
4. 现有的 web 网页都能通过改进成为 PWA, 能很快的转型,上线,实现业务、获取流量
5. 不需要开发 Android 和 IOS 两套不同的版本
## 什么是 service worker
> Service Worker 是 Chrome 团队提出和力推的一个 WEB API,用于给 web 应用提供高级的可持续的后台处理能力。
> Service Workers 就像介于服务器和网页之间的拦截器,能够拦截进出的 HTTP 请求,从而完全控制你的网站。

## 结构组成
1. ### html 文件
```html
PWA
```
2. ### manifest.json: Web 应用程序清单在一个 JSON 文本文件
```json
{
"name": "web pwa name", //完整名称,安装处显示
"short_name": "name", //简称,在app图标时显示
"start_url": ".", //启动屏
"scope": "/", //作用域哪些链接下有用
"display": "standalone", //显示模式。fullscreen|standalone|minimal-ui |browser
"background_color": "#fff", //web 背景色,
"theme_color": "aliceblue", //桌面图标的背景色,
"orientation": "portrait-primary",//Web应用程序顶级的默认方向
"description": "A simply readable Hacker News app.", //有关Web应用程序的一般描述。
"icons": [
{
//桌面图标
"src": "images/touch/homescreen48.png",
"sizes": "48x48",
"type": "image/png"
}
]
}
```
3. register.js 注册文件,用来加载 service worker
4. service worker.js
5. ## Service Worker 生命周期
```js
首先判断是否安装过=>(安装过调用install事件)=》再次进行注册(执行then)=>(首次安装调用install事件)=>执行beforeinstallprompt事件=》installing =》 installed
```

> 如果使用过 Service Worker,之前你可能遇到过这样的问题,原来的 Service Worker 还在起作用,即使文件本身已经更新过。其中的原因在于 Service Worker 生命周期中的一些微妙之处;它可能会被安装,而且是有效的,但实际上却没有被 document 纳入控制。
> Service Worker 可能拥有以下六种状态的一种: 解析成功(parsed),正在安装(installing),安装成功(installed),正在激活(activating),激活成功(activated),废弃(redundant)。

1. ### 解析成功(Parsed)
> 首次注册 Service Worker 时,浏览器解决脚本并获得入口点。如果解析成功(而且满足其他条件,如 HTTPS 协议),就可以访问到 Service Worker 注册对象(registration object),其中包含 Service Worker 的状态及其作用域。
```js
/* register.js */
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("./sw.js")
.then(function(registration) {
console.log("Service Worker Registered", registration);
})
.catch(function(err) {
console.log("Service Worker Failed to Register", err);
});
}
```
> Service Worker 注册成功,并不意味着它已经完成安装,也不能说明它已经激活,仅仅是脚本被成功解析,与 document 同源,而且源协议是 HTTPS。一旦完成注册,Service Worker 将进入下一状态。
2. ### 正在安装(Installing)
> Service Worker 脚本解析完成后,浏览器会试着安装,进入下一状态,“installing”。在 Service Worker 注册(registration) 对象中,我们可以通过 installing 子对象检查该状态。
```js
/* register.js */
navigator.serviceWorker.register("./sw.js").then(function(registration) {
if (registration.installing) {
// Service Worker is Installing
}
});
```
> 在 installing 状态中,Service Worker 脚本中的 install 事件被执行。我们通常在安装事件中,为 document 缓存静态文件。
```js
/* In sw.js */
self.addEventListener("install", function(event) {
event.waitUntil(
caches.open(currentCacheName).then(function(cache) {
return cache.addAll(arrayOfFilesToCache);
})
);
});
```
> 若事件中有 event.waitUntil() 方法,则 installing 事件会一直等到该方法中的 Promise 完成之后才会成功;若 Promise 被拒,则安装失败,Service Worker 直接进入废弃(redundant)状态。
```js
/* In sw.js */
self.addEventListener("install", function(event) {
event.waitUntil(
return Promise.reject();
);
});
//安装失败
```
3. ### 安装成功/等待中(Installed/Waiting)
> 如果安装成功,Service Worker 进入安装成功(installed)(也称为等待中[waiting])状态。在此状态中,它是一个有效的但尚未激活的 worker。它尚未纳入 document 的控制,确切来说是在等待着从当前 worker 接手。
> 在 Service Worker 注册(registration) 对象中,可通过 waiting 子对象检查该状态。
```js
/* register.js */
navigator.serviceWorker.register("./sw.js").then(function(registration) {
if (registration.waiting) {
//已经安装,等待激活
// Service Worker is Waiting
}
});
```
这是通知 App 用户升级新版本或自动升级的好时机
4. ### 正在激活(Activating)
> 处于 waiting 状态的 Service Worker,在以下之一的情况下,会被触发 activating 状态。
- 当前已无激活状态的 worker
- Service Worker 脚本中的 self.skipWaiting() 方法被调用
- 用户已关闭 Service Worker 作用域下的所有页面,从而释放了此前处于激活态的 worker
- 超出指定时间,从而释放此前处于激活态的 worker
> 处于 activating 状态期间,Service Worker 脚本中的 activate 事件被执行。我们通常在 activate 事件中,清理 cache 中的文件。
```js
/* In sw.js */
self.addEventListener("activate", function(event) {
event.waitUntil(
// 获取所有 cache 名称
caches.keys().then(function(cacheNames) {
return Promise.all(
// 获取所有不同于当前版本名称 cache 下的内容
cacheNames
.filter(function(cacheName) {
return cacheName != currentCacheName;
})
.map(function(cacheName) {
// 删除内容
return caches.delete(cacheName);
})
); // end Promise.all()
}) // end caches.keys()
); // end event.waitUntil()
});
```
> 与 install 事件类似,如果 activate 事件中存在 event.waitUntil() 方法,则在其中的 Promise 完成之后,激活才会成功。如果 Promise 被拒,激活事件失败,Service Worker 进入废弃(redundant)状态。
5. ### 激活成功(Activated)
> 如果激活成功,Service Worker 进入 active 状态。在此状态中,其成为接受 document 全面控制的激活态 worker。在 Service Worker 注册(registration) 对象中,可以通过 active 子对象检查此状态。
```js
/* register.js */
navigator.serviceWorker.register("./sw.js").then(function(registration) {
if (registration.active) {
// Service Worker is Active
}
});
```
> 如果 Service Worker 处于激活态,就可以应对事件性事件 —— fetch 和 message
```js
/* In sw.js */
self.addEventListener("fetch", function(event) {
// Do stuff with fetch events
});
self.addEventListener("message", function(event) {
// Do stuff with postMessages received from document
});
```
6. ### 废弃(Redundant)
> Service Worker 可能以下之一的原因而被废弃
- installing 事件失败
- activating 事件失败
- 新的 Service Worker 替换其成为激活态 worker
## Sync 后台同步

```js
/**register.js*/
/* ========================================== */
/* service worker background sync 相关部分 */
/* ========================================== */
export function syncData(tag = "syncData") {
if ("SyncManager" in window) {
// 一个background sync的基础版
// 进行注册
return navigator.serviceWorker.ready.then(function(registration) {
return registration.sync
.register(tag)
.then(function() {
//注册
console.log("后台同步已触发", tag);
var msg = JSON.stringify({ type: "bgsync", msg: { name: inputValue } });
navigator.serviceWorker.controller.postMessage(msg);
})
.catch(function(err) {
console.log("后台同步触发失败", err);
});
});
}
}
```
```js
/**sw.js*/
//为 SW 提供一个可以实现注册和监听同步处理的方法
self.addEventListener("sync", function(event) {
const tag = event.tag;
if (tag === "one1") {
const request = new Request(`sync`, { method: "POST" });
e.waitUntil(
//一直循环,直到成功,
fetch(request).then(function(response) {
response.json().then(console.log.bind(console));
return response;
})
);
} else {
}
});
```
## Push (用户订阅相关的 push 信息)

```js
/**register.js*/
const subscribeOptions = {
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey)
};
return registration.pushManager.subscribe(subscribeOptions).then(function(pushSubscription) {
return sendSubscriptionToServer({ pushSubscription, uniqueid: Date.now() }, url); //fetch 发送到服务器
});
```
```js
/**sw.js*/
/* ======================================= */
/* push处理相关部分,已添加对notification的调用 */
/* ======================================= */
self.addEventListener('push', function (e) {
let data = e.data;
self.registration.showNotification("PWA Push Title", {
body: "Test PWA Push Title",
icon: '/icons/book-128.png',
image: '/icons/book-521.png', // no effect
actions: [{
action: 'key1',
title: 'action1'
}, {
action: 'key2',
title: 'action2'
}],
tag: 'pwa',
renotify: true
});
})
```
```js
/**app.js*/
const webpush = require('web-push');
/**
* VAPID值
* 这里可以替换为你业务中实际的值
*/
const vapidKeys = {
publicKey: 'BOEQSjdhorIf8M0XFNlwohK3sTzO9iJwvbYU-fuXRF0tvRpPPMGO6d_gJC_pUQwBT7wD8rKutpNTFHOHN3VqJ0A',
privateKey: 'TVe_nJlciDOn130gFyFYP8UiGxxWd3QdH6C5axXpSgM'
};
// 设置web-push的VAPID值
webpush.setVapidDetails(
'mailto:alienzhou16@163.com',
vapidKeys.publicKey,
vapidKeys.privateKey
);
/**
* 向push service推送信息
* @param {*} subscription
* @param {*} data
*/
function pushMessage(subscription, data = {}) {
webpush.sendNotification(subscription, data, options).then(data => {
console.log('push service的相应数据:', JSON.stringify(data));
return;
}).catch(err => {
})
}
// app.js
const koaBody = require('koa-body');
/**
* 提交subscription信息,并保存
*/
const Record=new Map();
router.post('/subscription', koaBody(), async ctx => {
let body = ctx.request.body;
let {uniqueid, payload} = body;
await Record.add(uniqueid,body);
ctx.response.body = {
status: 0
};
});
/**
* 消息推送API,可以在管理后台进行调用
* 本例子中,可以直接post一个请求来查看效果
*/
router.post('/push', koaBody(), async ctx => {
let {uniqueid, payload} = ctx.request.body;
let list = uniqueid ? await [Record.get(uniqueid)] : await Object.values(Record);
let status = list.length > 0 ? 0 : -1;
for (let i = 0; i < list.length; i++) {
let subscription = list[i].subscription;
pushMessage(subscription, JSON.stringify(payload));
}
ctx.response.body = {
status
};
});
```