# 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 请求,从而完全控制你的网站。 ![](/image/worker.png) ## 结构组成 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 ``` ![生命周期](./image/life.png) > 如果使用过 Service Worker,之前你可能遇到过这样的问题,原来的 Service Worker 还在起作用,即使文件本身已经更新过。其中的原因在于 Service Worker 生命周期中的一些微妙之处;它可能会被安装,而且是有效的,但实际上却没有被 document 纳入控制。 > Service Worker 可能拥有以下六种状态的一种: 解析成功(parsed),正在安装(installing),安装成功(installed),正在激活(activating),激活成功(activated),废弃(redundant)。 ![生命周期](./image/life2.png) 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 后台同步 ![Sync 后台同步](./image/sync.png) ```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 信息) ![Push](./image/push.webp) ```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 }; }); ```