当主应用或微应用发版后,由于浏览器缓存和资源路径变化,会导致正在访问的用户出现访问资源bug

在这里插入图片描述

没找后端 纯前端解决bug

思路如下,在路由守卫beforEach处进行判断当前本地所存储版本号与服务器上文件版本号是否一致,如果不一致,则执行window.location.reload(),具体操作:

1、在public/static中添加version.json文件

{
    "version": "1.0.1"
}

2、在src中创建一个version文件夹,并创建versionUpdate.js文件

import axios from 'axios'
import { ElMessage } from 'element-plus'

const isNewVersion = () => {
  const url = `//${window.location.host}/static/version.json?t=${new Date().getTime()}`
  axios.get(url).then((res) => {
    if (res.status === 200) {
      console.log(res.data.version, 'version')

      const version = res.data.version
      const localVueVersion = localStorage.getItem('version')
      if (localVueVersion && localVueVersion !== version) {
        localStorage.setItem('version', version)
        ElMessage.info('获取有新版本,即将刷新页面获取最新版本')
        window.location.reload()
      } else {
        localStorage.setItem('version', version)
      }
    }
  })
}

export default {
  isNewVersion
}

3.在全局路由拦截中,只要每次版本号不同就重新加载页面清除浏览器缓存

router.beforeEach(async (to, from, next) => {
  versionTood.isNewVersion()
  // 其他逻辑
})

4、在src/version文件中,创建versionUpdate.js文件

import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'

// 获取当前文件的目录路径
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

//返回version的json数据
async function getVersionJson() {
  const versionPath = path.join(__dirname, '../../public/static/version.json')
  const data = fs.readFileSync(versionPath, 'utf-8') //fs读取文件
  return JSON.parse(data) //转换为json对象
}

export default async function addVersion() {
  const versionData = await getVersionJson() //获取version的json
  const arr = versionData.version.split('.') //切割后的版本号数组
  arr[2] = parseInt(arr[2]) + 1
  versionData.version = arr.join('.') //转换为以"."分割的字符串
  //用versionData覆盖version.json内容
  try {
    // 将数据转换为 JSON 字符串并写入文件
    const versionPath = path.join(__dirname, '../../public/static/version.json')
    fs.writeFileSync(versionPath, JSON.stringify(versionData, null, '\t'))
    console.log('version has been successfully updated.', versionData.version)
  } catch (err) {
    console.error('Error writing to version:', err)
  }
}

5、在在package.json中先执行addVersion.js脚本

"build:test": "node -e \"import('./src/version/addVersion.js').then(m => m.default())\"  && vite build --mode test",

6、先本地执行测试
在这里插入图片描述
自动更新版本号 成功!

7、切记 要发版两次 才能测试问题是否解决哦!
8、如果你的微服务由单独得域名 修改如下

const url = `${import.meta.env.VITE_MICRO_URL}/static/version.json?t=${new Date().getTime()}`

微服务版本更新问题修复方案

问题描述

当浏览器停留在微服务页面时,微服务发版后会出现资源加载 404 错误:

GET https://test-micro.megainfomediazxt.com/assets/index-D3Z4eODz.js net::ERR_ABORTED 404 (Not Found)

原因分析:

  1. 用户停留在微服务页面时,页面已加载旧版本的资源文件
  2. 发版后,旧的资源文件(如 index-D3Z4eODz.js)从服务器删除
  3. 当检测到版本更新并执行 window.location.reload() 时,页面仍尝试加载已删除的旧资源
  4. 导致 404 错误,页面无法正常加载

解决方案

1. 版本更新逻辑优化 (src/version/versionUpdate.js)

核心改进:

a) 使用硬刷新替代普通 reload

// ❌ 之前:使用 reload(),可能加载缓存中的旧资源
window.location.reload()

// ✅ 现在:使用 replace() + 版本参数,强制加载新资源
const newUrl = `${currentUrl}?_v=${version}&_t=${Date.now()}`
window.location.replace(newUrl)

b) 添加资源加载错误监听器

window.addEventListener(
  'error',
  (event) => {
    const target = event.target
    // 检查是否是资源加载错误(script、link等)
    if (target && (target.tagName === 'SCRIPT' || target.tagName === 'LINK')) {
      const resourceUrl = target.src || target.href
      // 如果是关键资源加载失败,执行硬刷新
      if (
        resourceUrl &&
        (resourceUrl.includes('/assets/') ||
          resourceUrl.includes('.js') ||
          resourceUrl.includes('.css'))
      ) {
        // 强制刷新并清除缓存
        window.location.replace(`${window.location.href}?_t=${Date.now()}`)
      }
    }
  },
  true
)

c) 微前端环境特殊处理

if (window.__POWERED_BY_WUJIE__) {
  // 通知主应用版本更新
  window.$wujie?.bus?.$emit('micro-version-update', {
    app: 'micro',
    oldVersion: localVueVersion,
    newVersion: version,
    timestamp: Date.now()
  })

  // 使用硬刷新
  window.location.replace(newUrl)
}

关键技术点

1. location.replace() vs location.reload()

方法 特点 问题
location.reload() 重新加载当前页面,可能使用缓存 可能加载旧的缓存资源
location.replace(url) 替换当前页面,不保留历史记录 可以通过 URL 参数强制刷新

3. 防止频繁刷新

// 防止无限刷新:最多只刷新一次
          const hasReloadedForError = sessionStorage.getItem('resource-error-reloaded')
          if (!hasReloadedForError) {
            console.warn('执行硬刷新以加载新版本资源')
            sessionStorage.setItem('resource-error-reloaded', 'true')

            // 延迟刷新,给其他资源加载的机会
            setTimeout(() => {
              const separator = window.location.href.includes('?') ? '&' : '?'
              window.location.replace(`${window.location.href}${separator}_reload=${Date.now()}`)
            }, 500)
          } else {
            console.error('已经刷新过一次,避免无限循环')
          }
测试 效果不理想 在微服务页面依然加载失败, 重新改造下代码

4、 尝试通过主应用完成刷新

主应用Micro/index.vue代码改造

// 在主应用中监听微服务版本更新
bus.$on('micro-version-update', (event: any) => {
  // 方案:销毁微服务实例,然后刷新主应用
  // 这样可以确保微服务重新加载新版本
  try {
    WujieVue.destroyApp(appName)
    console.log('微服务实例已销毁')
  } catch (err) {
    console.error('销毁微服务实例失败:', err)
  }

  // 延迟刷新,确保销毁完成
  setTimeout(() => {
    const separator = window.location.href.includes('?') ? '&' : '?'
    const newUrl = `${window.location.href}${separator}_microV=${event.newVersion}&_t=${Date.now()}`
    console.log('刷新主应用:', newUrl)
    window.location.replace(newUrl)
  }, 200)
})

微应用versionUpdate.js 完整代码

import axios from 'axios'

// 添加资源加载错误监听器(仅在版本更新时启用)
let hasSetupErrorListener = false
let resourceErrorCount = 0
const setupResourceErrorListener = () => {
  if (hasSetupErrorListener) return
  hasSetupErrorListener = true

  console.log('启用资源加载错误监听器(版本更新保护)')

  // 监听资源加载错误
  window.addEventListener(
    'error',
    (event) => {
      const target = event.target
      // 检查是否是资源加载错误(script、link等)
      if (target && (target.tagName === 'SCRIPT' || target.tagName === 'LINK')) {
        const resourceUrl = target.src || target.href
        console.error('资源加载失败:', resourceUrl)

        // 如果是关键资源加载失败,执行硬刷新
        if (
          resourceUrl &&
          (resourceUrl.includes('/assets/') ||
            resourceUrl.includes('.js') ||
            resourceUrl.includes('.css'))
        ) {
          resourceErrorCount++
          console.warn(`检测到关键资源加载失败 (${resourceErrorCount}次)`)

          // 防止无限刷新:最多只刷新一次
          const hasReloadedForError = sessionStorage.getItem('resource-error-reloaded')
          if (!hasReloadedForError) {
            console.warn('执行硬刷新以加载新版本资源')
            sessionStorage.setItem('resource-error-reloaded', 'true')

            // 延迟刷新,给其他资源加载的机会
            setTimeout(() => {
              const separator = window.location.href.includes('?') ? '&' : '?'
              window.location.replace(`${window.location.href}${separator}_reload=${Date.now()}`)
            }, 500)
          } else {
            console.error('已经刷新过一次,避免无限循环')
          }
        }
      }
    },
    true
  )
}

const isNewVersion = () => {
  const url = `${import.meta.env.VITE_MICRO_URL}/static/version.json?t=${new Date().getTime()}`
  axios
    .get(url)
    .then((res) => {
      if (res.status === 200) {
        console.log(res.data.version, 'version')

        const version = res.data.version
        const localVueVersion = localStorage.getItem('micro-version')
        if (localVueVersion && localVueVersion !== version) {
          console.warn('检测到新版本:', version, '旧版本:', localVueVersion)
          localStorage.setItem('micro-version', version)

          // 设置错误监听器,以防刷新时资源加载失败
          setupResourceErrorListener()

          if (window.__POWERED_BY_WUJIE__) {
            // 通知主应用有新版本,由主应用决定如何刷新
            window.$wujie?.bus?.$emit('micro-version-update', {
              app: 'micro',
              oldVersion: localVueVersion,
              newVersion: version,
              timestamp: Date.now()
            })
          } else {
            // 独立运行环境:直接硬刷新
            console.log('独立运行环境,检测到版本更新')

            // 清除刷新标记,允许资源错误时刷新
            sessionStorage.removeItem('resource-error-reloaded')

            console.log('将在 1 秒后刷新页面以加载新版本')
            setTimeout(() => {
              // 使用 location.replace 强制硬刷新,清除缓存
              const currentUrl = window.location.href.split('?')[0]
              const newUrl = `${currentUrl}?_v=${version}&_t=${Date.now()}`
              console.log('执行刷新:', newUrl)
              window.location.replace(newUrl)
            }, 1000)
          }
        } else {
          localStorage.setItem('micro-version', version)
        }
      }
    })
    .catch((error) => {
      console.error('获取版本信息失败:', error)
    })
}

export default {
  isNewVersion
}

原理 就是微应用检测到版本更新通知主应用来完成强制刷新动作

问题解决!

为什么微服务自己刷新不成功,主应用刷新就成功?

🏗️ 微前端架构分析

运行环境对比

真实浏览器窗口
├── 主应用 (window.location = https://admin.com/micro/order)
    └── Wujie 容器
        └── Iframe 沙箱
            └── 微服务 (沙箱 location = https://test-micro.com/order/list)

❌ 方案1: 微服务自己刷新(失败)

代码

// 微服务内部
if (window.__POWERED_BY_WUJIE__) {
  // 微服务自己刷新
  window.location.replace(`${window.location.href}?_v=${version}&_t=${Date.now()}`)
}

实际执行流程

1. 微服务检测到版本更新
   ↓
2. 调用 window.location.replace()
   ↓
3. ⚠️ 这个 window 是 iframe 沙箱的 window,不是浏览器的 window
   ↓
4. 刷新的是 iframe 内的内容
   ↓
5. Wujie 实例状态
   ├── ✅ 还存在(没有销毁)
   ├── ❌ props 可能过期
   ├── ❌ 生命周期钩子未重新触发
   ├── ❌ EventBus 可能有残留监听器
   └── ❌ 缓存状态未清理
   ↓
6. 微服务重新加载
   ├── ⚠️ 可能使用了旧的 props
   ├── ⚠️ 可能继承了旧的状态
   └── ⚠️ 可能有事件监听器冲突
   ↓
7. 问题:
   ├── 🐛 资源 URL 可能还是旧的(因为 Wujie 缓存)
   ├── 🐛 微服务的 index.html 可能被 Wujie 缓存
   ├── 🐛 Wujie 的模块加载器可能还记得旧的模块
   └── 🐛 导致加载旧版本资源 → 404 → 白屏

详细问题分析

问题 A: Wujie 的 URL 劫持
// Wujie 内部机制(简化)
class WujieInstance {
  constructor(url, props) {
    this.url = url // 初始 URL,被缓存了
    this.iframe = createIframe()
    this.proxy = createProxy() // 代理 window 对象
  }

  // 当微服务调用 location.replace 时
  // Wujie 可能不会完全重置内部状态
  handleLocationChange(newUrl) {
    // ⚠️ 可能只是更新 iframe src
    // 但 Wujie 的其他状态没有重置
    this.iframe.src = newUrl
    // ❌ this.url 可能还是旧的
    // ❌ 缓存的资源引用可能还是旧的
  }
}
问题 B: 资源缓存未清理
// Wujie 的资源加载机制
const resourceCache = new Map()

// 第一次加载
resourceCache.set('/index.html', {
  content: '<script src="/assets/index-OLD123.js"></script>'
})

// 微服务刷新后
// ⚠️ Wujie 可能还在使用这个缓存
// 导致加载的还是旧的 HTML,引用旧的 JS 文件
问题 C: 生命周期不完整
// Wujie 生命周期
beforeLoad() { }   // ❌ 微服务刷新时可能不触发
beforeMount() { }  // ❌ 微服务刷新时可能不触发
mounted() { }      // ❌ 微服务刷新时可能不触发
unmount() { }      // ❌ 微服务刷新时不会触发

✅ 方案2: 主应用刷新(成功)

代码

// 主应用
bus.$on('micro-version-update', (event) => {
  // 1. 先销毁微服务实例
  WujieVue.destroyApp(appName)

  // 2. 再刷新主应用
  setTimeout(() => {
    window.location.replace(`${window.location.href}?_t=${Date.now()}`)
  }, 200)
})

实际执行流程

1. 微服务检测到版本更新
   ↓
2. 通知主应用(EventBus)
   ↓
3. 主应用收到通知
   ↓
4. 主应用调用 WujieVue.destroyApp(appName)
   ├── ✅ 销毁 Wujie 实例
   ├── ✅ 移除 iframe
   ├── ✅ 清理 EventBus
   ├── ✅ 清理所有监听器
   ├── ✅ 清理缓存
   └── ✅ 完全释放资源
   ↓
5. 延迟 200ms(确保销毁完成)
   ↓
6. 主应用刷新(真实浏览器 window)
   ├── ✅ 整个页面重新加载
   ├── ✅ 主应用重新初始化
   └── ✅ 所有 JS/CSS 重新加载
   ↓
7. 主应用重新渲染 Micro 组件
   ↓
8. Wujie 创建全新实例
   ├── ✅ 全新的 props
   ├── ✅ 全新的生命周期
   ├── ✅ 全新的 EventBus
   ├── ✅ 没有任何缓存
   └── ✅ 完全干净的状态
   ↓
9. 加载微服务
   ├── ✅ 请求新版本的 index.html(带时间戳,不使用缓存)
   ├── ✅ 新 HTML 引用新版本的 JS/CSS 文件名
   └── ✅ 所有资源都是新版本
   ↓
10. ✅ 成功!

🔍 关键区别对比

维度 微服务自己刷新 主应用刷新
刷新对象 iframe 沙箱 真实浏览器窗口
Wujie 实例 ⚠️ 保留(状态可能不一致) ✅ 完全销毁后重建
资源缓存 ❌ 可能使用旧缓存 ✅ 完全清除
生命周期 ⚠️ 可能不完整 ✅ 完整触发
EventBus ⚠️ 可能有残留 ✅ 全新创建
Props ⚠️ 可能过期 ✅ 全新传递
URL 状态 ⚠️ 只改沙箱 URL ✅ 真实 URL 改变
index.html ❌ 可能被缓存 ✅ 强制重新获取

📊 视觉化对比

微服务自己刷新

┌─────────────────────────────────────────┐
│         浏览器窗口 (保持不变)             │
│  ┌─────────────────────────────────┐    │
│  │      主应用 (保持运行)            │    │
│  │  ┌───────────────────────────┐  │    │
│  │  │   Wujie 实例 (保留) ⚠️     │  │    │
│  │  │  ├─ props (可能过期) ❌    │  │    │
│  │  │  ├─ cache (旧数据) ❌      │  │    │
│  │  │  └─ listeners (残留) ❌    │  │    │
│  │  │                           │  │    │
│  │  │  ┌─────────────────────┐  │  │    │
│  │  │  │  Iframe (刷新)      │  │  │    │
│  │  │  │  微服务重新加载      │  │  │    │
│  │  │  │  但可能用旧状态 ⚠️   │  │  │    │
│  │  │  └─────────────────────┘  │  │    │
│  │  └───────────────────────────┘  │    │
│  └─────────────────────────────────┘    │
└─────────────────────────────────────────┘

主应用刷新

步骤1: 销毁
┌─────────────────────────────────────────┐
│         浏览器窗口                        │
│  ┌─────────────────────────────────┐    │
│  │      主应用                       │    │
│  │  ┌───────────────────────────┐  │    │
│  │  │   Wujie.destroyApp()      │  │    │
│  │  │   ├─ 移除 iframe ✅        │  │    │
│  │  │   ├─ 清理缓存 ✅          │  │    │
│  │  │   └─ 清理监听器 ✅        │  │    │
│  │  │                           │  │    │
│  │  │   [Wujie 实例已销毁] ✅   │  │    │
│  │  └───────────────────────────┘  │    │
│  └─────────────────────────────────┘    │
└─────────────────────────────────────────┘

步骤2: 刷新
┌─────────────────────────────────────────┐
│   浏览器窗口 (location.replace) ✅        │
│              ↓↓↓                        │
│        [完全重新加载]                     │
│              ↓↓↓                        │
│  ┌─────────────────────────────────┐    │
│  │   主应用 (全新实例) ✅            │    │
│  │  ┌───────────────────────────┐  │    │
│  │  │   Wujie (全新创建) ✅      │  │    │
│  │  │   ├─ props (全新) ✅      │  │    │
│  │  │   ├─ cache (空) ✅        │  │    │
│  │  │   └─ listeners (新) ✅    │  │    │
│  │  │                           │  │    │
│  │  │  ┌─────────────────────┐  │  │    │
│  │  │  │  微服务 (全新) ✅    │  │  │    │
│  │  │  │  - 新 HTML ✅        │  │  │    │
│  │  │  │  - 新 JS ✅          │  │  │    │
│  │  │  │  - 新 CSS ✅         │  │  │    │
│  │  │  └─────────────────────┘  │  │    │
│  │  └───────────────────────────┘  │    │
│  └─────────────────────────────────┘    │
└─────────────────────────────────────────┘

🔧 Wujie 的缓存机制(导致问题的根源)

HTML 缓存

// Wujie 内部可能的实现
class WujieInstance {
  htmlCache = null

  async loadHtml() {
    // 第一次加载
    if (!this.htmlCache) {
      this.htmlCache = await fetch(this.url).then((r) => r.text())
    }

    // ⚠️ 后续刷新可能直接使用缓存
    // 即使 URL 带了时间戳,Wujie 内部可能不重新请求
    return this.htmlCache
  }
}

模块缓存

// Wujie 的模块系统可能缓存了模块路径
const moduleCache = {
  '/assets/index.js': '/assets/index-OLD123.js' // 旧的映射
}

// 微服务刷新后,这个映射可能没有更新
// 导致还是加载旧文件

🎯 为什么必须销毁 Wujie 实例?

不销毁的问题

// Wujie 实例保留的状态
{
  url: 'https://test-micro.com/order/list',  // 原始 URL
  htmlCache: '<script src="/assets/index-OLD123.js"></script>',  // 旧 HTML
  scriptCache: Map {
    'index-OLD123.js': [Script Object]  // 旧脚本引用
  },
  styleCache: Map {
    'index-OLD123.css': [Style Object]  // 旧样式引用
  },
  props: { ... },  // 可能过期的 props
  eventBus: EventBus { /* 可能有残留监听器 */ },
  sandboxProxy: Proxy { /* 可能有旧的变量 */ }
}

销毁后的效果

// 销毁后所有状态清零
WujieVue.destroyApp(appName)

// 下次创建时,完全全新的实例
{
  url: 'https://test-micro.com/order/list',  // ✅ 新的
  htmlCache: null,  // ✅ 空的,会重新请求
  scriptCache: new Map(),  // ✅ 空的
  styleCache: new Map(),  // ✅ 空的
  props: { ... },  // ✅ 全新的 props
  eventBus: new EventBus(),  // ✅ 全新的 EventBus
  sandboxProxy: new Proxy()  // ✅ 全新的沙箱
}

💡 类比:为什么重启电脑能解决很多问题?

这个问题就像:

微服务自己刷新 = 只重启某个应用

Windows 系统 (主应用)
  └── Chrome 浏览器 (Wujie)
      └── 某个标签页 (微服务) [只刷新这个]

问题:
- Chrome 进程还在运行
- 可能有缓存、内存泄漏
- 可能有残留的后台任务
- 标签页刷新不彻底

主应用刷新 = 重启整个系统

Windows 系统 (主应用) [完全重启]
  └── Chrome 浏览器 (Wujie) [重新启动]
      └── 标签页 (微服务) [全新打开]

效果:
- 所有进程重新启动
- 所有缓存清除
- 所有内存释放
- 完全干净的状态

📝 总结

微服务自己刷新失败的核心原因

  1. 刷新的只是 iframe 沙箱,不是真实窗口
  2. Wujie 实例状态保留,可能有旧的缓存和状态
  3. 资源映射未更新,还在引用旧文件
  4. 生命周期不完整,某些钩子不会重新触发
  5. URL 虽然变了,但 Wujie 的内部状态没变

主应用刷新成功的核心原因

  1. 先销毁 Wujie 实例,完全清理所有状态
  2. 主应用完全重新加载,整个页面从零开始
  3. Wujie 全新创建,没有任何缓存和残留
  4. 微服务作为全新实例加载,获取最新资源
  5. 所有状态都是全新的,不会有任何冲突

最佳实践

// ✅ 正确的做法
// 1. 微服务通知主应用
window.$wujie?.bus?.$emit('micro-version-update', { ... })

// 2. 主应用销毁实例
WujieVue.destroyApp(appName)

// 3. 主应用刷新
setTimeout(() => {
  window.location.replace(...)
}, 200)

这就是为什么微服务自己刷新不成功,而通知主应用由主应用刷新就能成功的根本原因!🎯

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐