Skip to content

更新问题

生命周期
  1. Installing

    • 行为

      • 开发者通常在 install事件中预缓存关键静态资源(如 HTML、CSS、JS)。

      • 使用 event.waitUntil()确保安装完成前不进入下一阶段。若 Promise 被拒绝,安装失败,Service Worker 直接变为 redundant状态

    • ​跳过等待​​:通过 registration.installing属性访问正在安装的 Service Worker

  2. Installed/Waiting

    安装成功后进入 installed(或 waiting)状态。

    • 行为

      • 新 Service Worker 等待旧版本控制的页面全部关闭后才会激活

      • 可通过 registration.waiting访问等待中的 Service Worker

    • ​跳过等待​​:调用 self.skipWaiting()可强制立即激活,但需谨慎处理缓存冲突

  3. Activating

    • 激活条件

      • 旧 Service Worker 控制的页面全部关闭。

      • 调用 skipWaiting()或超时机制触发。

    • 核心事件​​:activate事件,通常用于清理旧缓存(如删除过期的 CacheStorage内容)

    • ​​状态检查​​:通过 registration.active访问已激活的 Service Worker

  4. ​​运行阶段(Activated)

    • 功能

      • 控制页面并处理 fetch(网络请求拦截)、push(推送通知)等事件。

      • 默认仅控制激活后新打开的页面。调用 clients.claim()可立即控制所有同源页面

    • ​​终止​​:若长时间无事件处理,浏览器可能终止 Service Worker 以节省资源。

当新旧 sw 存在的时候,如果采用了 self.skipWaiting(),那么页面可能在遇到某些缓存不匹配问题的时候,会出现意外错误; 正常的更新流程:所有的旧受控页面关闭,新的 sw 将会进行接管

需要提示更新的时候:

方法1(比较果断,不考虑用户行为):

  • 页面 js 中监听 controllerchange, 当版本替换的时候,执行 location.reload()

  • sw 进行 skipWaiting 会触发 controllerchange

方法2(询问):

js
function emitUpdate() {
    var event = document.createEvent('Event')
    event.initEvent('sw.update', true, true)
    window.dispatchEvent(event)
}

if ('serviceWorker' in navigator) {
    navigator.serviceWorker
        .register('/service-worker.js')
        .then(function (reg) {
            // 表示已安装但处于等待状态的 Service Worker(即新版本已下载但未激活)。
            if (reg.waiting) {
                emitUpdate()
                return
            }

            // 当检测到新版本的 Service Worker 开始安装时触发
            reg.onupdatefound = function () {
                var installingWorker = reg.installing
                installingWorker.onstatechange = function () {
                    switch (installingWorker.state) {
                        // 新 Worker 安装完成。
                        case 'installed':
                            // 检查当前是否有活跃的 Worker。若存在(即非首次安装),则触发更新事件
                            if (navigator.serviceWorker.controller) {
                                emitUpdate()
                            }
                            break
                    }
                }
            }
        })
        .catch(function (e) {
            console.error('Error during service worker registration:', e)
        })
}

demo

某些资源直接使用 event.request 进行缓存,之后 match 不会被命中(script[type="module"]), 需要改成使用 event.request.url

js
// https://juejin.cn/post/6950988377684443167?searchId=202506142335433B82D94BC578B6A454F2#heading-10
const SW_VERSION = '0.0.1'
// 需要缓存的资源后缀
const CACHE_SUFFIX = ['js', 'css']
// 一开始就需要加载的资源
const INITIAL_SOURCES = [
    '/', // 取消缓存,页面直接离线不可用
    // '/sw.js',
]

// e.waitUntil 接收一个 promise,promise 完成,那么这个事件就完成,install=》activate
// 保证 sw 能够正常运行(加载完必需的资源),避免浏览器关闭等事件导致的加载失败
self.addEventListener('install', (e) => {
    // e.waitUntil(self.skipWaiting()) // 立即激活
    e.waitUntil(
        caches.open(SW_VERSION).then(function (cache) {
            return cache.addAll(INITIAL_SOURCES)
        })
    )
})
// 激活的时候,清除旧 version cache
self.addEventListener('activate', function (event) {
    const keys = caches.keys()

    event.waitUntil(
        keys.then(function (keyList) {
            return Promise.all(
                keyList.filter((it) => it !== SW_VERSION).map((key) => caches.delete(key))
            )
        })
    )
})

// activate 完成,该事件才会生效,因此 activate 需要配合 waitUntil 先清除旧资源,避免提前 match 导致资源不对
self.addEventListener('fetch', function (event) {
    const { method, url } = event.request
    const { pathname } = new URL(url)

    const format = pathname.split('.').at(-1)
    event.respondWith(
        caches.match(url).then(function (response) {
            if (response !== undefined) {
                return response
            }
            return fetch(event.request).then(function (response) {
                if (
                    !response ||
                    url.includes('chrome-extension') ||
                    // ['/', ''].includes(pathname) ||
                    method === 'POST' ||
                    !(url || '').startsWith('http') ||
                    response.status !== 200 ||
                    !CACHE_SUFFIX.includes(format)
                ) {
                    return response
                }
                const clonedRes = response.clone()
                caches.open(SW_VERSION).then(function (cache) {
                    cache.put(url, clonedRes)
                })

                return response
            })
        })
    )
})

// 监听消息
self.addEventListener('message', (event) => {
    if (event.data === 'skipWaiting') {
        self.skipWaiting()
    }
})
ts
// 更新提示
async function updateAlert(): Promise<void> {
    return window.confirm('检测到新版本,立即更新?') ? Promise.resolve(void 0) : Promise.reject()
}

// 自动检测版本
function createIntervalVersionCheck(reg: ServiceWorkerRegistration, debounce = 60e3) {
    setInterval(() => {
        reg.update()
    }, debounce)
}

if ('serviceWorker' in navigator) {
    navigator.serviceWorker
        .register('/sw.js')
        .then(function (reg) {
            // registration worked
            reg.onupdatefound = function () {
                var installingWorker = reg.installing
                if (!installingWorker) return
                installingWorker.onstatechange = function () {
                    switch (installingWorker!.state) {
                        // 新 Worker 安装完成。
                        case 'installed':
                            // 检查当前是否有活跃的 Worker。若存在(即非首次安装),则触发更新事件
                            if (navigator.serviceWorker.controller) {
                                navigator.serviceWorker.getRegistration().then((reg) => {
                                    if (!reg || !reg.waiting) return
                                    await updateAlert()
                                    reg.waiting.postMessage('skipWaiting')
                                })
                            }
                            break
                    }
                }
            }
            createIntervalVersionCheck(reg)
        })
        .catch(function (error) {
            // registration failed
            console.log('Registration failed with ' + error)
        })
    // 监听版本切换,进行刷新
    navigator.serviceWorker.addEventListener('controllerchange', () => {
        window.location.reload()
    })
}