Loading chunk {n} failed 的解决方法

背景:

  • 前端代码更改后,每次发布到测试环境,用户的页面如果不刷新,会读取缓存,导致页面白掉!
  • 本地没有过,都是打包到服务器上才有

error info

1
2
3
4
5
6
7
8
9
10
11
12
13
Uncaught SyntaxError: Unexpected token '<'
Uncaught ChunkLoadError: Loading chunk 8 failed.
(missing: https://mispaceuat.mihoyo.com/static/js/8.98f2a71fc60af3a81dd1.js)
at Function.i.e (https://mispaceuat.mihoyo.com/static/js/app.98f2a71fc60af3a81dd1.js?98f2a71fc60af3a81dd1:1:934)
at https://mispaceuat.mihoyo.com/static/js/app.98f2a71fc60af3a81dd1.js?98f2a71fc60af3a81dd1:1:3105
at https://mispaceuat.mihoyo.com/static/js/vendor.98f2a71fc60af3a81dd1.js?98f2a71fc60af3a81dd1:2:1229684
at gl (https://mispaceuat.mihoyo.com/static/js/vendor.98f2a71fc60af3a81dd1.js?98f2a71fc60af3a81dd1:2:1229833)
at sc (https://mispaceuat.mihoyo.com/static/js/vendor.98f2a71fc60af3a81dd1.js?98f2a71fc60af3a81dd1:2:1221601)
at lc (https://mispaceuat.mihoyo.com/static/js/vendor.98f2a71fc60af3a81dd1.js?98f2a71fc60af3a81dd1:2:1221526)
at $l (https://mispaceuat.mihoyo.com/static/js/vendor.98f2a71fc60af3a81dd1.js?98f2a71fc60af3a81dd1:2:1218556)
at https://mispaceuat.mihoyo.com/static/js/vendor.98f2a71fc60af3a81dd1.js?98f2a71fc60af3a81dd1:2:1170263
at e.unstable_runWithPriority (https://mispaceuat.mihoyo.com/static/js/vendor.98f2a71fc60af3a81dd1.js?98f2a71fc60af3a81dd1:2:1244959)
at Ia (https://mispaceuat.mihoyo.com/static/js/vendor.98f2a71fc60af3a81dd1.js?98f2a71fc60af3a81dd1:2:1169972)

报错原因分析:

  • webpack 打包重命名了改动过的 css 和 js 文件,并删除了原有的文件
    • 场景 1.用户正在浏览页面时你发包了,并且你启用了懒加载,用户的 html 文件中的 js 和 css 名称就和服务器上的不一致导致
    • 场景 2.用户浏览器有 html 的缓存,访问了上一个版本发布的资源导致
  • webpack 进行 code spilt 之后某些 bundle 文件 lazy loading 失败
  • 其他原因:
    • 服务器打包时没有进行rm -f public/dist/*操作
    • chunk 文件内容是不是被篡改(通过抓包排查)
    • 没有升级版本号导致的问题

关键

  • 刷新:会重新获取一遍 html 文档,chunk 对应信息也就刷新
    • 仅捕获到错误就刷新,很可能出现死循环,因为浏览器或类似于 Nginx 缓存设置的原因,浏览器不一定每次刷新去获取新的 index.html

解决方案

  • 方案 1:结合重试次数和重试间隔来重试,用 location.reload 方法,相当于触发 F5 刷新页面
    • 缺点:reload 方法,相当于触发 F5 刷新页面,用户会察觉加载刷新
    • 捕获到了 Loading chunk {n} failed 的错误时,重新渲染目标页面,通过正则检测页面出错:用 window.location.reload(true)刷新页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// prompt user to confirm refresh
function forceRefresh() {
// 设置只强制刷行一次页面
if (location.href.indexOf('#reloaded') === -1) {
location.href = location.href + '#reloaded';
window.location.reload(true);
// window.location.reload();
} else {
alert('请手动刷新页面!');
}
}
window.addEventListener('error', (e) => {
const pattern = /Loading chunk (\d)+ failed/g;
const isChunkLoadFailed = error.message.match(pattern);
// const isChunkLoadFailed = /Loading chunk [\d]+ failed/.test(e.message)
if (isChunkLoadFailed) forceRefresh();
// const targetPath = router.history.pending.fullPath;
// if (isChunkLoadFailed) router.replace(targetPath);
});
  • 方案 2:构建时静态资源路径带上版本信息

    • 如路径中携带,如原来请求/static/js/balabal.[hash].js,现在/[version]/static/balabal.[hash].js
  • 方案 3:增加配置让页面每次加载新数据而不是走缓存,同时让后端帮忙修改 Nginx,设置 no-cache,让页面不要每次去读取缓存

1
2
3
<!-- React 入口文件添加配置让页面每次加载新数据而不是走缓存, -->
<meta http-equiv="Cache-control" content="no-cache" />
<meta http-equiv="Cache" content="no-cache" />
  • 方案 4:尝试入口文件 client\index.tsx 处进行热更新
1
2
3
4
5
6
declare const module
if (process.env.NODE_ENV === 'development' && module.hot) {
module.hot.accept('./app', () => {
location.reload()
})
}
  • 方案 5(使用 Umi 框架的项目)

    • 1.将 .umirc.js 中的 publicPath 配置成 /
    • 2.如果构建的 dist 文件多加一层目录 base
    1
    2
    3
    4
    location /base {
    # 用于配合 browserHistory使用
    try_files $uri $uri/ /index.html;
    }
  • 方案 6
    找到 public/index.html 页面,在脚本上 src 前加上”/“

1
<script src="index.js"></script>

拓展

React.Lazy 的原理

1
lazyComponent is not a component but a function that returns a promise object. Inside of the promise that we return from componentLoader, we trigger the function (lazyComponent) and add handlers for promise resolve (.then) and reject(.catch). Since the successful resolution of promise is not a problem in our use case, we let React.lazy handle the resolved contents.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function componentLoader(lazyComponent) {
return new Promise((resolve, reject) => {
lazyComponent()
.then(resolve)
.catch((error) => {
// let us retry after 1500 ms
setTimeout(() => {
// call componentLoader again!
if (attemptsLeft === 1) {
reject(error);
return;
}
componentLoader(lazyComponent, attemptsLeft - 1).then(
resolve,
reject
);
// add one line to make it all work
}, 1500);
});
});
}

封装 retry 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 function retry(
fn: () => Promise<{
default: React.ComponentType;
}>,
retriesLeft = 100,
interval = 1000
) {
return new Promise<{
default: React.ComponentType;
}>((resolve, reject) => {
fn()
.then(resolve)
.catch((error) => {
setTimeout(() => {
if (retriesLeft === 1) {
reject(error);
return;
}
retry(fn, retriesLeft - 1, interval).then(resolve, reject);
}, interval);
});
});
}
component: lazy(() => retry(() => import("./pages/dashboard/Dashboard")))

Loading chunk {n} failed 的解决方法
https://www.pengsifan.com/2020/12/01/Loading chunk {n} failed/
作者
Van
发布于
2020年12月1日
许可协议