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

12/8/2024 版本更新

本文转自 云在前端 原文地址 (opens new window)

背景

测试:页面怎么不对啊?

你:刷新下浏览器

用户:页面怎么不对啊?

你:刷新下浏览器

老板:出去!

前言

在单页应用(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

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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

监听 git commit hash 的变化

框架:Vite+Vue3

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

新建scripts/release.js

关键点:

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

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

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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

2. 配置启动命令

修改package.json


"build": "node ./scripts/release.js && vite build",
1
2

运行 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

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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

结尾

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