🤖 作者简介:水煮白菜王,一位资深前端劝退师 👻
👀 文章专栏: 前端专栏 ,记录一下平时在博客写作中,总结出的一些开发技巧和知识归纳总结✍。
感谢支持💕💕💕
一、应用场景
在现代Web应用中,为了提升用户体验并确保系统的稳定性和一致性,部署前端版本更新后及时提醒用户进行页面刷新是至关重要的。当生产环境发布了包含功能变化的新版本时,由于单页面(SPA)应用的路由特性和浏览器缓存机制,用户浏览器可能不会自动加载最新的代码资源,这可能导致用户遇到bug或体验到不一致的功能行为。通过实现自动检测机制来提醒用户版本更新,并引导其刷新页面,可以
- 避免用户使用过期版本
- 确保功能一致性
- 减少接口兼容性问题
- 提高应用可靠性
二、实现原理
2.1 核心检测逻辑
通过对比构建产物的哈希值变化实现版本检测:
- 定时轮询:每分钟检查静态资源变化
- 哈希对比:通过解析HTML中script标签指纹判断更新
- 强制刷新:检测到更新后提示用户刷新页面
// 核心对比逻辑
const isChanged = (oldSet, newSet) => {
return oldSet.size !== newSet.size ||
![...oldSet].every(hash => newSet.has(hash))
}
2.2 实现优势
- 通用性强:适用于任意前端框架
- 无侵入式检测:不依赖构建工具配置
- 用户可控:提示框让用户选择刷新时机
- 精准检测:通过对比script标签内容哈希值
- 低资源消耗:每分钟检测一次,单次请求性能消耗低
三 、具体实现
3.1 工程化封装
// 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 关键方法解析
脚本哈希获取:
const fetchScriptHashes = async () => {
const html = await fetch('/').then(res => res.text())
const scriptRegex = /<script(?:\s+[^>]*)?>(.*?)<\/script\s*>/gi
return new Set(html?.match(scriptRegex) || [])
}
对比逻辑:
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具体逻辑
// @/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
// App.vue
import versionUpdatefrom '@/utils/use-version-update.js'
export default {
setup() {
const { updateNotice } = versionUpdate()
return { updateNotice }
}
}
3、Vite 相关配置
// 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引入
// 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具体逻辑
/*
* @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
// App.vue
import versionUpdate from "@/util/use-version-update.js";
export default {
name: "app",
mixins: [versionUpdate],
data() {
return {};
},
};
3、Webpack 相关配置
// 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实现无缝更新
// 支持Service Worker的渐进增强方案
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => {
reg.addEventListener('updatefound', () => {
showUpdateNotification()
})
})
}
同时要确保服务器配置正确缓存策略,通常Nginx缓存策略默认不用打理
如果你觉得这篇文章对你有帮助,请点赞 👍、收藏 👏 并关注我!👀