vue生产环境报错 ChunkLoadError

这周在测试环境中监控到一个前端异常ChunkLoadError: Loading chunk * failed
出现的频率并不是很高,一开始也没有在意。
后来看到一个bug,在业务中点击下一步的时候,无法正确的跳转页面,且后续点击上一步也有问题

现场还原

第一时间看了业务代码后,发现这个业务的路由配置大概如下:

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
export const routes = [
// ....
{
name:'业务办理',
path:'/service',
component: () => import('./service.vue')
children:[
{
name:'步骤1',
path:'/service/step1',
component: () => import('./service/step1.vue')
},
{
name:'步骤2',
path:'/service/step2',
component: () => import('./service/step2.vue')
},
{
name:'步骤3',
path:'/service/step3',
component: () => import('./service/step3.vue')
}
]
}
// ...
]

这时候我才意识到,是 Loading chunk * failed 这个错误导致的这个错误。
当我们点击下一步跳转路由的时候,如果懒加载这个路由文件失败,就会出现页面停留在当前页面的错误,但其实我们路由的hash或者history其实已经是下一个路由了。

寻找解决方法

  • 百度一下

    在搜索引擎一番搜索这个问题的解决方法后,看到很多阶段方案都是在vue-router中添加onError监听:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 在router.js 添加此段代码解决问题
    // 解决Loading chunk (\d)+ failed问题
    router.onError((error) => {
    const pattern = /Loading chunk (\d)+ failed/g;
    const isChunkLoadFailed = error.message.match(pattern);
    if (isChunkLoadFailed) {
    // 用路由的replace方法,并没有相当于F5刷新页面,失败的js文件并没有从新请求,会导致一直尝试replace页面导致死循环,而用 location.reload 方法,相当于触发F5刷新页面,虽然用户体验上来说会有刷新加载察觉,但不会导致页面卡死及死循环,从而曲线救国解决该问题
    location.reload();
    // const targetPath = $router.history.pending.fullPath;
    // $router.replace(targetPath);
    }
    });
    export default router;

    这个解决方法对于我们的场景是很契合,我们的业务是一个流程式的场景,如果在失败后暴力的刷新页面,对用户的体验是机器糟糕的。

  • 分析下我们的代码

    既然刷新页面这个场景不能满足我们的场景,那从我们的场景出发,就会发现,如果我们在这个业务场景下所有的路由是在一个chunk下,那是不是就不会出现办理中资源加载失败的问题啦?
    这个其实很好弄,直接加上/* webpackChunkName: "service" */就行啦

    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
    export const routes = [
    // ....
    {
    name:'业务办理',
    path:'/service',
    component: () => import(/* webpackChunkName: "service" */ './service.vue')
    children:[
    {
    name:'步骤1',
    path:'/service/step1',
    component: () => import(/* webpackChunkName: "service" */ './service/step1.vue')
    },
    {
    name:'步骤2',
    path:'/service/step2',
    component: () => import(/* webpackChunkName: "service" */ './service/step2.vue')
    },
    {
    name:'步骤3',
    path:'/service/step3',
    component: () => import(/* webpackChunkName: "service" */ './service/step3.vue')
    }
    ]
    }
    // ...
    ]

    本地打包一下,发现确实多了一个名为servicechunk
    Ok , 上测试 , 问题修复

  • 新问题

    上完测试后,确实在流程中,不会出现这个问题了,但是后台监控发现,出现了Loading chunk service failed
    这就尴尬了,说明我们只是解决了这个bug,并没有从根本上解决这个问题。

好的,那就先仔细分析下可能出现这个问题的原因吧:

  • 网络异常

    不考虑代码部署、环境和极限临界情况下,如果网络出现异常,那必然会出现 Loading chunk * failed
    • 解决方法

      • 请求加载失败的时候,是否可以重试一下
  • html缓存

    如果用户访问的版本是0.0.1的版本的应用,此时我们的应用版本是0.0.2,如果我们在部署新版本0.0.2的时候,删除或者覆盖了0.0.1的静态资源,那这个时候0.0.1版本就加载不到对应的资源了。
    • 解决方法

      • 禁止app入口文件index.html等html使用缓存
      • 确保不同版本的app的资源唯一,且可以并存在服务器上

如何解决

  • 失败后重试
    在搜索引擎搜索这个问题后,很快就发现,有一个webpack pluginwebpack-retry-chunk-load-plugin可以帮我们解决问题
    根据文档配置,配置后就可以实现失败后重试
  • 禁止app入口文件index.html等html使用缓存
    这个也简单,直接在html中加入缓存控制
    1
    2
    3
    4
    <!-- HTTP 1.1 -->
    <meta http-equiv="pragma" content="no-cache">
    <!-- HTTP 1.0 -->
    <meta http-equiv="cache-control" content="no-cache">
  • 确保不同版本的app的资源唯一,且可以并存在服务器上
    这一点其实就是两个问题,
    • 确保不同版本的app的资源唯一
      webpack在打包的时候可以在chunk后面添加上文件的hash,通过这个可以实现这个问题
    • 并存在服务器上
      这一点其实只要我们在部署的时候不删除原有文件,只是添加新文件到服务器上就可以啦

总结思路

对于这个问题,我们可以整理出如下顺序的解决步骤

  • html 禁止缓存
  • 指定懒加载组件的webpackChunkName,合理合并chunk
  • 使用webpack-retry-chunk-load-plugin,在失败后重试几次

额外思考

  • 异步加载路由的时候,在vue-router异步加载过程中,如果加上一个loading的交互,是不是可以提升用户体检?
  • 我们如何判断这个chunk是因为网络情况加载失败,还是请求404,这其实是两个问题?