基于 Service Worker 实现前端应用自动更新方案

背景介绍

在前端应用开发中,当我们部署新版本后,往往需要用户手动刷新页面才能获取最新内容。这种体验并不理想,本文将介绍如何使用 Service Worker 结合版本号实现自动检测更新并提示用户刷新页面的功能。

实现原理

  1. 使用 Service Worker 拦截并缓存应用资源
  2. 通过检测 package.json 中的版本号来判断是否有更新
  3. 在检测到更新时通过消息机制通知页面
  4. 提供用户更新提示,并在用户确认后刷新页面

具体实现

1. Service Worker 实现(sw.js)

在 public 目录下创建 sw.js 文件,主要功能包括:

  • 缓存静态资源
  • 拦截网络请求
  • 定期检查版本更新
  • 向页面发送更新通知

关键代码:

const 复制代码
const CACHE_FILES = [
  '/',
  '/index.html',
  '/package.json'
];

let currentVersion = null;

// 安装 Service Worker
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(CACHE_FILES);
    })
  );
});

// 激活 Service Worker
self.addEventListener('activate', (event) => {
  event.waitUntil(
    Promise.all([
      // 清理旧缓存
      caches.keys().then((cacheNames) => {
        return Promise.all(
          cacheNames
            .filter((name) => name !== CACHE_NAME)
            .map((name) => caches.delete(name))
        );
      }),
      // 立即接管页面
      self.clients.claim()
    ]).then(() => {
      // 激活后立即检查版本
      checkVersion();
    })
  );
});

// 添加请求拦截器
self.addEventListener('fetch', (event) => {
  // 特别处理 package.json 的请求,始终从网络获取最新版本
  if (event.request.url.includes('package.json')) {
    event.respondWith(
      fetch(event.request, {
        cache: 'no-cache',
        headers: {
          'Cache-Control': 'no-cache',
          'Pragma': 'no-cache'
        }
      }).then(response => {
        return response;
      }).catch(error => {
        throw error;
      })
    );
    return;
  }

  event.respondWith(
    caches.match(event.request).then((response) => {
      // 如果在缓存中找到响应,则返回缓存的版本
      if (response) {
        return response;
      }
      // 否则发起网络请求
      return fetch(event.request);
    })
  );
});

// 检查版本更新
async function checkVersion() {
  try {
    console.log('[SW] 开始检查版本...');
    const response = await fetch('/package.json', { 
      cache: 'no-cache',
      headers: {
        'Cache-Control': 'no-cache',
        'Pragma': 'no-cache'
      }
    });
    
    const data = await response.json();
    
    if (data.version !== currentVersion) {
      console.log('[SW] 检测到新版本');
      // 使用 includeUncontrolled: true 和 type: 'window' 参数
      const clients = await self.clients.matchAll({
        includeUncontrolled: true,
        type: 'window'
      });
      
      console.log('[SW] 找到的客户端数量:', clients.length);
      
      if (clients.length === 0) {
        // 如果没有找到客户端,使用 BroadcastChannel
        const bc = new BroadcastChannel('version-update');
        bc.postMessage({
          type: 'VERSION_UPDATE',
          version: data.version,
          showAlert: true
        });
      } else {
        // 如果找到客户端,使用常规方式发送消息
        clients.forEach((client) => {
          client.postMessage({
            type: 'VERSION_UPDATE',
            version: data.version,
            showAlert: true
          });
        });
      }
      
      currentVersion = data.version;
    }
  } catch (error) {
    console.error('[SW] 版本检查失败:', error);
  }
}

// 监听消息
self.addEventListener('message', (event) => {
  console.log('[SW] 收到消息:', event.data);
  if (event.data === 'CHECK_VERSION') {
    console.log('[SW] 收到检查版本请求');
    checkVersion();
  }
});

// 每分钟检查一次版本(测试阶段)
setInterval(checkVersion, 5 * 60 * 1000);

// 立即进行首次检查
checkVersion();

2. Service Worker 注册和消息处理(serviceWorker.ts)

创建工具函数处理 Service Worker 的注册和消息通信:

typescript:utils/serviceWorker.ts 复制代码
import { ElMessageBox, ElMessage } from 'element-plus';

// 显示更新提示
function showUpdateNotification() {
  console.log('[Main] 显示更新提示');
  ElMessageBox.confirm(
    '系统发现新版本,为了更好的体验,建议立即更新',
    '更新提示',
    {
      confirmButtonText: '立即更新',
      cancelButtonText: '稍后更新',
      type: 'warning',
    }
  )
    .then(() => {
      console.log('[Main] 用户确认更新');
      // 清除缓存并刷新页面
      return caches.keys().then(function(names) {
        return Promise.all(names.map(name => caches.delete(name)));
      });
    })
    .then(() => {
      console.log('[Main] 缓存已清除,准备刷新页面');
      window.location.reload(true);
    })
    .catch(() => {
      console.log('[Main] 用户取消更新');
      ElMessage({
        type: 'info',
        message: '您可以继续使用,但可能存在部分功能更新'
      });
    });
}

// 注册 Service Worker
export const registerServiceWorker = async () => {
  // 本地开发环境不注册 Service Worker
  if (import.meta.env.DEV) {
    console.log('[Main] 本地开发环境,跳过 Service Worker 注册');
    return;
  }
  if ('serviceWorker' in navigator) {
    try {
      console.log('[Main] 开始注册 Service Worker...');
      
      // 注册 Service Worker
      const registration = await navigator.serviceWorker.register('/sw.js');
      console.log('[Main] Service Worker 注册成功:', registration);

      // 添加常规消息监听
      const messageHandler = (event: MessageEvent) => {
        console.log('[Main] 收到 Service Worker 消息:', event.data);
        if (event.data.type === 'VERSION_UPDATE') {
          console.log('[Main] 收到版本更新通知');
          showUpdateNotification();
        }
      };
      
      // 添加 BroadcastChannel 监听
      const bc = new BroadcastChannel('version-update');
      bc.onmessage = (event) => {
        console.log('[Main] 收到广播消息:', event.data);
        if (event.data.type === 'VERSION_UPDATE') {
          console.log('[Main] 收到版本更新通知');
          showUpdateNotification();
        }
      };

      navigator.serviceWorker.addEventListener('message', messageHandler);

      if (registration.active) {
        console.log('[Main] Service Worker 已激活,发送检查版本消息');
        registration.active.postMessage('CHECK_VERSION');
      }

    } catch (error) {
      console.error('[Main] Service Worker 注册失败:', error);
    }
  } else {
    console.warn('[Main] 浏览器不支持 Service Worker');
    // 不支持 Service Worker 时,使用定时器检查更新
    console.log('[Main] 使用定时器检查更新');
    setInterval(() => {
      fetch('/package.json', {
        cache: 'no-cache',
        headers: {
          'Cache-Control': 'no-cache', 
          'Pragma': 'no-cache'
        }
      })
      .then(response => response.json())
      .then(data => {
        const currentVersion = localStorage.getItem('app-version');
        if (currentVersion && currentVersion !== data.version) {
          console.log('[Main] 检测到新版本');
          showUpdateNotification();
        }
        localStorage.setItem('app-version', data.version);
      })
      .catch(error => {
        console.error('[Main] 版本检查失败:', error);
      });
    }, 5 * 60 * 1000); // 每5分钟检查一次
  }
};

// 检查更新
export const checkForUpdates = () => {
  if (navigator.serviceWorker && navigator.serviceWorker.controller) {
    console.log('[Main] 手动触发版本检查');
    navigator.serviceWorker.controller.postMessage('CHECK_VERSION');
  } else {
    console.warn('[Main] Service Worker 未就绪,无法检查更新');
  }
}; 

3. 自动版本号递增(autoIncrementVersion.ts)

为了方便版本管理,实现了一个 Vite 插件在构建时自动递增版本号:

typescript:autoIncrementVersion.ts 复制代码
import { Plugin } from 'vite'
import fs from 'fs'
import path from 'path';
//版本号自增
const incrementVersion = (version: string) => {
  const parts = version.split('.').map(Number);
  parts[2]++;
  if (parts[2] > 9) {
    parts[2] = 0;
    parts[1]++;
    if (parts[1] > 9) {
      parts[1] = 0;
      parts[0]++;
    }
  }
  return parts.join('.');
}

export default function autoIncrementVersion(): Plugin {
  return {
    name: 'vite:autoIncrementVersion',
    apply: 'build',
    //构建开始时的钩子
    buildStart(options) {
      if (options) {
        try {
          const pkgPath = path.resolve("./package.json");
          console.log('Reading package.json from:', pkgPath);
          const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
          console.log('Current version:', pkg.version);
          pkg.version = incrementVersion(pkg.version);
          console.log('New version:', pkg.version);
          fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
        } catch (error) {
          console.error('Error updating version:', error);
        }
      }
    },
  }
}

使用方法

  1. 在 main.ts 中注册 Service Worker:
typescript:main.ts 复制代码
// 注册 Service Worker
import { registerServiceWorker } from '/@/utils/serviceWorker';
registerServiceWorker();
  1. 在 vite.config.ts 中添加版本自增插件:
typescript:vite.config.ts 复制代码
import autoIncrementVersion from './autoIncrementVersion'

export default defineConfig({
  plugins: [autoIncrementVersion()]
})

注意事项

  1. Service Worker 必须在 HTTPS 环境下运行(本地开发环境除外)
  2. 本地开发环境(localhost)可以正常使用
  3. 为了保证版本检查的准确性,package.json 的请求始终绕过缓存
  4. 提供了降级方案,在不支持 Service Worker 的环境下使用定时器检查更新

总结

通过 Service Worker 结合版本号检查,我们实现了一个可靠的前端应用自动更新机制。该方案具有以下优点:

  1. 自动检测新版本
  2. 提供友好的更新提示
  3. 支持离线缓存
  4. 包含降级方案
  5. 自动化的版本管理

这个方案可以显著提升用户体验,确保用户始终使用最新版本的应用。

相关推荐
剪刀石头布啊1 分钟前
js常见的单例
前端·javascript
剪刀石头布啊2 分钟前
数据口径
前端·后端·程序员
剪刀石头布啊6 分钟前
http状态码大全
前端·后端·程序员
剪刀石头布啊8 分钟前
iframe通信、跨标签通信的常见方案
前端·javascript·html
宇之广曜17 分钟前
搭建 Mock 服务,实现前端自调
前端·mock
yuko093118 分钟前
【手机验证码】+86垂直居中的有趣问题
前端
用户15129054522022 分钟前
Springboot中前端向后端传递数据的几种方式
前端
阿星做前端23 分钟前
如何构建一个自己的 Node.js 模块解析器:node:module 钩子详解
前端·javascript·node.js
用户15129054522026 分钟前
Web Worker:让前端飞起来的隐形引擎
前端
司宸27 分钟前
学习笔记十 —— 自定义hooks设计原则 笔试实现
前端