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;
}
}
愿你走出半生,归来仍是少年

浙公网安备 33010602011771号