PWA:Service Worker实现离线访问、域名失效启用备用域名......

news/2024/11/18 18:42:31/

介绍下Service Worker

service worker 运行在独立的线程,可以拦截和处理网络请求,以实现离线缓存、推送通知和后台同步等功能。具体可以看MDN的介绍

怎么使用Service Worker

1、注册脚本

// useServiceWorker.ts
navigator.serviceWorker?.register('/sw.js').then(() => {console.log('Service Worker Registered')
}// sw.js
// 注册后触发
self.addEventListener('install', (e) => {self.skipWaiting()e.waitUntil(caches.open(storeName).then((cache) => cache.addAll(['/'])),)
})

2、接管网络请求

// sw.js
// 拦截响应数据
self.addEventListener('fetch', (e) => {const response = requestHandler(e) // 经过requestHandler处理后,返回缓存的内容或服务器响应的数据try {e.respondWith(response) // 将数据返回给主线程相应的请求} catch (err) {console.error(err, e.request, response);}
})

3、利用接管网络请求的功能,实现本地数据持久化,为离线访问提供服务

// 处理请求
const requestHandler = async (e) => {return new Promise( async (resolve, reject) => {const newRequest = await fetchInterceptor(e)if (['image', 'document', 'script'].includes(e.request.destination) || e.request.url.slice(-4) === '.css' || e.request.url.slice(-3) === '.js' ) {// 匹配缓存中请求,命中则返回缓存内容,否则获取服务器内容resolve(await caches.match(e.request) ||  await getData(newRequest))} else if (newRequest.url !== e.request.url) {resolve( await fetch(newRequest) )} else {resolve( await fetch(newRequest) )}})
}// 发起请求, 缓存请求成功的get请求
async function getData(request) {const response = await fetch(request)const cache = await caches.open(storeName)response.clone()?.status === 200 && cache.add(request, response.clone())return response
}

4、利用接管网络请求的功能,拿到请求报文后,将过期域名修改为备用域名,返回新的请求报文

// 拦截请求,返回新请求地址
async function fetchInterceptor(e) {let { url, method, headers, destination } = e.requestif (!parkedDomain|| url.indexOf(location.origin) === -1|| ['image', 'document', 'script'].includes(destination)|| e.request.url.slice(-4) === '.css'|| e.request.url.slice(-3) === '.js') return e.request;url = url.replace(location.origin, parkedDomain)const init = { method, headers, mode: 'cors' }let paramsif (e.request.clone().body) {params = await e.request.clone().json()init.body = JSON.stringify(params)}const newRequest = new Request(url, init)return newRequest
}

5、主线程传数据到Service Worker

// useServiceWorker.ts
// 发送信息给Service Worker
navigator.serviceWorker?.controller?.postMessage({'parkedDomain': localStorage.getItem('parkedDomain') || ''
})// sw.js
let currentClient = null // 通讯对象,后续需要这个对象传信息到主线程
// 接收主线程传来的信息
self.addEventListener("message", (e) => {if (!currentClient) {currentClient = await self.clients.get(e.source.id)}
})

6、Service Worker传数据到主线程

// sw.js
let currentClient = null // 通讯对象, 前面接收过主线程的数据,已经获取到通讯的对象
currentClient?.postMessage({ event, payload: JSON.stringify(payload) })// useServiceWorker.ts
// 接收来自Service Worker的信息
navigator.serviceWorker?.addEventListener('message', (e) => {const event: 'setItem' | 'reload' | 'doLog' = e.data.eventconst payload: string = e.data.payloadif (!event || !payload) return;handlerMap[event]?.(payload)
})

7、更新sw.js

// sw.js更新的时候,需要页面重新加载才会执行新的ServiceWorker脚本
let refreshing = false
navigator.serviceWorker?.addEventListener('controllerchange', () => {if (refreshing) returnrefreshing = truelocation.reload()
})

完整的代码如下

1、useServiceWorker.ts

// useServiceWorker.ts
if (!window.NativeBridge && process.env.NODE_ENV === 'production') {let refreshing = false// sw.js更新的时候,需要页面重新加载才会执行新的ServiceWorker脚本navigator.serviceWorker?.addEventListener('controllerchange', () => {if (refreshing) returnrefreshing = truelocation.reload()})navigator.serviceWorker?.register('/sw.js').then(() => {const handlerMap = {setItem: (payload: any) => {const obj = JSON.parse(payload)localStorage.setItem(obj.key, obj.value)},reload: () => {location.reload()},doLog: (data: any) => {// useMultiDoLog(JSON.parse(data))}}navigator.serviceWorker?.addEventListener('message', (e) => {const event: 'setItem' | 'reload' | 'doLog' = e.data.eventconst payload: string = e.data.payloadif (!event || !payload) return;handlerMap[event]?.(payload)})navigator.serviceWorker?.controller?.postMessage({'parkedDomain': localStorage.getItem('parkedDomain') || '','domainFileUrl': localStorage.getItem('domainFileUrl') || ''})})
}

2、sw.js

/** * storeName在打包时自动更改,要调整注意修改ReplaceInFileWebpackPlugin插件配置 
*/
const storeName = 'video-store-SERVICEWORKER_NAME' 
/** 当前域名不可以则开启请求拦截,使用可用域名替换当前域名 */
let parkedDomain = ''
/** 可用域名列表文件 */
let domainFileUrl = ''/** 通讯对象 */
let currentClient = nullself.addEventListener('install', (e) => {self.skipWaiting()e.waitUntil(caches.open(storeName).then((cache) => cache.addAll(['/'])),)
})
// 清除失效缓存
self.addEventListener('activate', (event) => {event.waitUntil(caches.keys().then((cacheNames) => {return Promise.all(cacheNames.filter((cacheName) => {return cacheName !== storeName}).map((cacheName) => {return caches.delete(cacheName)}))}))
})
// 拦截响应数据
self.addEventListener('fetch', (e) => {const response = requestHandler(e)try {e.respondWith(response)} catch (err) {console.error(err, e.request, response);}
})// 拦截请求,返回新请求地址
async function fetchInterceptor(e) {let { url, method, headers, destination } = e.requestif (!parkedDomain|| url.indexOf(location.origin) === -1|| ['image', 'document', 'script'].includes(destination)|| e.request.url.slice(-4) === '.css'|| e.request.url.slice(-3) === '.js') return e.request;url = url.replace(location.origin, parkedDomain)const init = { method, headers, mode: 'cors' }let paramsif (e.request.clone().body) {params = await e.request.clone().json()init.body = JSON.stringify(params)}const newRequest = new Request(url, init)return newRequest
}// 处理请求
const requestHandler = async (e) => {return new Promise( async (resolve, reject) => {const newRequest = await fetchInterceptor(e)if (['image', 'document', 'script'].includes(e.request.destination) || e.request.url.slice(-4) === '.css' || e.request.url.slice(-3) === '.js' ) {// 匹配缓存中请求,命中则返回缓存内容,否则获取服务器内容resolve(await caches.match(e.request) ||  await getData(newRequest))} else if (newRequest.url !== e.request.url) {resolve( await fetch(newRequest) )} else {resolve( await fetch(newRequest) )}})
}// 接收主线程传来的信息
self.addEventListener("message", async (e) => {if (!currentClient) {currentClient = await self.clients.get(e.source.id)}if('parkedDomain' in  e?.data) {e?.data?.parkedDomain && (parkedDomain = e?.data?.parkedDomain)e?.data?.domainFileUrl && (domainFileUrl = e?.data?.domainFileUrl)parkedDomainStrategy()}
})// 发起请求, 缓存请求成功的get请求
async function getData(request) {const response = await fetch(request)const cache = await caches.open(storeName)response.clone()?.status === 200 && cache.add(request, response.clone())return response
}// 备用域名策略
async function parkedDomainStrategy() {sendMessageToClient('doLog', { event: 'xxxx' }) // 可用域名上报日志const isValid = await verifyDomain(parkedDomain)if(isValid) {sendMessageToClient('doLog', { event: 'xxxx' }) // 可用域名上报日志parkedDomain && sendMessageToClient('reload', {})} else {failureDomainHandler()}
}
// 校验域名是否有效
function verifyDomain (domain) {return new Promise((resolve, reject) => {fetch(domain + '/xxxx.txt').then(res => { resolve(res.ok)}).catch(err => {resolve(false)})})
}
// 域名失效处理
function failureDomainHandler () {// 请求html文件失败,则域名不可用,使用可用域名去替换请求地址fetch(domainFileUrl).then(stream => {stream.text().then( async (text) => {let domians = text.split('\n')for (const index in domians) {const isValid = await verifyDomain(domians[index])if (isValid) { parkedDomain = domians[index]sendMessageToClient('doLog', {value:''}) // 可用域名上报日志sendMessageToClient('setItem', { key: 'parkedDomain', value: parkedDomain || '' })sendMessageToClient('reload', {})break }}})})
}// 发送消息到主线程
async function sendMessageToClient(event, payload) {currentClient?.postMessage({event,payload: JSON.stringify(payload)})
}

补充

由于文件会缓存到本地,所以每次更新代码都需要新建一个缓存空间存储更新后的代码,旧的缓存空间则作废
这里用到的是replace-in-file-webpack-plugin插件,在打包时,将SERVICEWORKER_NAME替换为当前的时间戳

// sw.js
const storeName = 'video-store-SERVICEWORKER_NAME' // vue.config.js
const ReplaceInFileWebpackPlugin = require('replace-in-file-webpack-plugin');
module.exports = defineConfig({configureWebpack: {new ReplaceInFileWebpackPlugin([{dir: 'dist',test: /\.js$/,rules: [{search: /SERVICEWORKER_NAME/g,replace: JSON.stringify(new Date().getTime())}]}])}
})

http://www.ppmy.cn/news/258141.html

相关文章

美敦力PB560呼吸机设计图纸 源代码分享

美敦力PB560呼吸机设计图纸 源代码分享 全套资料下载:一牛网论坛

PyQt5实时曲线实现(肺功能仪,呼吸机)

PyQt5——实时曲线 摘自大佬:https://www.it610.com/article/1282184782742044672.htm 稍微修改了一下,仅做笔记,还会添加功能 import sys import random from PyQt5.QtChart import QDateTimeAxis, QValueAxis, QSplineSeries, QChart, QCh…

美国进口呼吸机PB560设计完整资料分享

美国美敦力公司(Medtronic, Inc.)成立于1949年,是全球领先的医疗科技公司,致力于为慢性疾病患者提供终身的治疗方案。美敦力公司为了响应FDA的号召,宣布公开分享其旗下Puritan Bennett 560 (PB560&#xff…

​医用呼吸机Everspin MRAM应用笔记

由于当前疫情的大流行,增加医用呼吸机的供应迫在眉睫。Everspin Technologies提供了一种独特的MRAM存储技术,它将为这类设备的电子系统设计带来好处。 呼吸机是一种通过将可呼吸的空气移入和移出肺部来提供机械通气的机器,用于向身体不能呼吸…

呼吸机的前世与今生:呼吸机的发展历史

最早在16世纪时,Andreas Vesalius第一次提出了一种可以被认为是人工通气的方法:他在动物的气管里插入一个气管,通过气管向动物的肺里鼓风,借此来维持动物的生命 。[1] 1670年,John Mayow使用插入膀胱内部的气囊模拟了…

中国睡眠呼吸机市场研究与未来预测报告(2021版)

内容简介: 随着以COPD和OSA 为主的呼吸及睡眠相关疾病患者人数持续增长,全球对睡眠呼吸机的需求也逐年增长。2020年,全球睡眠呼吸机市场达到27.74亿美元。随着睡眠呼吸机在包括中国在内的新兴市场不断普及,预计到2021年&#xff…

开源呼吸机设计- 基于TMC4671

Trinamic开源呼吸机 为了应对COVID-19爆发的医疗设备需求,Trinamic开发了Trinamic开源呼吸机(TOSV)。TOSV使用基于TMC4671-LA和TMC4671-Eval-Kit的模块。小巧集成模块的PCB能处理所有驱动程序功能以及提供压力传感器的接口。Raspberry Pi等单…

瑞思迈Astral生命支持呼吸机现已在美国提供AutoEPAP

其他升级包括轻松的界面切换和旨在改善易用性的程序 圣迭戈 -- (美国商业资讯) -- 云端呼吸治疗装置的全球领先者瑞思迈(ResMed, NYSE: RMD, ASX: RMD)今天发布了其Astral生命支持呼吸机的关键升级,包括面向美国患者的iVAPS模式下AutoEPAP选项,这是一种具…