react 实现前端发版监测

先说下前端发版流程#

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

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

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

4. 重启服务器

问题1#

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

前端发版检测原理#

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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是值是否被修改

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
/**
 * 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);
  }
};

 

 获取请求的头部信息 

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

 

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

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

 

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

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
/**
 * 版本检测逻辑
 * 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;
  }
}

 

作者:冯叶青

出处:https://www.cnblogs.com/yz-blog/p/18298935

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   冯叶青  阅读(21)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示