基于 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. 自动化的版本管理

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

相关推荐
bin91531 分钟前
DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加行拖拽排序功能示例4,TableView16_04 跨表格拖拽示例
前端·javascript·vue.js·ecmascript·deepseek
玄魂3 分钟前
报表优化实战:组件库Table升级VTable
前端·开源·数据可视化
琹箐8 分钟前
js文字两端对齐
前端·javascript·css
摆烂工程师10 分钟前
炸裂了~兄弟们,GPT4o出图效果太好了
前端·后端·程序员
开心小老虎11 分钟前
用HTML和CSS生成炫光动画卡片
前端·css·html
米粒宝的爸爸17 分钟前
vue3 vue-router 传递路由参数
前端·javascript·vue.js
前端同学23 分钟前
react版本主要区别
前端·react.js·前端框架
2401_878454531 小时前
Es6进阶
前端·javascript·es6
KarajanKing1 小时前
elementUI的el-table 树状表格本地模糊搜索并返回原有格式进行展示等
前端
鲁子狄1 小时前
[笔记] 多层 Nginx反向代理与Docker容器化前端应用部署 : 客户端 -> 本地 Nginx -> Docker 内的 Nginx -> 前端应用
前端·后端·docker