如何在前端实现版本更新提示功能

在现代Web开发中,尤其是在多部门共用的后台管理系统中,频繁的版本迭代是常态。然而,用户往往不会主动刷新页面,导致他们可能在使用旧版本的页面,从而影响功能体验。为了解决这个问题,我研究并实现了一个在前端检测版本更新并提示用户刷新的方案。本文将详细介绍这个方案的实现思路和代码细节。

整体思路

  1. 文件命名与缓存:项目使用Webpack + Vue2构建,配置了打包后的文件依据文件内容生成文件名。为了确保浏览器不会缓存请求,我们在请求URL中加上时间戳。

  2. 轮询检测:通过轮询的方式,在根页面向服务器请求当前页面的HTML内容。

  3. 比对脚本标签:比对当前HTML文件的<script>标签的src路径是否一致。由于打包后的JS文件会以<script>标签的形式注入到HTML页面中,因此通过比对<script>标签可以检测到版本更新。

  4. 通知用户刷新:如果发现<script>标签的路径不一致,则通知用户刷新页面。

实现

1. 创建VersionChecker

我们首先创建一个VersionChecker类,用于处理版本更新的检测和通知。

export class VersionMonitor {
  constructor(options = {}) {
    this.oldScriptSrc = new Set();
    this.newScriptSrc = new Set();
    this.listeners = {};
    this.intervel = null;
    this.options = options;
    this.init();
    this.timing(options.timer);
  }

  async init() {
    try {
      const html = await this.getHtml();
      this.oldScriptSrc = this.extractScriptSrc(html);
    } catch (error) {
      console.error('初始化失败:', error);
    }
  }

  async getHtml() {
    try {
      const response = await fetch('/?timestamp=' + new Date().getTime());
      if (!response.ok) {
        throw new Error('网络请求失败');
      }
      return await response.text();
    } catch (error) {
      console.error('获取 HTML 失败:', error);
      return '';
    }
  }

  extractScriptSrc(html) {
    if (!html) {
      return new Set();
    }
    const scriptSrc = new Set();
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, 'text/html');
    const scripts = doc.querySelectorAll('script[src]');
    scripts.forEach((script) => {
      scriptSrc.add(script.src);
    });
    return scriptSrc;
  }

  on(key, fn) {
    if (!key || typeof fn !== 'function') {
      console.error('无效的 key 或 fn');
      return this;
    }
    (this.listeners[key] || (this.listeners[key] = [])).push(fn);
    return this;
  }

  compare(oldSrc, newSrc) {
    if (!oldSrc || !newSrc) {
      console.error('无法比较,src 集合为空');
      return;
    }
    if (oldSrc.size !== newSrc.size) {
      this.notifyUpdate();
      return;
    }
    for (const src of oldSrc) {
      if (!newSrc.has(src)) {
        this.notifyUpdate();
        return;
      }
    }
    this.notifyNoUpdate();
  }

  notifyUpdate() {
    if (this.listeners.update) {
      this.listeners.update.forEach((fn) => fn());
    }
  }

  notifyNoUpdate() {
    if (this.listeners['no-update']) {
      this.listeners['no-update'].forEach((fn) => fn());
    }
  }

  timing(time = 30000) {
    this.intervel = setInterval(async () => {
      try {
        const newHtml = await this.getHtml();
        this.newScriptSrc = this.extractScriptSrc(newHtml);
        this.compare(this.oldScriptSrc, this.newScriptSrc);
      } catch (error) {
        console.error('轮询失败:', error);
      }
    }, time);
  }

  destroy() {
    clearInterval(this.intervel);
  }

  async restart() {
    try {
      await this.init();
      this.timing(this.options.timer);
    } catch (error) {
      console.error('重启失败:', error);
    }
  }
}

2. 在App组件中使用

在Vue的App组件中,我们实例化VersionChecker类,并设置更新通知。

async mounted() {
  if (process.env.VUE_APP_ENV === 'pro') {  // 通过环境变量判断是否是生产环境
    // 实例化该类
    const up = new VersionChecker({
      timer: 1000 * 60 * 5  // 传入定义好的轮询时间
    });

    // 更新通知
    up.on('update', () => {
      up.destroye(); // 每次通知先销毁 保证计时器不叠加
      // 弹窗提示用户
      this.$confirm('系统有更新,点击刷新按钮立即刷新', '提示', {
        type: 'info',
        cancelButtonText: '知道了,稍后手动刷新!',
        confirmButtonText: '刷新',
        closeOnClickModal: false, // 是否点击遮罩(点击空白处)关闭
        showClose: false // 是否显示右上角的x
      })
        .then(() => {
          location.reload();  // 点击刷新
          up.restart();  // 重新开始轮询
        })
        .catch((err) => {
          console.log(err);
          // 用户选择稍后刷新 重新开始轮询
          up.restart();
        });
    });
  }
}

1.使用 DOMParser 解析 HTML,并提取 src 属性,使用 Set 存储和比对 src 属性。
2.将比对逻辑拆分为 compare、notifyUpdate 和 notifyNoUpdate 方法,代码更易读和维护。
3.如果需要检测其他属性或顺序,可以轻松扩展 extractScriptSrc 和 compare 方法。

总结

通过利用Webpack根据内容生成文件名的特性,结合fetch请求和时间戳,我们实现了一个简单但有效的版本更新提示功能。这个方案不仅解决了用户和开发者的困扰,还展示了如何在前端实现轮询检测和发布订阅模式的应用。

虽然代码并不复杂,但使用面向对象的写法可以使代码更加清晰和易于维护。希望这个方案能为大家在类似场景下的开发提供一些参考和启发。

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐