web如何检测版本更新(一)

2,661 阅读3分钟

前言

近期发现每次在大版本上线时,总是要人工在群里或者各渠道通知用户手动刷新页面清理浏览器缓存。用户基数不大的情况下可控。一旦后期用户基数成倍增长后,要通知到每个用户就不是那么容易了。因此新版本上线后,能否像APP一样主动去提示用户系统更新并要求他去主动刷新呢?答案是,能。

思路

  • webpack 打包时文件名加入hash
  • 检测当前hash与最新版本的hash值是否一致,若不一致则提示用户有新版本。

实现

1.打包时给文件加入hash值

  • 使用react-app-rewired插件则在config-overrides.js中增加如下配置
module.exports = function (config, env) {
    ...(省略其他配置)
    config.output = {
        filename: 'bundle.[hash].js',
        path: path.resolve(__dirname, 'build'),
    };
}
return config;
  • 如果你是自己用webpack搭的项目,直接在webpack.config.js增加如下配置
module.exports = {
    ...(省略其他配置)
    output: {
        filename: 'bundle.[hash].js',
        path: path.resolve(__dirname, 'dist')
    },
    ...(省略其他配置)

但加了hash值之后。每次改动源码编译后都会产生新的hash文件,那build里就会有好多没有用的过时的文件垃圾。每次都手动删除。比较烦。可以使用clean-webpack-plugin清除。

2.检测当前hash与最新版本的hash值是否一致

  • 获取当前hash
  /** 获取当前hash */
  const getCurrentHash = useCallback(() => {
      const curHashSrc = document
        .getElementsByTagName('body')[0]
        .getElementsByTagName('script')[0]
        .src.split('/');
      curHash.current = curHashSrc[curHashSrc.length - 1].split('.')[1];
  }, []);
  • 获取最新hash
 /*** timestamp-时间戳防止请求缓存*/
 const fetchNewHash = useCallback(async () => {
    // 在 js 中请求首页地址不会更新页面
    const timestamp = new Date().getTime();
    const response = await axios.get(`${window.location.origin}?time=${timestamp}`);
    // 返回的是字符串,需要转换为 html
    const el = document.createElement('html');
    el.innerHTML = response.data;
    // 拿到 hash
    const newHashSrc = el
      .getElementsByTagName('body')[0]
      .getElementsByTagName('script')[0]
      .src.split('/');
    const newHash = newHashSrc[newHashSrc.length - 1].split('.')[1];
    console.log('fetchNewHash', curHash.current, newHash);
    if (newHash && newHash !== curHash.current && !visible) {
      // 版本更新,弹出提示
      setVisible(true);
    } else if (newHash && newHash === curHash.current && visible) {
      setVisible(false);
    }
  }, [visible]);

到这里大家应该会有个疑问,在什么节点去触发检测呢。我的想法是将其封装成组件然后挂载在项目的根节点下,初始化项目的时候,触发一次更新,并且开启轮询检测(半小时/一小时一次)如果用户离开或者熄屏则清除轮询。点亮屏幕或切回网页所在的页签则触发检测,并且开启轮询。

3.提示用户更新

在用户点击刷新后,自动刷新,浏览器,并关闭提示。

  /** 立即刷新 */
  const handleConfirm = useCallback(() => {
    setVisible(false);
    setTrackerUserId();
    window.location.reload();
  }, []);

写在最后

这种做法有个弊端就是,获取hash值方式的扩展性不高(推荐方案二)要按不同项目的实际情况去取。其实在做这个之前在网上也发现有类似的插件@femessage/update-popup但是不能自定义。

  • 完整代码
import React, { useState, useRef, useEffect, useCallback, useMemo, memo } from 'react';
import axios from 'axios';

import LionetConfirm from '@/shared/components/LionetConfirm';
import { setTrackerUserId } from './Tracker';

/** 检测版本更新 */
const VersionUpdateToast = () => {
  /** 当前hash值 */
  const curHash = useRef<any>(null);
  /** 定时器 */
  const timer = useRef<any>(null);
  const [visible, setVisible] = useState(false);

  /** 轮询间隔 */
  const pollingTime = useMemo(() => {
    return 30 * 60 * 1000;
  }, []);

  /** 获取最新hash值
   * timestamp-时间戳防止请求缓存
   */
  const fetchNewHash = useCallback(async () => {
    // 在 js 中请求首页地址不会更新页面
    const timestamp = new Date().getTime();
    const response = await axios.get(`${window.location.origin}?time=${timestamp}`);
    // 返回的是字符串,需要转换为 html
    const el = document.createElement('html');
    el.innerHTML = response.data;
    // 拿到 hash
    const newHashSrc = el
      .getElementsByTagName('body')[0]
      .getElementsByTagName('script')[0]
      .src.split('/');
    const newHash = newHashSrc[newHashSrc.length - 1].split('.')[1];
    console.log('fetchNewHash', curHash.current, newHash);
    if (newHash && newHash !== curHash.current && !visible) {
      // 版本更新,弹出提示
      setVisible(true);
    } else if (newHash && newHash === curHash.current && visible) {
      setVisible(false);
    }
  }, [visible]);

  /** 开启定时器 */
  const setTimer = useCallback(() => {
    timer.current = setInterval(fetchNewHash, pollingTime);
  }, [fetchNewHash, pollingTime]);

  /** 清除定时器 */
  const clearTimer = useCallback(() => {
    if (timer.current) {
      clearInterval(timer.current);
      timer.current = null;
    }
  }, []);

  /** 获取当前hash */
  const getCurrentHash = useCallback(() => {
    const curHashSrc = document
      .getElementsByTagName('body')[0]
      .getElementsByTagName('script')[0]
      .src.split('/');
    // eslint-disable-next-line prefer-destructuring
    curHash.current = curHashSrc[curHashSrc.length - 1].split('.')[1];
  }, []);

  /** 获取hash */
  const getHash = useCallback(() => {
    getCurrentHash();
    fetchNewHash();
  }, [fetchNewHash, getCurrentHash]);

  /** 浏览器窗口是否显示隐藏 */
  const onVisibilityChange = useCallback(() => {
    // eslint-disable-next-line prefer-destructuring
    if (!document.hidden) {
      setTimer();
      getHash();
    } else {
      clearTimer();
    }
  }, [clearTimer, getHash, setTimer]);

  useEffect(() => {
    getHash();
    setTimer();
    return () => {
      clearTimer();
    };
  }, []);

  useEffect(() => {
    document.addEventListener('visibilitychange', onVisibilityChange);
    return () => {
      document.removeEventListener('visibilitychange', onVisibilityChange);
    };
  }, []);

  /** 立即刷新 */
  const handleConfirm = useCallback(() => {
    setVisible(false);
    setTrackerUserId();
    window.location.reload();
  }, []);

  return (
    <LionetConfirm
      visible={visible}
      isShowCancelBtn={false}
      confirmText="立即刷新"
      onConfirm={handleConfirm}
    >
      <span>发现新版本,请刷新后使用哦~</span>
    </LionetConfirm>
  );
};

export default memo(VersionUpdateToast);