当前位置: 首页 > news >正文

Service Work离线体验与性能优化

Service Work离线体验与性能优化

引言

先放个意外事件,万事开头难🤣🤣🤣
原计划是分享离线应用与数据资源缓存的应用实践,结果发现这一技术已被web标准废弃
在这里插入图片描述
曾经做过一个PC应用,业务需求要求应用具备容灾机制,能够在无网络情况下离线使用,并在网络恢复后同步数据。这需要利用缓存技术来实现。

传统前端缓存技术

HTTP缓存

  • 工作原理:HTTP缓存通过请求头中的过期时间和标识符来判断是否使用缓存。
  • 局限性:依赖于服务器配置,不适用于复杂的离线场景。

浏览器缓存

  • 主要形式:localStorage、sessionStorage、cookie。
  • 用途:存储少量的数据,但不适合大规模数据缓存。

应用缓存–Application Cache

  • 全称:Offline Web Application。
  • 特点:通过manifest文件标注要缓存的静态文件清单。
  • 问题:更新机制复杂,页面更新延迟,已被Web标准废弃

它的缓存内容被存在浏览器的Application Cache中。主要是通过manifest文件来标注要被缓存的静态文件清单。但是在缓存静态文件的同时,也会默认缓存html文件。这导致页面的更新只能通过manifest文件中的版本号来决定。而且,即使我们更新了version,用户的第一次访问还是会访问到老的页面,只有下一次再访问才能访问到新的页面。所以,应用缓存只适合那种常年不变化的静态网站。

数据库缓存

  • 选项:WebSQL(已废弃)、IndexedDB。
  • IndexedDB:浏览器提供的本地数据库,支持大量数据存储和索引,适合复杂的数据缓存需求。
    IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。

因此通过Manifest+indexDB可做到应用可离线使用,数据缓存且不丢失。

  • Manifest配置需要缓存的静态资源(打包过后的dist文件内容)
  • indexDB对业务数据进行存储

既然manifest这一方案已被废弃。于是转战Service Worker这一更优方案。

Service Worker概述

什么是 Service Worker?

  • 定义:一种可编程的网络代理,允许你拦截和处理应用发出的所有网络请求,包括拦截和处理网络请求、管理缓存和处理推送通知等。
  • 功能:离线支持、推送通知、缓存管理等。

Service Worker的应用场景

  1. 离线支持:通过缓存静态资源和动态内容,确保应用在没有网络连接时仍然可以使用
  2. 缓存管理:提高应用性能,通过缓存减少网络请求次数和加快页面加载速度。
  3. 推送通知:Service Worker 可以处理推送通知,即使用户没有打开应用也能接收消息。

三、Service Worker的特点

  • 独立于主线程: Service Worker 运行在独立的线程中,不会阻塞主页面的执行,是后台运行的脚本。
  • 生命周期管理:Service Worker 有安装、激活和更新等生命周期事件,被install后就永远存在,除非被手动卸载。
  • 跨域与安全限制:同源策略,必须是https的协议才能使用。
  • 不能直接操纵dom:因为Service Worker是个独立于网页运行的脚本。
  • 可拦截请求和返回,缓存文件。Service Worker可以通过fetch这个api,来拦截网络和处理网络请求,再配合cacheStorage来实现web页面的缓存管理以及与前端postMessage通信。

Service Worker的生命周期

当一个Service Worker被注册成功后,它将开始它的生命周期,我们对Service Worker的操作一般都是在其生命周期里面进行的。Service Worker的生命周期分为这么几个状态 安装中, 安装后, 激活中, 激活后, 废弃。

  • 安装( Installing ): 这个状态发生在 Service Worker 注册之后,表示开始安装,这个状态会触发 install 事件,一般会在install事件的回调里面进行静态资源的离线缓存, 如果这些静态资源缓存失败了,那 Service Worker 安装就会失败,生命周期终止。
  • 安装后( Installed ): 当成功捕获缓存到的资源时,Service Worker 会变为这个状态,当此时没有其他的Service Worker 线程在工作时,会立即进入激活状态,如果此时有正在工作的Service Worker 工作线程,则会等待其他的 Service Worker 线程被关闭后才会被激活。可以使用 self.skipWaiting() 方法强制正在等待的servicework工作线程进入激活状态。
  • 激活( Activating ): 在这个状态下会触发activate事件,在activate 事件的回调中去清理旧版缓存。
  • 激活后( Activated ): 在这个状态下,servicework会取得对整个页面的控制
  • 废弃状态 ( redundant ): 这个状态表示一个 Service Worker 的生命周期结束。新版本的 Service Worker 替换了旧版本的 Service Worker会出现这个状态

Service Worker的缓存机制

Service Worker 技术不可或缺的一个方面是 Cache 接口,它是一种完全独立于 HTTP 缓存的缓存机制。可在 Service Worker 作用域和主线程作用域内访问 Cache 接口。

HTTP缓存会受到 HTTP 标头中指定的缓存指令的影响,而 Cache 接口可通过 JavaScript 进行编程。这意味着,网络请求的响应可以基于最适合指定网站的任何逻辑。例如:

  • 在第一次请求时将静态资源存储在缓存中,并且仅为每个后续请求从缓存中提供这些资源
  • 将网页标记存储在缓存中,但仅在离线场景中提供缓存中的标记。
  • 从缓存中为某些资产提供过时的响应,但要在后台通过网络对其进行更新。
  • 从网络流式传输部分内容,并将其与缓存中的 App Shell 组合起来,以提升感知性能。

Service Worker的简单实践

  1. 浏览器兼容性检查

首先,在开始使用 Service Worker 之前,你需要确保用户的浏览器支持这项技术。Service Worker 对象存在于navigator对象下,可以通过以下代码来检查

 if ('serviceWorker' in navigator) {// 浏览器支持 Service Worker} else {console.log('Service Worker not supported');
}
  1. 注册Service Worker

注册是启动 Service Worker 的第一步。通常是在主应用中通过 JavaScript 来完成这个过程,在主线程中调用navigator.serviceWorker.register()方法来注册 Service Worker:

register 方法接受两个参数
第一个参数表示ServiceWork.js相对于origin的路径
第二个参数是 Serivce Worker 的配置项,可选填,其中比较重要的是 scope 属性,用来指定你想让 service worker 控制的内容的目录。 默认值为servicework.js所在的目录。这个属性所表示的路径不能在 service worker 文件的路径之上,默认是 Serivce Worker 文件所在的目录。 成功注册或返回一个promise。

 if ('serviceWorker' in navigator) {// 浏览器支持 Service Workerwindow.addEventListener('load', () => {navigator.serviceWorker.register('/service-worker.js').then((registration) => {console.log('Service Worker registered with scope:', registration.scope)}).catch((error) => {console.error('Service Worker registration failed:', error)})
})} else {console.log('Service Worker not supported');
}

此代码在主线程上运行,并执行以下操作:
1、由于用户首次访问网站发生在没有注册的 Service Worker 的情况下, 等到页面完全加载后再注册一个。 这样可以在 Service Worker 预缓存任何内容时避免带宽争用。
2、进行快速检查有助于避免在不支持此功能的浏览器出现错误。
3、当页面完全加载且支持 Service Worker 时,注册 /service-worker.js。

  1. Service Worker-安装(Installing)

安装事件发生在 Service Worker 首次安装时,每个 Service Worker 仅调用一次 install,并且在更新之前不会再次触发。

self.addEventListener('install', event => {event.waitUntil(caches.open('缓存版本ID').then(cache => {return cache.addAll(['/',...]);}));
});

此代码会创建一个新的 Cache 实例并预缓存资产。
此处重点关注:event.waitUntil事件
event.waitUntil 接受 promise; 并等待该 promise 得到解决。 该 promise 执行两项异步操作:
创建名为 ‘缓存版本ID’ 的新 Cache 实例。
创建缓存后 资源网址数组使用其异步缓存资源预缓存 addAll 方法。
如果传递给 event.waitUntil 的 promise 已拒绝。 如果发生这种情况,Service Worker 会被丢弃。

  1. Service Worker-激活

如果注册和安装成功, Service Worker 激活,并且其状态变为 ‘activating’ ,你可以在这里执行清理旧版本资源的操作:

self.addEventListener('activate', event => {const cacheWhitelist = ['缓存版本ID'];event.waitUntil(caches.keys().then(cacheNames => {return Promise.all(cacheNames.map(cacheName => {if (cacheWhitelist.indexOf(cacheName) === -1) {return caches.delete(cacheName);}}));}));
});
  1. Service Worker-捕获 Fetch

通过监听Service Worker的 fetch 事件来拦截网络请求,
调用 event 上的 respondWith() 方法来劫持当前servicework控制域下的 HTTP 请求,该方法会直接返回一个Promise 结果 ,这个结果就会是http请求的响应。上面代码中就一个简单的逻辑,先劫持http请求,然后看看缓存中是否有这个请求的资源,如果有则直接返回,如果没有就去请求服务器上的资源。 event.respondWith 方法只能在 Service Worker 的 fetch 事件中使用。

self.addEventListener('fetch', event => {event.respondWith(caches.match(event.request).then(response => {if (response) {console.log('Serving from cache:', event.request.url);return response;}console.log('Fetching from network:', event.request.url);return fetch(event.request).then(networkResponse => {if (networkResponse && networkResponse.ok) {console.log('Caching new response:', event.request.url);return caches.open('f69905188ac970f1').then(cache => {cache.put(event.request, networkResponse.clone());return networkResponse;});}throw new Error('Network response not ok');}).catch(error => {console.error('Fetch failed:', error);throw error;});}));
});
  • 开始:Service Worker监听到fetch事件。
  • 缓存中是否存在请求资源:检查缓存中是否有匹配的请求资源。
  • 从缓存返回资源:如果缓存中有匹配资源,直接返回该资源。
  • 发起网络请求:如果缓存中没有匹配资源,则发起网络请求。
  • 网络请求是否成功:检查网络请求是否成功。
  • 响应状态是否为OK:检查网络响应的状态码是否为200(OK)。
  • 缓存新响应:如果网络请求成功且响应状态为OK,则将响应缓存。
  • 抛出错误:如果响应状态不是OK,则抛出错误。
  • 捕获错误并抛出:如果网络请求失败,则捕获错误并抛出。
  • 结束:流程结束。

Service Worker资源缓存-插件自动生成

通过上述资料可知资料缓存需要配置缓存ID和所需要的缓存文件路径,而每次打包的文件名都是混淆之后的,人工写是非常不实际,所以我们可以通过插件帮我们自动生成Service Worker文件自动插入到dist目录下

配置插件进行自动化生产

在vite项目中,根据Rollup接口提供的writeBundle()钩子函数拿到构建后的文件列表,自动生成service-worker.js

import path from 'path'
import * as fs from 'fs'
import * as crypto from 'crypto'// 定义插件选项类型
interface ManifestPluginOptions {outputPath: stringversion?: stringserviceWorkerFileName?: string
}export default function ServiceWorkerManifestPlugin(options: ManifestPluginOptions) {const { outputPath, version, serviceWorkerFileName } = options// 生成随机版本号const generateRandomVersion = (): string => {return crypto.randomBytes(8).toString('hex')}const manifestVersion = version || generateRandomVersion()// 使用默认的 service-worker.js 文件名,如果没有传入自定义文件名const serviceWorkerPath = `/${serviceWorkerFileName || 'service-worker.js'}`// 递归遍历目录并获取所有文件路径const getAllFiles = (dirPath: string, relativePath: string = ''): string[] => {let files: string[] = []const entries = fs.readdirSync(dirPath, { withFileTypes: true })for (const entry of entries) {const fullPath = path.join(dirPath, entry.name)const relativeFullPath = path.join(relativePath, entry.name)if (entry.isDirectory()) {files = files.concat(getAllFiles(fullPath, relativeFullPath))} else {files.push(`/${relativeFullPath}`)}}return files}// 生成 service-worker.js 文件内容const generateServiceWorkerContent = (cachedFiles: string[], manifestVersion: string): string => {return `
self.addEventListener('install', event => {event.waitUntil(caches.open('${manifestVersion}').then(cache => {return cache.addAll([${cachedFiles.map((file) => `'${file}'`).join(',\n')}]);}));
});//Service Worker监听到fetch事件。
self.addEventListener('fetch', event => {event.respondWith(// 缓存中是否存在请求资源:检查缓存中是否有匹配的请求资源。caches.match(event.request).then(response => {// 从缓存返回资源:如果缓存中有匹配资源,直接返回该资源。if (response) {console.log('Serving from cache:', event.request.url);return response;}console.log('Fetching from network:', event.request.url);// 发起网络请求:如果缓存中没有匹配资源,则发起网络请求。return fetch(event.request).then(networkResponse => {// 缓存新资源:如果网络请求成功,则将新资源缓存。if (networkResponse && networkResponse.ok) {console.log('Caching new response:', event.request.url);// 缓存新响应:如果网络请求成功且响应状态为OK,则将响应缓存。return caches.open('${manifestVersion}').then(cache => {cache.put(event.request, networkResponse.clone());return networkResponse;});}// 如果响应状态不是OK,则抛出错误。throw new Error('Network response not ok');}).catch(error => {console.error('Fetch failed:', error);throw error;});}));
});self.addEventListener('activate', event => {const cacheWhitelist = ['${manifestVersion}'];event.waitUntil(caches.keys().then(cacheNames => {return Promise.all(cacheNames.map(cacheName => {if (cacheWhitelist.indexOf(cacheName) === -1) {return caches.delete(cacheName);}}));}));
});
`}// 修改 index.html 文件const modifyIndexHtml = (indexPath: string, serviceWorkerPath: string): void => {try {let indexContent = fs.readFileSync(indexPath, 'utf-8')// 确保 <html> 标签存在if (indexContent.includes('<html')) {// 添加 Service Worker 注册脚本const serviceWorkerRegistrationScript = `
<script>if ('serviceWorker' in navigator) {// 浏览器支持 Service Workerwindow.addEventListener('load', () => {navigator.serviceWorker.register('${serviceWorkerPath}').then(registration => {console.log('Service Worker registered with scope:', registration.scope);}).catch(error => {console.error('Service Worker registration failed:', error);});});} else {console.log('Service Worker not supported');}
</script>
`// 将脚本插入到 </head> 标签之前indexContent = indexContent.replace('</head>', `${serviceWorkerRegistrationScript}</head>`)fs.writeFileSync(indexPath, indexContent)console.log('index.html modified successfully.')} else {console.warn('index.html does not contain a <html> tag.')}} catch (error) {console.error('Failed to modify index.html:', error)}}return {name: 'manifest-plugin', // 必须的,将会在 warning 和 error 中显示writeBundle() {try {const cachedFiles = getAllFiles(outputPath)// 确保 service-worker.js 也被缓存if (!cachedFiles.includes(serviceWorkerPath)) {cachedFiles.push(serviceWorkerPath)}// 生成 service-worker.js 文件内容const serviceWorkerContent = generateServiceWorkerContent(cachedFiles, manifestVersion)// 写入 service-worker.js 文件const serviceWorkerFilePath = path.join(outputPath, serviceWorkerPath.replace(/^\//, ''))fs.writeFileSync(serviceWorkerFilePath, serviceWorkerContent)console.log('service-worker.js generated successfully.')// 修改 index.html 文件const indexPath = path.join(outputPath, 'index.html')if (fs.existsSync(indexPath)) {modifyIndexHtml(indexPath, serviceWorkerPath)} else {console.warn('index.html not found in the output directory.')}} catch (error) {console.error('Failed to write bundle:', error)}},}
}

Service Worker调试与监控

使用Chrome DevTools

  • 查看缓存:在“Application”面板中查看当前注册的Service Workers及其缓存内容。
  • 模拟离线:通过DevTools的“Network”面板模拟不同的网络状况,测试应用的离线表现。
  • 日志记录:利用console.log()配合DevTools的日志功能追踪Service Worker内部发生的事件及执行过程。

通过Chrome DevTools可看到我们的文件被正确的缓存,且通过Application工具管理我们的Service Workers
请添加图片描述
请添加图片描述

http://www.lryc.cn/news/521903.html

相关文章:

  • Unity 语音转文字 Vosk 离线库
  • VSCode连接Github的重重困难及解决方案!
  • 《AI赋能鸿蒙Next,打造极致沉浸感游戏》
  • 小白:react antd 搭建框架关于 RangePicker DatePicker 时间组件使用记录 2
  • <C++学习>C++ std 多线程教程
  • 用 Python 自动化处理日常任务
  • 《深入浅出HTTPS​​​​​​​​​​​​​​​​​》读书笔记(28):DSA数字签名
  • type 属性的用途和实现方式(图标,表单,数据可视化,自定义组件)
  • PSINS工具箱学习(四)捷联惯导更新算法
  • P1Linux和Docker常用终端命令:保姆级图文详解
  • Windows重装后NI板卡LabVIEW恢复正常
  • 深度解析统计学四大分布:Z、卡方、t 与 F 的关联与应用
  • zkServer.sh脚本
  • CV(10)--目标检测
  • UML系列之Rational Rose笔记七:状态图
  • C++单例模式的设计
  • 基于springboot的自习室预订系统
  • shell笔记
  • 《鸿蒙Next微内核:解锁人工智能决策树并行计算的加速密码》
  • AI刷题-最大矩形面积问题、小M的数组变换
  • Redis集群部署详解:主从复制、Sentinel哨兵模式与Cluster集群的工作原理与配置
  • LeetCode热题100(三十四) —— 23.合并K个升序链表
  • kalilinux - 目录扫描之dirsearch
  • 浅谈云计算04 | 云基础设施机制
  • 文件上传 分片上传
  • 【0391】Postgres内核 checkpointer process ① 启动初始化
  • 链路追踪SkyWalking
  • Uniapp判断设备是安卓还是 iOS,并调用不同的方法
  • 计算机网络 (42)远程终端协议TELNET
  • rtthread学习笔记系列-- 23 环形缓冲块 ringblock