2种纯前端检测版本更新提示

背景

在单页应用(SPA)项目中,路由的变化并不会导致前端资源的重新加载。然而,当服务端进行了更新,例如接口的字段名或结构发生变化,而前端的静态资源没有同步更新时,可能会导致报错。对于那些长时间保持电脑开机且浏览器页面保持打开状态的用户来说,版本升级提示显得尤为重要。

前言

为了避免因版本不匹配而引发的报错,提供及时、准确的版本更新提示变得至关重要。

1. 如何比较版本?

  • 监听响应头中的 Etag 的变化

  • 监听 git commit hash 的变化

2. 何时比较版本?

  1. 使用 setInterval 轮询固定时间段进行比较
  2. 监听 visibilitychange 事件
  3. 监听 focus 事件

大部分情况使用setInterval设置一个合理的时间段即可,但当tab被切换时,浏览器的资源分配可能会受到限制,例如减少CPU和网络的使用,setInterval的执行可能会受到影响。

因此还需监听visibilitychange事件,当其选项卡的内容变得可见或被隐藏时,会在 document 上触发 visibilitychange 事件。

但在 pc 端,从浏览器切换到其他应用程序并不会触发 visibilitychange 事件,所以加以 focus 辅佐;当鼠标点击过当前页面 (必须 focus 过),此时切换到其他应用会触发页面的 blur 事件;再次切回到浏览器则会触发 focus 事件;

使用setInterval+visibilitychange+focus便可解决绝大部分应用更新提示情况。如果更新频繁,对版本提示实时性要求较高的,还可以监听路由的变化,在路由切换时,获取版本的信息进行对比。

监听响应头中的 Etag 的变化

框架:React+Antd

关键点:

  1. getETag:获取静态资源的Etag或last-modified

  2. getHash:如果localStorage中没有版本信息,说明是新用户,直接存储版本信息。如果本地存储的版本信息与获取到的Etag不同,则弹出更新提示弹窗

  3. 使用setInterval+visibilitychange+focus调用getHash

js 复制代码
import { useCallback, useEffect, useRef } from 'react';

import { notification, Button } from 'antd';

const getETag = async () => {
  const response = await fetch(window.location.origin, {
    cache: 'no-cache',
  });
  return response.headers.get('etag') || response.headers.get('last-modified');
};

const useVersion = () => {
  const timer = useRef();
  //防止弹出多个弹窗
  const uploadNotificationShow = useRef(false);

  const close = useCallback(() => {
    uploadNotificationShow.current = false;
  }, []);

  const onRefresh = useCallback(
    (new_hash) => {
      close();
      // 更新localStorage版本号信息
      window.localStorage.setItem('vs', new_hash);
      // 刷新页面
      window.location.reload();
    },
    [close],
  );

  const openNotification = useCallback(
    (new_hash) => {
      uploadNotificationShow.current = true;
      const btn = (
        <Button type="primary" size="small" onClick={() => onRefresh(new_hash)}>
          确认更新
        </Button>
      );
      notification.open({
        message: '版本更新提示',
        description: '检测到系统当前版本已更新,请刷新后使用。',
        btn,
        duration: 0,
        onClose: close,
      });
    },
    [close, onRefresh],
  );

  const getHash = useCallback(() => {
    if (!uploadNotificationShow.current) {
      // 在 js 中请求首页地址,这样不会刷新界面,也不会跨域
      getETag().then((res) => {
        const new_hash = res;
        const old_hash = localStorage.getItem('vs');
        if (!old_hash && new_hash) {
          // 如果本地没有,则存储版本信息
          window.localStorage.setItem('vs', new_hash);
        } else if (new_hash && new_hash !== old_hash) {
          // 本地已有版本信息,但是和新版不同:版本更新,弹出提示
          openNotification(new_hash);
        }
      });
    }
  }, [openNotification]);

  /* 初始时检查,之后1h时检查一次 */
  useEffect(() => {
    getHash();
    timer.current = setInterval(getHash, 60 * 60 * 1000);
    return () => {
      clearInterval(timer.current);
    };
  }, [getHash]);

  useEffect(() => {
    /* 切换浏览器tab时 */
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'visible') {
        getHash();
      }
    });

    /* 当鼠标点击过当前页面,此时切换到其他应用会触发页面的blur;
    再次切回到浏览器则会触发focus事件 */
    document.addEventListener('focus', getHash, true);
  }, [getHash]);
};

export default useVersion;

监听 git commit hash 的变化

框架:Vite+Vue3

1. 将版本号写入环境变量中

新建scripts/release.js

关键点:

  1. 执行git rev-parse HEAD获取最新git commit hash

  2. 使用fs.writeFileSync将版本号写入到.env.production文件中

js 复制代码
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filenameNew = fileURLToPath(import.meta.url);

const __dirname = path.dirname(__filenameNew);

//执行git命令,获取当前 HEAD 指针所指向的提交的完整哈希值,并通过substring取前十个数字或字母
const hash = execSync('git rev-parse HEAD', { cwd: path.resolve(__dirname, '../') })
  .toString()
  .trim()
  .substring(0, 10);

//版本号:时间戳+git的hash值
const version = Date.now() + '_' + hash;

//.env.production文件路径
const envFile = path.join(__dirname, '../.env.production');

//读取目标文件,并通过正则判断.env.production文件中是否有VITE_APP_VERSION开头的环境变量
try {
  const data = fs.readFileSync(envFile, {
    encoding: 'utf-8',
  });
  const reg = /VITE_APP_VERSION=\d+_[\w-_+:]{7,14}/g;
  const releaseStr = `VITE_APP_VERSION=${version}`;
  let newData = '';
  if (reg.test(data)) {
    newData = data.replace(reg, releaseStr);
    fs.writeFileSync(envFile, newData);
  } else {
    newData = `${data}\n${releaseStr}`;
    fs.writeFileSync(envFile, newData);
  }
  console.log(`插入release版本信息到env完成,版本号:${version}`);
} catch (e) {
  console.error(e);
}

2. 配置启动命令

修改package.json

js 复制代码
"build": "node ./scripts/release.js && vite build",

运行 yarn build,可以在.env.production文件中看到环境变量VITE_APP_VERSION已成功写入

3. 轮询+visibilitychange + focus检测

关键点:

  1. import.meta.env.VITE_APP_VERSION:获取新版本的hash值

  2. getHash:如果localStorage中没有版本信息,说明是新用户,直接存储版本信息。如果本地存储的版本信息与新版本的hash值不同,则弹出更新提示弹窗

  3. 使用setInterval+visibilitychange+focus调用getHash

js 复制代码
import { onBeforeUnmount, onMounted, ref } from 'vue';

const useCheckUpdate = () => {
  const timer = ref();
  const new_hash = import.meta.env.VITE_APP_VERSION; //获取新版本的hash值
  let uploadNotificationShow = false; //防止弹出多个框

  const getHash = () => {
    if (!uploadNotificationShow && new_hash) {
      const old_hash = localStorage.getItem('vs');
      if (!old_hash) {
        // 如果本地没有,则存储版本信息
        window.localStorage.setItem('vs', new_hash);
      } else if (new_hash !== old_hash) {
        uploadNotificationShow = true;
        // 本地已有版本信息,但是和新版不同:版本更新,弹出提示
        if (window.confirm('检测到系统当前版本已更新,请刷新浏览器后使用。')) {
          uploadNotificationShow = false;
          // 更新localStorage版本号信息
          window.localStorage.setItem('vs', new_hash);
          // 刷新页面
          window.location.reload();
        } else {
          uploadNotificationShow = false;
        }
      }
    }
  };

  onMounted(() => {
    getHash();
    timer.value = setInterval(getHash, 60 * 60 * 1000);
    /* 切换浏览器tab时 */
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'visible') {
        getHash();
      }
    });

    /* 当鼠标点击过当前页面,此时切换到其他应用会触发页面的blur;
        再次切回到浏览器则会触发focus事件 */
    document.addEventListener('focus', getHash, true);
  });

  onBeforeUnmount(() => {
    clearInterval(timer.value);
  });
};

export default useCheckUpdate;

结尾

文章中只是介绍了大概的实现方式与思路,还有些细节可根据自己的实际情况实现。例如在开发环境下,不要弹出版本更新提示弹窗等功能。

如果有其他更好的方式实现版本更新提示,可以在评论区留言,大家积极探讨。

最后,创造不易,欢迎大家点赞支持!!!

相关推荐
Leyla6 分钟前
【代码重构】好的重构与坏的重构
前端
影子落人间9 分钟前
已解决npm ERR! request to https://registry.npm.taobao.org/@vant%2farea-data failed
前端·npm·node.js
世俗ˊ33 分钟前
CSS入门笔记
前端·css·笔记
子非鱼92134 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
6230_38 分钟前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
想退休的搬砖人1 小时前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
加勒比海涛1 小时前
HTML 揭秘:HTML 编码快速入门
前端·html
啥子花道1 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
茶茶只知道学习1 小时前
通过鼠标移动来调整两个盒子的宽度(响应式)
前端·javascript·css