前端实现版本更新自动检测✅

🤖 作者简介:水煮白菜王,一位资深前端劝退师 👻

👀 文章专栏: 前端专栏 ,记录一下平时在博客写作中,总结出的一些开发技巧和知识归纳总结✍。

感谢支持💕💕💕

一、应用场景

在现代Web应用中,为了提升用户体验并确保系统的稳定性和一致性,部署前端版本更新后及时提醒用户进行页面刷新是至关重要的。当生产环境发布了包含功能变化的新版本时,由于单页面(SPA)应用的路由特性和浏览器缓存机制,用户浏览器可能不会自动加载最新的代码资源,这可能导致用户遇到bug或体验到不一致的功能行为。通过实现自动检测机制来提醒用户版本更新,并引导其刷新页面,可以

  1. 避免用户使用过期版本
  2. 确保功能一致性
  3. 减少接口兼容性问题
  4. 提高应用可靠性

二、实现原理

2.1 核心检测逻辑

是 否 是 否 变化 是 否 未变化 应用启动 生产环境? 启动定时器 结束流程 等待60秒 获取当前脚本哈希 首次运行? 保存初始哈希 哈希变化? 停止定时器 显示更新提示 用户确认? 刷新页面 记录取消操作

通过对比构建产物的哈希值变化实现版本检测:

  1. 定时轮询:每分钟检查静态资源变化
  2. 哈希对比:通过解析HTML中script标签指纹判断更新
  3. 强制刷新:检测到更新后提示用户刷新页面
javascript 复制代码
// 核心对比逻辑
const isChanged = (oldSet, newSet) => {
  return oldSet.size !== newSet.size || 
         ![...oldSet].every(hash => newSet.has(hash))
}

2.2 实现优势

  • 通用性强:适用于任意前端框架
  • 无侵入式检测:不依赖构建工具配置
  • 用户可控:提示框让用户选择刷新时机
  • 精准检测:通过对比script标签内容哈希值
  • 低资源消耗:每分钟检测一次,单次请求性能消耗低

三 、具体实现

3.1 工程化封装

javascript 复制代码
// useVersionHash.js 核心实现
export default function useVersionHash() {
  // 状态管理
  const timerUpdate = ref(null)
  let scriptHashes = new Set()

  // 生命周期
  onMounted(() => startTimer())
  onBeforeUnmount(() => stopTimer())

  // 业务方法
  const fetchScriptHashes = async () => { /*...*/ }
  const compareScriptHashes = async () => { /*...*/ }
  
  return { compareScriptHashes }
}

3.2 关键方法解析

脚本哈希获取:

javascript 复制代码
const fetchScriptHashes = async () => {
  const html = await fetch('/').then(res => res.text())
  const scriptRegex = /<script(?:\s+[^>]*)?>(.*?)<\/script\s*>/gi
  return new Set(html?.match(scriptRegex) || [])
}

对比逻辑:

javascript 复制代码
if (scriptHashes.size === 0) {
  // 初始化基准值
  scriptHashes = newScriptHashes  
} else if (
  scriptHashes.size !== newScriptHashes.size ||
  ![...scriptHashes].every(hash => newScriptHashes.has(hash))
) {
  // 触发更新流程
  stopTimer()
  showUpdateDialog()
}

四、全部代码🚀🚀🚀

4.1 vue3

1、use-version-update.js具体逻辑

javascript 复制代码
// @/utils/use-version-update.js
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { ElMessageBox } from 'element-plus'

let scriptHashes = new Set()
const timerUpdate = ref(null)

export default function useVersionHash() {
  const isProduction = import.meta.env.MODE === 'production'

  const fetchScriptHashes = async () => {
    try {
      const html = await fetch('/').then((res) => res.text())
      const scriptRegex = /<script(?:\s+[^>]*)?>(.*?)<\/script\s*>/gi
      return new Set(html?.match(scriptRegex) || [])
    } catch (error) {
      console.error('获取脚本哈希失败:', error)
      return new Set()
    }
  }

  const compareScriptHashes = async () => {
    try {
      const newScriptHashes = await fetchScriptHashes()

      if (scriptHashes.size === 0) {
        scriptHashes = newScriptHashes
      } else if (
        scriptHashes.size !== newScriptHashes.size ||
        ![...scriptHashes].every(hash => newScriptHashes.has(hash))
      ) {
        stopTimer()
        updateNotice()
      }
    } catch (error) {
      console.error('版本检查失败:', error)
    }
  }

  const updateNotice = () => {
    ElMessageBox.confirm(
      '检测到新版本,建议立即更新以确保平台正常使用',
      '更新提示',
      {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }
    ).then(() => window.location.reload())
  }

  const startTimer = () => {
    if (!isProduction) return
    timerUpdate.value = setInterval(compareScriptHashes, 60000)
  }

  const stopTimer = () => {
    timerUpdate.value && clearInterval(timerUpdate.value)
  }

  onMounted(startTimer)
  onBeforeUnmount(stopTimer)

  return { compareScriptHashes, updateNotice }
}

2、引入use-version-update.js

javascript 复制代码
// App.vue
import versionUpdatefrom '@/utils/use-version-update.js'
export default {
  setup() {
    const { updateNotice } = versionUpdate()
    return { updateNotice }
  }
}

3、Vite 相关配置

javascript 复制代码
// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
         // 主入口文件命名规则
        entryFileNames: 'js/[name]-[hash:8].js',
        
        // 代码分割块命名规则
        chunkFileNames: 'js/[name]-[hash:8].js',
        
        // 静态资源文件命名规则
        assetFileNames: ({ name }) => {
          const ext = name?.split('.').pop()
          return `assets/${ext}/[name]-[hash:8].[ext]`
        }
      }
    },
    // 启用文件哈希的manifest生成
    manifest: true
  }
})

也可以将use-version-update写成1JS、TS模块化,在入口文件中main.ts引入

javascript 复制代码
// use-version-update.ts
export const versionUpdate = () => {
  ... 具体处理逻辑
}

// main.ts
import { versionUpdate} from "@/utils/use-version-update"
if (import.meta.env.MODE == 'production') {
  versionUpdate()
}

4.2 vue2

1、use-version-update.js具体逻辑

javascript 复制代码
/*
 * @Author: baicaiKing
 * @Date: 2025-01-02 13:50:33
 * @LastEditors: Do not edit
 * @LastEditTime: 2025-01-03 09:40:36
 * @FilePath: \code\src\utils\use-version-update.js
 */
// 存储当前脚本标签的哈希值集合
let scriptHashes = new Set();
let timerUpdate = undefined;
export default {
    data() {
        return {
        };
    },
    created() {
    },
    mounted() {
        // 每60秒检查一次是否有新的脚本标签更新
        if (process.env.NODE_ENV === 'production') { // 只针对生产环境
            timerUpdate= setInterval(() => {
                this.compareScriptHashes()
            }, 60000);
        }
    },
    beforeDestroy() {
        clearInterval(timerUpdate);
        timerUpdate = null;
    },
    methods: {
        /**
         * 从首页获取脚本标签的哈希值集合
         * @returns {Promise<Set<string>>} 返回包含脚本标签的哈希值的集合
         */
        async fetchScriptHashes() {
            // 获取首页HTML内容
            const html = await fetch('/').then((res) => res.text());
            // 正则表达式匹配所有<script>标签
            const scriptRegex = /<script(?:\s+[^>]*)?>(.*?)<\/script\s*>/gi;
            // 获取匹配到的所有<script>标签内容
            // const scripts = html.match(scriptRegex) ?? [];
            const scripts = html ? html.match(scriptRegex) || [] : [];
            // 将脚本标签内容存入集合并返回
            return new Set(scripts);
        },
        /**
         * 比较当前脚本标签的哈希值集合与新获取的集合,检测是否有更新
         */
        async compareScriptHashes() {
            // 获取新的脚本标签哈希值集合
            const newScriptHashes = await this.fetchScriptHashes();

            if (scriptHashes.size === 0) {
                // 初次运行时,存储当前脚本标签哈希值
                scriptHashes = newScriptHashes;
            } else if (
                scriptHashes.size !== newScriptHashes.size ||
                ![...scriptHashes].every((hash) => newScriptHashes.has(hash))
            ) {
                // 如果脚本标签数量或内容发生变化,则认为有更新
                console.info('已检测到更新文件', {
                    oldScript: [...scriptHashes],
                    newScript: [...newScriptHashes],
                });
                // 清除定时器
                clearInterval(timerUpdate);
                // 提示用户更新
                this.updateNotice();
            } else {
                // 没有更新
                console.info('未检测到更新时机', {
                    oldScript: [...scriptHashes],
                });
            }
        },
        updateNotice() {
            this.$confirm('检测到新版本,建议立即更新以确保平台正常使用', '更新提示', {
                confirmButtonText: '确定',
                cancelButtonText: '取消(自行刷新)',
                type: 'warning'
            }).then(() => {
                window.location.reload();
            }).catch(() => {
                console.eror('用户取消刷新!');

            });
        }
    },

};

2、引入use-version-update.js

javascript 复制代码
// App.vue
import versionUpdate from "@/util/use-version-update.js";
export default {
  name: "app",
  mixins: [versionUpdate],
  data() {
    return {};
  },
};

3、Webpack 相关配置

javascript 复制代码
// vue.config
module.exports = {
  configureWebpack: {
    output: {
      filename: 'js/[name].[hash].js',
      // filename: 'js/[name].[contenthash].js',
    },
  },
  devServer: {
  },
};

五、注意事项与常见问题

5.1 可能出现的问题

问题现象 可能原因 解决方案
检测不准确 正则匹配失效 更新正则表达式
生产环境未生效 环境变量配置错误 检查构建配置
跨域请求失败 部署路径不匹配 调整fetch请求路径
内存泄漏 定时器未正确清除 使用WeakRef优化

5.2 浏览器兼容方案

可结合Service Worker实现无缝更新

javascript 复制代码
// 支持Service Worker的渐进增强方案
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(reg => {
      reg.addEventListener('updatefound', () => {
        showUpdateNotification()
      })
    })
}

同时要确保服务器配置正确缓存策略,通常Nginx缓存策略默认不用打理

如果你觉得这篇文章对你有帮助,请点赞 👍、收藏 👏 并关注我!👀

相关推荐
张拭心29 分钟前
2024 总结,我的停滞与觉醒
android·前端
念九_ysl30 分钟前
深入解析Vue3单文件组件:原理、场景与实战
前端·javascript·vue.js
Jenna的海糖31 分钟前
vue3如何配置环境和打包
前端·javascript·vue.js
Mr.NickJJ1 小时前
React Native v0.78 更新
javascript·react native·react.js
星之卡比*1 小时前
前端知识点---库和包的概念
前端·harmonyos·鸿蒙
灵感__idea1 小时前
Vuejs技术内幕:数据响应式之3.x版
前端·vue.js·源码阅读
烛阴1 小时前
JavaScript 构造器进阶:掌握 “new” 的底层原理,写出更优雅的代码!
前端·javascript
Alan-Xia1 小时前
使用jest测试用例之入门篇
前端·javascript·学习·测试用例
浪遏1 小时前
面试官😏 :文本太长,超出部分用省略号 ,怎么搞?我:🤡
前端·面试
昕er1 小时前
CefSharp 文件下载和保存功能-监听前端事件
前端