背景:

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

error info

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)刷新页面
// 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:让页面每次加载新数据而不是走缓存,React入口文件,加入;同时让后端帮忙修改Nginx,设置no-cache,让页面不要每次去读取缓存

<meta http-equiv="Cache-control" content="no-cache">
<meta http-equiv="Cache" content="no-cache">
  • 方案4:对比 test 和 uat 环境 config 文件配置

  • 方案5:尝试入口文件 client\index.tsx 处进行热更新

declare const module: any
if (process.env.NODE_ENV === 'development' && module.hot) {
module.hot.accept('./app', () => {
  location.reload()
})
}

拓展

react错误边界

网易开源库:https://github.com/x-orpheus/catch-react-error

GitHub讨论:https://github.com/nuxt/nuxt.js/issues/742

React.Lazy 的原理

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.
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方法

function retry(
  fn: () => Promise<{
    default: React.ComponentType<any>;
  }>,
  retriesLeft = 100,
  interval = 1000
) {
  return new Promise<{
    default: React.ComponentType<any>;
  }>((resolve, reject) => {
    fn()
      .then(resolve)
      .catch((error: any) => {
        setTimeout(() => {
          if (retriesLeft === 1) {
            reject(error);
            return;
          }
          retry(fn, retriesLeft - 1, interval).then(resolve, reject);
        }, interval);
      });
  });
}
component: lazy(() => retry(() => import("./pages/dashboard/Dashboard")))

交流


1、QQ群:可添加qq群共同进阶学习: 进军全栈工程师疑难解  群号:   856402057

2、公众号:公众号「进军全栈攻城狮」 ,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!通过公众号可加我vx拉群

 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐