记一次前端应用部署后,刷新静态资源丢失问题
一、前言
最近在生产环境发现一个很奇怪的现象,当更新生产环境版本时,就监控到部分用户js资源出现如下提示,但是刷新后发现页面访问正常。
基本就是这个Issues的描述信息
发现了这个问题后,首先怀疑,服务器前端资源B版本更新后,用户浏览器缓存了入口文件index.html
A版本,index.html
拉取A版本js资源,但B版本更新后A版本资源不存在导致的。
给入口文件增加强制不缓存后,发现还有个别用户出现这个问题。
这还了得,赶紧查查,但总是偶现并不能随时重现,开始怀疑是nginx转发的问题,后来经过排查发现:
当前端组件A为懒加载,且用户保持页面在线且没有点击该组件A,此时更新版本后,用户点击A组件路由,浏览器拉取静态资源404报错。
所以因为浏览器缓存的存在,要避免前端资源的直接覆盖,容易导致各种js找不到的情况出现。
二、模拟环境准备
1、一个前端项目
这里的前端项目通过create-react-app
来搭建,路由通过懒加载方式实现,核心代码如下:
import { Suspense, lazy } from 'react';
import { HashRouter, Routes, Route, Link } from "react-router-dom";
const AFunction = lazy(() => import('./page/a.js'))
const BFunction = lazy(() => import('./page/b.js'))
function App() {
return (
<div className="App">
<HashRouter basename="/">
<div style={{marginBottom: 20}}>
<Link style={{marginRight: 20}} to="/">Home</Link>
<Link to="/about">About</Link>
</div>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<AFunction />}></Route>
<Route path="/about" element={<BFunction />} />
</Routes>
</Suspense>
</HashRouter>
</div>
);
}
export default App;
2、本地安装Docker服务
生产环境一般使用Nginx部署,我们在本地复现,采用nginx + docker来模拟。
三、重现生产环境问题
1、通过nginx正常部署前端服务
docker pull nginx
docker run -d 80:80 -v /Users/user/Desktop/my-app:/usr/share/nginx/html nginx
如图所示,我们启动了my-app后,当点击About时,可以看到加载了About的资源,而不是一开始就全部加载。
重现问题操作
刷新当前页面,保持页面位于Home页面,不要点击About。此时加载了前端资源但没有加载About相关的资源。
2、更新版本发布后,复现问题
修改About组件内容(注意要修改待测试的组件,防止由于组件hash没有更新,无法重现问题),重新打包build文件
import React from 'react';
function BFunction(){
return <>
b组件3
</>
}
export default BFunction
重新启动Docker,查看index.html
静态资源引入
docker restart [containerId]
此时打开原来的页面,不要刷新,直接点击About组件,发现资源加载404,页面空白
此时刷新当前url或者重新打开url,发现页面加载正常
以上就重现了我们生产环境遇到的问题,更新版本个别用户访问页面出现空白。
四、分析并解决问题
1、解决思路
问题的原因,我们知道了,那么解决问题的方案有以下几种
- 用户更新资源
- 1、每次切换页面,都重新加载入口文件,这样每次都能加载到最新的静态资源。
- 2、当版本更新后,提示用户重新加载站点
- 保留服务器老版本资源
- 3、既然浏览器有缓存,还要加载原来的资源,那么就保留多个资源版本,这样当用户在浏览系统时,即使服务器更新了版本,用户只要不刷新页面,那么访问的依旧是老的版本
以上方案的优缺点
- 1、方案一明显浪费了单页面应用加载时间的优越性,属于自废武功。
- 2、方案二更新版本提示用户,对用户过于打扰。
- 3、方案三无疑是最好的方案,保存多个版本,还能做灰度方案,在出现问题时及时回退。
- 4、你还有什么建议吗?
3、服务器资源版本配置
具体实现,首先要改造项目,让项目构建时打包成不同的版本,即修改文件的打包路径,由于我们的项目是通过create-react-app实现的,那么更新打包路径就要释放webpack配置或者利用插件
我们部署nginx的访问静态服务器/data/web
文件目录为
- 2023080101
- static
- css
- js
- 2023080102
- static
- css
- js
- 2023080201
- ...
- index.html
- logo.png
当我们构建新的版本时,只需要在项目下新增一个版本,然后更新index.html
,这样当老的index.html切换路由,可以拉取到老的资源文件,新的也可以使用新的资源。
3、改造前端服务
要实现上面的方案,就要改写项目,改写方式很多,只需要选择一种即可。
1)安装依赖和修改webpack的插件
yarn add react-app-rewired customize-cra -D
2) 修改webpack配置文件
在项目根目录下新建文件config-overrides.js
module.exports = function override(config, env){
config.output = {
...config.output,
// 保持开发环境访问路径不变,生产环境改变打包路径
publicPath: env === 'production' ? `/${process.env.BUILD_PATH}/` : '/'
}
return config;
};
3)修改script指令
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
}
4)管理打包的版本号,方便后续自动化构建和统一管理
在项目下新建.env
文件统一管理版本
BUILD_PATH = "2023080101"
4、部署第一个版本
打包前端服务,打开index.html
,发现引入资源路径更新了
重新Docker部署,注意js路径变成了2023080101
下面
5、部署第二个版本
修改.env
版本号,重新构建,将打包的文件放在Nginx服务目录下,然后重启Docker
BUILD_PATH = "2023090101"
重新上一步的页面,点击About发现加载正常,资源是之前的2023080101
这样就解决了上面提到的问题,接着刷新当前页面,查看加载的资源,发现资源已经更新成2023090101
五、总结
以上,我们解决了更新版本资源丢失的问题,这个解决方案还是比较好的。而且后续可以支持我们做灰度的管理、版本的控制、回退处理
他的难点在:
- 1、需要对项目进行改造,构建不同的版本
- 2、需要将资源放在对应的目录下,如果手动操作就比较繁琐
解决方案:
- 1、建议接入CI/CD,这样可以将流程固定化,版本号通过时间戳来记录。
- 2、备份index.html,通过脚本实现回退能力。
转载自:https://juejin.cn/post/7269694294597287971