首页 > 其他分享 >react 实现前端发版监测

react 实现前端发版监测

时间:2024-07-12 17:41:45浏览次数:8  
标签:resolve const 前端 manifest react json eTag etag 发版

先说下前端发版流程

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;
  }
}

 

标签:resolve,const,前端,manifest,react,json,eTag,etag,发版
From: https://www.cnblogs.com/yz-blog/p/18298935

相关文章

  • 前端web程序发布到windows服务器流程详解
    假定已完成前端程序开发并完成构建。#步骤1:准备服务器环境我们将使用IIS作为Web服务器。确保你的Windows系统已经安装了IIS。#步骤2:配置Web服务器1.打开"控制面板">“程序”>“启用或关闭Windows功能”。2.选中"InternetInformationServices",确保"Web服务器(IIS......
  • vue3前端项目结构解析(2024-07-12)
    .├──build#打包脚本相关│  ├──config#配置文件│  ├──generate#生成器│  ├──script#脚本│  └──vite#vite配置├──mock#mock文件夹├──public#公共静态资源目录├──src#主目录│  ├──api#接口......
  • 前端 纯CSS border-radius画一个波浪动画
    利用border-radius生成椭圆并不是利用旋转的椭圆本身生成波浪效果,而是利用它去切割背景,产生波浪的效果。HTML:<h2>波浪动画</h2>SCSS:body{position:relative;align-items:center;min-height:100vh;background-color:rgb(118,218,255);ov......
  • 适用于react、vue菜单格式化工具函数
    场景在一个动态菜单场景中,你向接口获取树形菜单,但最后拿到的树未能达到你的预期,这个时候就需要手写递归重新处理这颗树适用于react、vue菜单格式化工具函数包含功能1.当前路由是否存在返回按钮判断逻辑:只要存在左侧可点击的菜单都不具备返回按钮,其他则具有返回按钮2.错误......
  • 版本发布 | IvorySQL 3.3 发版
    [发行日期:2024年7月11日]IvorySQL3.3基于PostgreSQL16.3,并修复了多个问题。更多信息请参考 文档网站。>>>新版本体验链接:https://docs.ivorysql.org/cn/ivorysql-doc/v3.3/v3.3/welcome.html1增强功能>>>PostgreSQL16.3的增强功能 1)将pg_stats_ext和pg_sta......
  • 图标组件的封装与管理(React/svg)
    一概要1.1背景最近在项目中使用了很多从iconfont拿到的图标。使用官网的导入方法有些繁琐,也不易管理。于是捣鼓了一下...1.2目的能够像组件一样使用,具有规范性。比如暴露一个type属性,根据不同的type使用不同的主题色。高自由度。可以直接在项目中管理图标,只需要处理从其......
  • 前端使用 Vue 3,后端使用 Spring Boot 构建 Hello World 程序
    前端使用Vue3,后端使用SpringBoot构建HelloWorld程序前端(Vue3)首先,创建一个Vue3项目。1.安装VueCLInpminstall-g@vue/cli2.创建Vue项目vuecreatefrontend在交互式提示中,选择默认的Vue3预设。3.修改App.vue在frontend/src目录下,修改......
  • 前端传参
    前端传参参数各种格式详解一、form-data二、application/x-www-form-urlencoded三、application/json四、text/xml总结 上传文件采用 form-data一般接口采用 application/x-www-form-urlencoded form-dataenctype等于multipart/form-data。form-data格式一般是......
  • 如何看待“前端已死”?
    现在所说的前端已死到底是怎么回事,首先毋庸置疑,现在的行情确实比之前差的太多了,不过前端并不是“死了”,这里的前端已死暗指两条第一就是对学历的要求更加严格了,比如之前找工作就算你学历或者年龄没有达到标准,但你只要能写页面也就睁一只眼闭一只眼过去了,现在不一样了,随着人数......
  • 前端如何接收EventStream中的数据?
    本文目录1、fetch2、EventSourcefetchfetch是浏览器内置的方法无需下载fetch("http://127.0.0.1:6594/ws/getAccessToken",{method:"get",}).then((response)=>{constdecoder=newTextDecoder("utf-8");......