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; } } |
愿你走出半生,归来仍是少年
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!