Service Worker 运用与实践

359 阅读3分钟

一、前言

Service Worker本质上也是浏览器缓存资源用的,只不过他不仅仅是Cache,也是通过worker的方式来进一步优化。

他基于h5的web worker,所以绝对不会阻碍当前js线程的执行,sw最重要的工作原理就是:

1、后台线程:独立于当前网页线程;

2、网络代理:在网页发起请求时代理,来缓存文件。

二、调试方法

被Service Worker缓存的文件,可以在Network中看到Size项为from Service Worker:

也可以在Application的Cache Storage中查看缓存的具体内容:

如果是具体的断点调试,需要使用对应的线程,不再是main线程了,这也是webworker的通用调试方法:

三、使用条件

sw 是基于 HTTPS 的,因为Service Worker中涉及到请求拦截,所以必须使用HTTPS协议来保障安全。如果是本地调试的话,localhost是可以的。

而我们刚好全站强制https化,所以正好可以使用。

四、生命周期

注册

"serviceWorker" in navigator && window.addEventListener("load",
    function() {
        var e = location.pathname.match(/\/news\/[a-z]{1,}\//)[0] + "article-sw.js?v=08494f887a520e6455fa";
        navigator.serviceWorker.register(e).then(function(n) {
            n.onupdatefound = function() {
                var e = n.installing;
                e.onstatechange = function() {
                    switch (e.state) {
                        case "installed":
                            navigator.serviceWorker.controller ?
                              console.log("New or updated content is available.") :
                              console.log("Content is now available offline!");
                            break;
                        case "redundant":
                            console.error("The installing service worker became redundant.")
                    }
                }
            }
        }).
        catch(function(e) {
            console.error("Error during service worker registration:", e)
        })
    })

installing

//service worker安装成功后开始缓存所需的资源
var CACHE_PREFIX = 'cms-sw-cache';
var CACHE_VERSION = '0.0.20';
var CACHE_NAME = CACHE_PREFIX+'-'+CACHE_VERSION;
var allAssets = [    './main.css'];
self.addEventListener('install', function(event) {
    //调试时跳过等待过程
    self.skipWaiting();
    // Perform install steps
    //首先 event.waitUntil 你可以理解为 new Promise,
    //它接受的实际参数只能是一个 promise,因为,caches 和 cache.addAll 返回的都是 Promise,
    //这里就是一个串行的异步加载,当所有加载都成功时,那么 SW 就可以下一步。
    //另外,event.waitUntil 还有另外一个重要好处,它可以用来延长一个事件作用的时间,
    //这里特别针对于我们 SW 来说,比如我们使用 caches.open 是用来打开指定的缓存,但开启的时候,
    //并不是一下就能调用成功,也有可能有一定延迟,由于系统会随时睡眠 SW,所以,为了防止执行中断,
    //就需要使用 event.waitUntil 进行捕获。另外,event.waitUntil 会监听所有的异步 promise
    //如果其中一个 promise 是 reject 状态,那么该次 event 是失败的。这就导致,我们的 SW 开启失败。
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(function(cache) {
                console.log('[SW]: Opened cache');
                return cache.addAll(allAssets);
            })
    );
});

activated

activated阶段可以做很多有意义的事情,比如更新存储在Cache中的key和value:

var CACHE_PREFIX = 'cms-sw-cache';var CACHE_VERSION = '0.0.20';
/**
 * 找出对应的其他key并进行删除操作
 * @returns {*}
 */
function deleteOldCaches() {
    return caches.keys().then(function (keys) {
        var all = keys.map(function (key) {
            if (key.indexOf(CACHE_PREFIX) !== -1 && key.indexOf(CACHE_VERSION) === -1){
                console.log('[SW]: Delete cache:' + key);
                return caches.delete(key);
            }
        });
        return Promise.all(all);
    });
}
//sw激活阶段,说明上一sw已失效
self.addEventListener('activate', function(event) {
    event.waitUntil(
        // 遍历 caches 里所有缓存的 keys 值
        caches.keys().then(deleteOldCaches)
    );
});

idle

这个空闲状态一般是不可见的,这种一般说明sw的事情都处理完毕了,然后处于闲置状态了。

浏览器会周期性的轮询,去释放处于idle的sw占用的资源。

fetch

该阶段是sw最为关键的一个阶段,用于拦截代理所有指定的请求,并进行对应的操作。

所有的缓存部分,都是在该阶段,这里举一个简单的例子:

//监听浏览器的所有fetch请求,对已经缓存的资源使用本地缓存回复
self.addEventListener('fetch', function(event) {
    event.respondWith(
        caches.match(event.request)
            .then(function(response) {
                //该fetch请求已经缓存
                if (response) {
                    return response;
                }
                return fetch(event.request);
             })
    );
});

五、缓存未更新问题解决

1、 什么是Service Worker缓存?

 Service Worker缓存主要通过Cache API实现,允许开发者在客户端存储网络资源(如HTML、CSS、JavaScript、图片等)。缓存的资源可以在离线状态下提供访问,或在网络状况不佳时提升加载速度。缓存策略的选择直接影响资源的更新与获取方式。 

2、常见的缓存策略

Cache First:优先从缓存中获取资源,若缓存不存在则从网络获取并缓存。

Network First:优先从网络获取资源,若网络请求失败则从缓存中获取。

Stale-While-Revalidate:立即从缓存中提供资源,同时在后台更新缓存。

Cache Only:仅使用缓存。 

Network Only:仅使用网络。

3、Service Worker 缓存未更新的常见原因 

3.1 缓存版本管理不当

 未能有效管理缓存版本,导致旧的缓存持续存在,新资源未被缓存更新。

 3.2 Service Worker 更新流程不正确

 Service Worker的生命周期包括安装(install)、激活(activate)和事件监听。如果未正确处理这些阶段,可能导致新版本的Service Worker无法激活,从而旧缓存未更新。

 3.3 缓存策略选择不当

 选择了不适合的缓存策略,导致新资源未被及时缓存。例如,使用了Cache First策略而忽略了网络更新。

 3.4 缓存清理不彻底 

未能在激活阶段清理旧缓存,导致新资源无法正确覆盖旧缓存。

 3.5 浏览器缓存干扰

 浏览器自身的缓存机制可能与Service Worker的缓存策略冲突,导致资源未更新。

 3.6 资源请求路径问题

请求资源的URL路径发生变化或不一致,导致Service Worker无法正确匹配和更新缓存。

 3.7 缓存API使用错误

在Service Worker中错误地使用了Cache API,如未正确打开缓存、添加或删除资源。

4、如何检测缓存未更新的问题 

4.1 使用浏览器开发者工具

 大多数现代浏览器(如Chrome、Firefox、Edge)提供了强大的开发者工具,帮助开发者检测和调试Service Worker及其缓存。

 步骤:

 打开开发者工具:按F12或右键选择“检查”。

 切换到“Application”(应用)面板: 在Chrome中,选择“Application” > “Service Workers”。 在Firefox中,选择“Storage” > “Service Workers”。

 检查Service Worker状态: 确认Service Worker是否已注册并处于激活状态。 查看是否有新版本的Service Worker等待激活。

查看缓存内容: 在“Cache Storage”下,查看各个缓存的名称和存储的资源。 检查是否有最新的资源已被缓存。

 监控网络请求: 切换到“Network”(网络)面板,观察资源加载来源(缓存或网络)。 强制刷新(Shift + F5)并观察Service Worker如何处理请求。

 4.2 添加日志输出

 在Service Worker的各个生命周期事件和缓存操作中添加console.log语句,帮助跟踪执行流程和资源处理情况。

 示例:

self.addEventListener('install', event => {
  console.log('[Service Worker] Install Event');
  // 其他安装逻辑
});

self.addEventListener('activate', event => {
  console.log('[Service Worker] Activate Event');
  // 其他激活逻辑
});

self.addEventListener('fetch', event => {
  console.log(`[Service Worker] Fetch Event for: ${event.request.url}`);
  // 其他抓取逻辑
});

4.3 使用断点调试

 在Service Worker的脚本中设置断点,逐步执行代码,检查缓存更新过程中的变量和状态。

 步骤: 

 在开发者工具中打开Service Worker脚本。

 **设置断点:**点击行号设置断点。

触发事件:如刷新页面,触发安装或激活事件。

**逐步执行:**使用调试工具逐步执行,观察变量和缓存状态的变化。

5、解决Service Worker 缓存未更新的方法

 5.1 正确管理缓存版本

 通过为缓存命名添加版本号,确保在更新Service Worker时清理旧缓存,缓存新资源。

 示例: 

const CACHE_NAME = 'my-app-cache-v2';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/script/main.js',
  '/images/logo.png'
];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('[Service Worker] Caching all: app shell and content');
        return cache.addAll(urlsToCache);
      })
  );
});

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys()
      .then(cacheNames => {
        return Promise.all(
          cacheNames.map(cache => {
            if (cache !== CACHE_NAME) {
              console.log('[Service Worker] Deleting old cache:', cache);
              return caches.delete(cache);
            }
          })
        );
      })
  );
});

解释:

**缓存命名:**CACHE_NAME包含版本号,便于管理不同版本的缓存。

**安装事件:**在安装阶段打开并缓存指定资源。

**激活事件:**在激活阶段清理旧版本的缓存,仅保留当前版本的缓存。

 5.2 实现适当的缓存策略

 根据不同资源的特性选择合适的缓存策略,确保新资源能够及时更新。

示例:Cache First 策略

 适用于不经常变化的资源,如图标、样式表等。 

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        if (response) {
          return response; // 从缓存中返回资源
        }
        return fetch(event.request)
          .then(networkResponse => {
            return caches.open(CACHE_NAME)
              .then(cache => {
                cache.put(event.request, networkResponse.clone());
                return networkResponse;
              });
          });
      })
  );
});

示例:Network First 策略

适用于经常更新的数据,如API请求。

self.addEventListener('fetch', event => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(
      fetch(event.request)
        .then(networkResponse => {
          return caches.open(CACHE_NAME)
            .then(cache => {
              cache.put(event.request, networkResponse.clone());
              return networkResponse;
            });
        })
        .catch(() => {
          return caches.match(event.request);
        })
    );
  } else {
    // 其他资源使用不同的策略
  }
});

5.3 使用skipWaiting和clients.claim

确保新版本的Service Worker能够立即激活,并控制所有客户端,从而实现缓存的即时更新。

示例:

self.addEventListener('install', event => {
  self.skipWaiting(); // 强制等待中的Service Worker立即激活
});

self.addEventListener('activate', event => {
  event.waitUntil(
    self.clients.claim() // 使新的Service Worker立即控制所有页面
  );
});

注意: 使用skipWaitingclients.claim需要谨慎,确保不会破坏用户的当前会话或导致未保存的数据丢失。

5.4 清理旧缓存

在激活阶段,删除所有不再需要的旧缓存,避免缓存膨胀和资源冲突。

示例:

const CACHE_VERSION = 'v2';
const CURRENT_CACHES = {
  main: `my-app-cache-${CACHE_VERSION}`
};

self.addEventListener('activate', event => {
  const expectedCacheNames = Object.values(CURRENT_CACHES);
  event.waitUntil(
    caches.keys()
      .then(cacheNames => {
        return Promise.all(
          cacheNames.map(cacheName => {
            if (!expectedCacheNames.includes(cacheName)) {
              console.log('[Service Worker] Deleting old cache:', cacheName);
              return caches.delete(cacheName);
            }
          })
        );
      })
  );
});

5.5 实现缓存更新通知

通过向客户端发送消息,通知用户有新内容可用,并提示用户刷新页面以获取最新资源。

示例:

self.addEventListener('activate', event => {
  event.waitUntil(
    (async () => {
      // 清理缓存逻辑
      // ...
      // 通知客户端有更新
      const clientsList = await self.clients.matchAll();
      clientsList.forEach(client => {
        client.postMessage({ type: 'SW_UPDATED' });
      });
    })()
  );
});

在客户端监听消息:

navigator.serviceWorker.addEventListener('message', event => {
  if (event.data.type === 'SW_UPDATED') {
    // 提示用户刷新页面
    alert('有新版本可用,请刷新页面以获取最新内容。');
  }
});

5.6 使用工具和库辅助管理Service Worker

利用成熟的工具和库,如Workbox,可以简化Service Worker的配置和管理,自动处理缓存更新和版本管理。

示例:使用Workbox生成Service Worker

安装Workbox CLI:

npm install workbox-cli --global

生成Service Worker配置文件:

workbox wizard

构建和部署:

workbox generateSW workbox-config.js

Workbox示例配置(workbox-config.js):

module.exports = {
  "globDirectory": "dist/",
  "globPatterns": [
    "**/*.{html,js,css,png,jpg}"
  ],
  "swDest": "dist/service-worker.js",
  "clientsClaim": true,
  "skipWaiting": true,
  "runtimeCaching": [{
    "urlPattern": /\/api\//,
    "handler": "NetworkFirst",
    "options": {
      "cacheName": "api-cache",
      "networkTimeoutSeconds": 10,
      "expiration": {
        "maxEntries": 50,
        "maxAgeSeconds": 300
      },
      "cacheableResponse": {
        "statuses": [0, 200]
      }
    }
  }]
};

优点:

  • 自动处理缓存版本管理。
  • 提供丰富的缓存策略选项。
  • 减少手动配置和维护的工作量。

6、最佳实践

6.1 明确缓存策略

根据资源的特性和更新频率,选择合适的缓存策略。将不常变化的静态资源使用Cache First策略,将频繁变化的数据使用Network First策略。

6.2 版本化缓存

通过为缓存命名添加版本号,确保在Service Worker更新时清理旧缓存,避免资源冲突和缓存污染。

6.3 及时更新Service Worker

确保在部署新版本应用时,Service Worker能够及时更新并激活,推送最新的资源到客户端

6.4 提供用户反馈

在Service Worker更新或缓存更新时,向用户提供明确的反馈和操作提示,如刷新页面以获取最新内容。

6.5 使用工具简化管理

利用Workbox等工具,自动化Service Worker的配置和管理,减少手动操作和配置错误的可能性。

6.6 定期清理缓存

避免缓存膨胀和资源冗余,定期清理不再需要的缓存,保持缓存的高效和整洁。

6.7 监控和记录

在生产环境中,使用错误监控和性能分析工具(如Sentry、LogRocket),实时监控Service Worker的运行状态和缓存行为,及时发现和解决问题。

7、实战案例

7.1 React应用中Service Worker缓存未更新的解决方案

场景:

在一个使用Create React App构建的React应用中,部署新版本后,用户仍然加载旧的静态资源,导致功能或样式未更新。

问题原因:

  • Service Worker未正确激活新版本。
  • 缓存策略导致旧资源被优先加载。
  • 缓存版本管理不当。

解决方案步骤:

检查Service Worker配置:

确认Service Worker在src/index.js中正确注册。

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// 注册Service Worker
serviceWorkerRegistration.register({
  onUpdate: registration => {
    if (window.confirm("新版本可用,是否刷新页面?")) {
      registration.waiting.postMessage({ type: 'SKIP_WAITING' });
      window.location.reload();
    }
  }
});

更新Service Worker脚本:

修改public/service-worker.js或相关配置,确保新版本的Service Worker能够跳过等待并立即激活。

self.addEventListener('install', event => {
  self.skipWaiting();
});

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cache => {
          if (cache !== CURRENT_CACHE_NAME) {
            return caches.delete(cache);
          }
        })
      );
    })
  );
  self.clients.claim();
});

更新缓存版本:

在Service Worker中,更新缓存名称,确保旧缓存被清理,新缓存被创建。

const CACHE_NAME = 'my-app-cache-v3'; // 更新版本号
const urlsToCache = [
  '/',
  '/index.html',
  '/static/js/main.js',
  '/static/css/main.css',
  // 其他资源
];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        return cache.addAll(urlsToCache);
      })
  );
});

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cache => {
          if (cache !== CACHE_NAME) {
            return caches.delete(cache);
          }
        })
      );
    })
  );
  self.clients.claim();
});

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        return response || fetch(event.request);
      })
  );
});

部署并测试:

  • 部署新版本应用:确保新的Service Worker脚本和缓存配置已部署。
  • 测试更新流程

在浏览器中打开应用,查看Service Worker的注册状态。

刷新页面,确保新资源被加载。

检查旧缓存是否已被清理,新缓存是否已创建。

  • 处理用户提示:在用户确认更新后,刷新页面获取最新资源。

7.2 使用Workbox管理缓存更新

场景:

在一个复杂的Web应用中,手动管理Service Worker的缓存更新和版本控制较为繁琐,决定使用Workbox简化流程。

解决方案步骤:

1.安装Workbox:

npm install workbox-webpack-plugin --save-dev

2.配置Webpack使用Workbox插件:

webpack.config.js中,添加Workbox的生成Service Worker的配置。

const WorkboxPlugin = require('workbox-webpack-plugin');

module.exports = {
  // 其他Webpack配置
  plugins: [
    // 其他插件
    new WorkboxPlugin.GenerateSW({
      clientsClaim: true,
      skipWaiting: true,
      runtimeCaching: [{
        urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
        handler: 'CacheFirst',
        options: {
          cacheName: 'images',
          expiration: {
            maxEntries: 50,
            maxAgeSeconds: 30 * 24 * 60 * 60, // 30 
          },
        },
      },
      {
        urlPattern: new RegExp('/api/'),
        handler: 'NetworkFirst',
        options: {
          cacheName: 'api-cache',
          networkTimeoutSeconds: 10,
          expiration: {
            maxEntries: 50,
            maxAgeSeconds: 5 * 60, // 5 分钟
          },
          cacheableResponse: {
            statuses: [0, 200],
          },
        },
      }],
    }),
  ],
};

3.构建项目并部署:

npm run build

Workbox将自动生成一个优化的Service Worker脚本,处理缓存策略和更新流程。

4.测试缓存更新:

  • 检查生成的Service Worker:在build目录下查看生成的service-worker.js
  • 验证缓存策略:通过开发者工具检查不同资源的缓存策略是否按预期工作。
  • 模拟更新:修改资源文件,重新构建并部署,确保新资源被正确缓存并替换旧缓存。

优点:

  • 自动处理缓存策略和版本控制。
  • 提供丰富的缓存策略选项,适应不同资源类型。
  • 简化Service Worker的配置和管理。

8、结论

Service Worker 是提升Web应用性能和用户体验的重要工具,合理管理其缓存机制至关重要。然而,由于缓存策略、版本管理和浏览器特性的复杂性,Service Worker缓存未更新的问题在实际开发中较为常见。通过以下关键措施,开发者可以有效地预防和解决缓存未更新的问题:

  1. 正确管理缓存版本:通过为缓存命名添加版本号,确保在Service Worker更新时清理旧缓存,缓存新资源。
  2. 实施合适的缓存策略:根据资源的特性选择合适的缓存策略,如Cache FirstNetwork First等,确保资源的及时更新和高效加载。
  3. 使用skipWaitingclients.claim:确保新版本的Service Worker能够立即激活并控制所有客户端,推动缓存的即时更新。
  4. 提供用户反馈和控制:在Service Worker更新或缓存更新时,向用户提供明确的反馈和操作提示,提升用户体验。
  5. 利用工具和库:使用如Workbox等成熟的工具和库,简化Service Worker的配置和管理,自动处理缓存策略和版本控制。
  6. 定期清理缓存:避免缓存膨胀和资源冗余,定期清理不再需要的缓存,保持缓存的高效和整洁。
  7. 监控和记录:在生产环境中,使用错误监控和性能分析工具,实时监控Service Worker的运行状态和缓存行为,及时发现和解决问题。
  8. 代码审查与测试:通过代码审查和编写单元测试,确保Service Worker和缓存策略在不同浏览器中的一致性和稳定性。

参考:

网易云课堂 Service Worker 运用与实践

Service Worker 缓存未更新的原因与解决方案

Workbox 项目常见问题解决方案

谨慎处理 Service Worker 的更新

Service Worker学习与实践(一)——离线缓存

service worker 实践中遇到的问题

使用 Service Worker 让首页秒开