Loading

react 实现前端发版监测

先说下前端发版流程

1. 前端打包输出产物 /dist 文件

2. 删除远程服务下打包的旧代码

3. 将打包参物 /dist 文件 copy 到远程服务器目录

4. 重启服务器

问题1

在步骤2,3,4中用户访问目标服务器会报JS错误,正常情况打开网页的控制面板会看下报错信息 `Failed to fetch dynamically imported module`

前端发版检测原理

这个报错信息其实会触发react的错误边界,我们可以利用这个错误边界来获取是否在发版,可以看下面检测流程

1. 修改配置,让打包产物多出一个manifest.json 文件

vite配置如下,其他打包工具自行看官方文档配置

    build: {
      manifest: true, //加上此配置可生成 manifest.json 文件
      assetsDir: 'static',
      rollupOptions: {
        input: {
          index: resolve(__dirname, 'index.html')
        },
        output: {
          chunkFileNames: 'static/js/[name]-[hash].js',
          entryFileNames: 'static/js/[name]-[hash].js'
        }
      },
      commonjsOptions: {
        transformMixedEsModules: true
      }
    },

2. 默认获取manifest.json 的etag ,一般情况,manifest.json 内容没有变更,etag值是不会变的,只有manifest.json变了,etag才会变,由此可见,当manifest.json的etag值变更了,意味着发版走到了发版步骤3

3. 步骤3中,copy是一个过程,而不是立马就可以结束,所以我们下一步就要监测步骤3什么时候结束

4. 随机抽取manifest.json中的文件,抽取数量大家可以随意修改,我这边检测的是3个

5. 这些文件检测完之后再等待个5s,继续去请求manifest.json文件,请求成功之后再刷新浏览器

为啥还要等5s再继续请求manifest.json?

因为你把文件全部获取到了,服务可能需要重启,这个时候如果重启过程中,你也是获取不到服务器资源的

下面开始贴代码块

eTag管理,主要是检测mainfest.json的etag是值是否被修改

/**
 * eTag管理
 * 服务器发版检测用
 * */
export const eTag = {
  init: (doNotCache?: boolean) => {
    return new Promise((resolve, reject) => {
      fetchRequestHeader().then((headers) => {
        const etag = headers.get('etag');
        if (!doNotCache) {
          eTag.set(etag);
        }
        resolve(etag);
      });
    });
  },
  //获取远程eTag
  getRemoteETag: () => {
    return new Promise((resolve, reject) => {
      eTag
        .init(true)
        .then((data) => {
          resolve(data);
        })
        .catch(() => {
          reject();
        });
    });
  },
  get get() {
    return window.localStorage.getItem('eTag') || '';
  },
  set: (value: string | null) => {
    value && window.localStorage.setItem('eTag', value);
  }
};

 

 获取请求的头部信息 

/** 获取请求的头部信息 */
export const fetchRequestHeader = (): Promise<Headers> => {
  return fetch(`/admin/manifest.json`, {
    method: 'HEAD',
    cache: 'no-cache'
  }).then((response) => response.headers);
};

 

求随机数,随机获取文件时可用

/**
 *  求min与max之间的随机数
 * */
export const rand = (min: number, max: number) => {
  return Math.round(Math.random() * (max - min)) + min;
};

 

 QkErrorBound/index.tsx 错误边界代码块

/**
 * 版本检测逻辑
 * 1. 先比对manifest.json文件是否有变动
 *    1.1 变动,则随机向manifest.json抽出三个文件
 *      1.1.1 轮询同时请求这三个文件
 *      1.1.1.1 请求成功,刷新界面
 *    1.2 不变动,继续1.1
 * */

import React, { PureComponent } from 'react';
import { Result, Badge } from 'antd';
import { eTag, rand } from '@/utils/tools.ts';
import { fetchManifestJson } from '@/services/common.ts';

type QkErrorBoundType = {
  children: React.ReactNode;
};

export default class QkErrorBound extends PureComponent<
  QkErrorBoundType,
  {
    hasError: boolean;
    type: number;
    time: number;
    count: number;
    errMsg: string;
    loadEerr: boolean;
  }
> {
  detectionTimerId: NodeJS.Timeout | null = null; //检测
  countdownTimerId: NodeJS.Timeout | null = null; //倒计时
  constructor(props: NonNullable<any>) {
    super(props);
    this.state = {
      hasError: false,
      type: 1,
      time: 30,
      count: 0,
      errMsg: '',
      loadEerr: false
    };
  }

  static getDerivedStateFromError(error: Error & { componentStack: string }) {
    console.log({ error, type: 'getDerivedStateFromError' });
    return { hasError: true };
  }
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.log({ error, errorInfo });
    let loadEerr = false;
    if (
      error?.message?.includes('Failed to fetch dynamically imported module')
    ) {
      this.handleVersionUpdates();

      loadEerr = true;
    }
    this.timedOutFefresh();
    this.setState({
      hasError: true,
      errMsg: error.message || JSON.stringify(errorInfo),
      loadEerr
    });
  }

  getManifestJson() {
    fetchManifestJson()
      .then(async (data) => {
        const len = Object.keys(data).length;
        const files = [rand(0, len), rand(0, len), rand(0, len)];
        const manifestJson: [string, Record<string, any>][] =
          Object.entries(data);
        console.log(1111);
        const fetchs: boolean[] = [];
        for (let i = 0; i < files.length; i++) {
          await new Promise((resolve, reject) => {
            fetch(manifestJson[files[i]][1]?.file, {
              method: 'HEAD',
              cache: 'no-cache'
            })
              .then((response) => {
                console.log(response);
                fetchs.push(response.ok);
                resolve(response.ok);
              })
              .catch((reason) => {
                console.log(reason);
                fetchs.push(false);
                resolve(false);
              });
          });
        }
        if (fetchs.filter(Boolean).length === files.length) {
          window.reload();
          console.log('3');
        } else {
          console.log('请求失败,3s重新请求中....');
          setTimeout(() => {
            this.getManifestJson();
          }, 3000);
        }
      })
      .catch(() => {
        setTimeout(() => {
          this.getManifestJson();
        }, 3000);
      });
  }

  /** 检测是否有版本更新 */
  handleVersionUpdates = () => {
    this.detectionTimerId && clearInterval(this.detectionTimerId);
    this.detectionTimerId = setInterval(() => {
      eTag.getRemoteETag().then((data) => {
        if (data !== eTag.get) {
          this.detectionTimerId && clearInterval(this.detectionTimerId);
          this.getManifestJson();
        }
      });
    }, 3000);
  };

  /** 超过1分钟进行刷新 */
  timedOutFefresh = () => {
    this.countdownTimerId && clearInterval(this.countdownTimerId);
    this.countdownTimerId = setInterval(() => {
      this.setState({
        count: this.state.count + 1
      });
      /** 升级超过一分钟自动刷新页面 */
      console.log({ count: this.state.count });
      if (this.state.count >= 60) {
        this.countdownTimerId && clearInterval(this.countdownTimerId);
        window.reload();
      }
    }, 1000);
  };
  render() {
    if (this.state.hasError) {
      return (
        <div>
          <Result
            status="500"
            title={
              <Badge offset={[7, 0]} dot={!this.state.loadEerr}>
                <h2 className="font-normal">系统升级</h2>
              </Badge>
            }
            subTitle={
              this.state.type === 1 ? (
                '检测到系统功能已升级,正在获取最新系统...'
              ) : (
                <div>
                  系统正在升级中,预计
                  <span className="text-primary">{this.state.time}s</span>
                  后完成升级
                </div>
              )
            }
          />
        </div>
      );
    }
    return this.props.children;
  }
}

 

posted @ 2024-07-12 17:42  冯叶青  阅读(16)  评论(0编辑  收藏  举报