Service Work离线体验与性能优化

打印 上一主题 下一主题

主题 829|帖子 829|积分 2487

Service Work离线体验与性能优化

一、什么是 Service Worker?

Service Worker是一种可编程的网络代理,允许你拦截和处理惩罚应用发出的全部网络请求,包括拦截和处理惩罚网络请求、管理缓存和处理惩罚推送关照等。它使得开辟者能够在客户端实现强盛的新功能,比如离线支持、推送关照以及缓存管理。Service Worker 运行在独立于主欣赏器进程的背景下,因此不会影响页面的性能。它还能够为网页或应用程序提供雷同于本地应用的特性,它的重要目的是增强 Web 应用的离线体验和性能。
二、 Service Worker的应用场景


  • 离线支持:通过缓存静态资源和动态内容,确保应用在没有网络连接时仍然可以使用
  • 缓存管理:提高应用性能,通过缓存减少网络请求次数和加快页面加载速率。
  • 推送关照: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的简朴素践


  • 欣赏器兼容性检查
   首先,在开始使用 Service Worker 之前,你必要确保用户的欣赏器支持这项技术。Service Worker 对象存在于navigator对象下,可以通过以下代码来检查
  1. if ('serviceWorker' in navigator) {
  2.   // 浏览器支持 Service Worker
  3.   
  4. } else {
  5.   console.log('Service Worker not supported');
  6. }
复制代码

  • 注册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。
  1. if ('serviceWorker' in navigator) {
  2.   // 浏览器支持 Service Worker
  3.    window.addEventListener('load', () => {
  4.      navigator.serviceWorker
  5.           .register('/service-worker.js')
  6.           .then((registration) => {
  7.             console.log('Service Worker registered with scope:', registration.scope)
  8.           })
  9.           .catch((error) => {
  10.             console.error('Service Worker registration failed:', error)
  11.           })
  12. })
  13. } else {
  14.   console.log('Service Worker not supported');
  15. }
复制代码
  此代码在主线程上运行,并执行以下利用:
1、由于用户初次访问网站发生在没有注册的 Service Worker 的情况下, 等到页面完全加载后再注册一个。 这样可以在 Service Worker 预缓存任何内容时避免带宽争用。
2、进行快速检查有助于避免在不支持此功能的欣赏器出现错误。
3、当页面完全加载且支持 Service Worker 时,注册 /service-worker.js。
  

  • Service Worker-安装(Installing)
   安装变乱发生在 Service Worker 初次安装时,每个 Service Worker 仅调用一次 install,并且在更新之前不会再次触发。
  1. self.addEventListener('install', event => {
  2.   event.waitUntil(
  3.     caches.open('缓存版本ID').then(cache => {
  4.       return cache.addAll([
  5.         '/',
  6.                   ...
  7.       ]);
  8.     })
  9.   );
  10. });
复制代码
  此代码会创建一个新的 Cache 实例并预缓存资产。
此处重点关注:event.waitUntil变乱
event.waitUntil 接受 promise; 并等候该 promise 得到办理。 该 promise 执行两项异步利用:
创建名为 ‘缓存版本ID’ 的新 Cache 实例。
创建缓存后 资源网址数组使用其异步缓存资源预缓存 addAll 方法。
如果传递给 event.waitUntil 的 promise 已拒绝。 如果发生这种情况,Service Worker 会被丢弃。
  

  • Service Worker-激活
   如果注册和安装成功, Service Worker 激活,并且其状态变为 ‘activating’ ,你可以在这里执行清理旧版本资源的利用:
  1. self.addEventListener('activate', event => {
  2.   const cacheWhitelist = ['缓存版本ID'];
  3.   event.waitUntil(
  4.     caches.keys().then(cacheNames => {
  5.       return Promise.all(
  6.         cacheNames.map(cacheName => {
  7.           if (cacheWhitelist.indexOf(cacheName) === -1) {
  8.             return caches.delete(cacheName);
  9.           }
  10.         })
  11.       );
  12.     })
  13.   );
  14. });
复制代码

  • Service Worker-捕获 Fetch
   通过监听Service Worker的 fetch 变乱来拦截网络请求,
调用 event 上的 respondWith() 方法来劫持当前servicework控制域下的 HTTP 请求,该方法会直接返回一个Promise 效果 ,这个效果就会是http请求的响应。上面代码中就一个简朴的逻辑,先劫持http请求,然后看看缓存中是否有这个请求的资源,如果有则直接返回,如果没有就去请求服务器上的资源。 event.respondWith 方法只能在 Service Worker 的 fetch 变乱中使用。
  1. self.addEventListener('fetch', event => {
  2.   event.respondWith(
  3.     caches.match(event.request).then(response => {
  4.       if (response) {
  5.         console.log('Serving from cache:', event.request.url);
  6.         return response;
  7.       }
  8.       console.log('Fetching from network:', event.request.url);
  9.       return fetch(event.request).then(networkResponse => {
  10.         if (networkResponse && networkResponse.ok) {
  11.           console.log('Caching new response:', event.request.url);
  12.           return caches.open('f69905188ac970f1').then(cache => {
  13.             cache.put(event.request, networkResponse.clone());
  14.             return networkResponse;
  15.           });
  16.         }
  17.         throw new Error('Network response not ok');
  18.       }).catch(error => {
  19.         console.error('Fetch failed:', error);
  20.         throw error;
  21.       });
  22.     })
  23.   );
  24. });
复制代码
  

  • 开始:Service Worker监听到fetch变乱。
  • 缓存中是否存在请求资源:检查缓存中是否有匹配的请求资源。
  • 从缓存返回资源:如果缓存中有匹配资源,直接返回该资源。
  • 发起网络请求:如果缓存中没有匹配资源,则发起网络请求。
  • 网络请求是否成功:检查网络请求是否成功。
  • 响应状态是否为OK:检查网络响应的状态码是否为200(OK)。
  • 缓存新响应:如果网络请求成功且响应状态为OK,则将响应缓存。
  • 抛堕落误:如果响应状态不是OK,则抛堕落误。
  • 捕获错误并抛出:如果网络请求失败,则捕获错误并抛出。
  • 结束:流程结束。
  七、Service Worker资源缓存-插件自动生成

   通过上述资料可知资料缓存必要设置缓存ID和所必要的缓存文件路径,而每次打包的文件名都是混淆之后的,人工写是非常不实际,以是我们可以通过插件帮我们自动生成Service Worker文件自动插入到dist目次下
    此处我是采用vite提供的插件钩子函数拿到构建后的文件列表,自动生成service-worker.js
  1. import path from 'path'
  2. import * as fs from 'fs'
  3. import * as crypto from 'crypto'
  4. // 定义插件选项类型
  5. interface ManifestPluginOptions {
  6.   outputPath: string
  7.   version?: string
  8.   serviceWorkerFileName?: string
  9. }
  10. export default function ServiceWorkerManifestPlugin(options: ManifestPluginOptions) {
  11.   const { outputPath, version, serviceWorkerFileName } = options
  12.   // 生成随机版本号
  13.   const generateRandomVersion = (): string => {
  14.     return crypto.randomBytes(8).toString('hex')
  15.   }
  16.   const manifestVersion = version || generateRandomVersion()
  17.   // 使用默认的 service-worker.js 文件名,如果没有传入自定义文件名
  18.   const serviceWorkerPath = `/${serviceWorkerFileName || 'service-worker.js'}`
  19.   // 递归遍历目录并获取所有文件路径
  20.   const getAllFiles = (dirPath: string, relativePath: string = ''): string[] => {
  21.     let files: string[] = []
  22.     const entries = fs.readdirSync(dirPath, { withFileTypes: true })
  23.     for (const entry of entries) {
  24.       const fullPath = path.join(dirPath, entry.name)
  25.       const relativeFullPath = path.join(relativePath, entry.name)
  26.       if (entry.isDirectory()) {
  27.         files = files.concat(getAllFiles(fullPath, relativeFullPath))
  28.       } else {
  29.         files.push(`/${relativeFullPath}`)
  30.       }
  31.     }
  32.     return files
  33.   }
  34.   // 生成 service-worker.js 文件内容
  35.   const generateServiceWorkerContent = (cachedFiles: string[], manifestVersion: string): string => {
  36.     return `
  37. self.addEventListener('install', event => {
  38.   event.waitUntil(
  39.     caches.open('${manifestVersion}').then(cache => {
  40.       return cache.addAll([
  41.         ${cachedFiles.map((file) => `'${file}'`).join(',\n')}
  42.       ]);
  43.     })
  44.   );
  45. });
  46. //Service Worker监听到fetch事件。
  47. self.addEventListener('fetch', event => {
  48.   event.respondWith(
  49.   // 缓存中是否存在请求资源:检查缓存中是否有匹配的请求资源。
  50.     caches.match(event.request).then(response => {
  51.       // 从缓存返回资源:如果缓存中有匹配资源,直接返回该资源。
  52.       if (response) {
  53.         console.log('Serving from cache:', event.request.url);
  54.         return response;
  55.       }
  56.       console.log('Fetching from network:', event.request.url);
  57.       // 发起网络请求:如果缓存中没有匹配资源,则发起网络请求。
  58.       return fetch(event.request).then(networkResponse => {
  59.         // 缓存新资源:如果网络请求成功,则将新资源缓存。
  60.         if (networkResponse && networkResponse.ok) {
  61.           console.log('Caching new response:', event.request.url);
  62.           // 缓存新响应:如果网络请求成功且响应状态为OK,则将响应缓存。
  63.           return caches.open('${manifestVersion}').then(cache => {
  64.             cache.put(event.request, networkResponse.clone());
  65.             return networkResponse;
  66.           });
  67.         }
  68.         // 如果响应状态不是OK,则抛出错误。
  69.         throw new Error('Network response not ok');
  70.       }).catch(error => {
  71.         console.error('Fetch failed:', error);
  72.         throw error;
  73.       });
  74.     })
  75.   );
  76. });
  77. self.addEventListener('activate', event => {
  78.   const cacheWhitelist = ['${manifestVersion}'];
  79.   event.waitUntil(
  80.     caches.keys().then(cacheNames => {
  81.       return Promise.all(
  82.         cacheNames.map(cacheName => {
  83.           if (cacheWhitelist.indexOf(cacheName) === -1) {
  84.             return caches.delete(cacheName);
  85.           }
  86.         })
  87.       );
  88.     })
  89.   );
  90. });
  91. `
  92.   }
  93.   // 修改 index.html 文件
  94.   const modifyIndexHtml = (indexPath: string, serviceWorkerPath: string): void => {
  95.     try {
  96.       let indexContent = fs.readFileSync(indexPath, 'utf-8')
  97.       // 确保 <html> 标签存在
  98.       if (indexContent.includes('<html')) {
  99.         // 添加 Service Worker 注册脚本
  100.         const serviceWorkerRegistrationScript = `
  101. <script>
  102.   if ('serviceWorker' in navigator) {
  103.     // 浏览器支持 Service Worker
  104.     window.addEventListener('load', () => {
  105.       navigator.serviceWorker.register('${serviceWorkerPath}')
  106.         .then(registration => {
  107.           console.log('Service Worker registered with scope:', registration.scope);
  108.         })
  109.         .catch(error => {
  110.           console.error('Service Worker registration failed:', error);
  111.         });
  112.     });
  113.   } else {
  114.     console.log('Service Worker not supported');
  115.   }
  116. </script>
  117. `
  118.         // 将脚本插入到 </head> 标签之前
  119.         indexContent = indexContent.replace('</head>', `${serviceWorkerRegistrationScript}</head>`)
  120.         fs.writeFileSync(indexPath, indexContent)
  121.         console.log('index.html modified successfully.')
  122.       } else {
  123.         console.warn('index.html does not contain a <html> tag.')
  124.       }
  125.     } catch (error) {
  126.       console.error('Failed to modify index.html:', error)
  127.     }
  128.   }
  129.   return {
  130.     name: 'manifest-plugin', // 必须的,将会在 warning 和 error 中显示
  131.     writeBundle() {
  132.       try {
  133.         const cachedFiles = getAllFiles(outputPath)
  134.         // 确保 service-worker.js 也被缓存
  135.         if (!cachedFiles.includes(serviceWorkerPath)) {
  136.           cachedFiles.push(serviceWorkerPath)
  137.         }
  138.         // 生成 service-worker.js 文件内容
  139.         const serviceWorkerContent = generateServiceWorkerContent(cachedFiles, manifestVersion)
  140.         // 写入 service-worker.js 文件
  141.         const serviceWorkerFilePath = path.join(outputPath, serviceWorkerPath.replace(/^\//, ''))
  142.         fs.writeFileSync(serviceWorkerFilePath, serviceWorkerContent)
  143.         console.log('service-worker.js generated successfully.')
  144.         // 修改 index.html 文件
  145.         const indexPath = path.join(outputPath, 'index.html')
  146.         if (fs.existsSync(indexPath)) {
  147.           modifyIndexHtml(indexPath, serviceWorkerPath)
  148.         } else {
  149.           console.warn('index.html not found in the output directory.')
  150.         }
  151.       } catch (error) {
  152.         console.error('Failed to write bundle:', error)
  153.       }
  154.     },
  155.   }
  156. }
复制代码
八、Service Worker调试与监控

   通过Chrome DevTools可看到我们的文件被正确的缓存,且通过Application工具管理我们的Service Workers



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

梦见你的名字

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表