重新部署前端之后用户点击导航无法跳转新路由

1,308 阅读3分钟

之前一直有个问题没有解决,就是测试突然就发现页面点不动了,点击什么都没反应。通过排查发现是重新部署前端包之后,测试点击路由跳转找不到新的 js 文件导致的。一直没有解决这个问题。最近项目忙的差不多了,通过测试发现这种情况下页面会有一个 ChunkLoadError 的异常,应该是 webpack 自定义的异常。控制台报错如下: image2021-7-1_11-43-44.png

可以看到有 Uncaught(in promise) 这样的错误。这样的话,就可以对全局 window 添加一个异常捕获,来实现对用户的提示。让用户知道是项目更新导致的,然后自动强制刷新当前页面,用户就可以使用最新版的前端包了。大概思路如下:

window.addEventListener('unhandledrejection', evt => {
  if (evt?.reason?.name === 'ChunkLoadError') { // 发现找不到新的 js 文件
    const alertContent = (
      <Fragment>
        <p>
          <Icon className="mr-10" id={alertError} size={16} fill={errorColor} />
          平台可能已更新,关闭提示将自动刷新页面使用最新版
        </p>
        <p>如刷新失败请手动刷新</p>
      </Fragment>
    );
    const config = {
      width: 400,
      handleCancel: () => window.location.reload(),
      handleEnsure: () => window.location.reload()
    };
    Modal.alert(alertContent, config); // 弹框提示用户
  }
});

注意,这里的 uncaught(in promise) 的错误就是 promise 没有添加 catch 冒泡上来的错误,这种错误是通过 unhandledrejection 来捕获。

最终效果如下图

image.png

后续

最近把项目中的 react-router 升级了一下,从 3.x 升级到了 5.x,然后发现上面的这个提示没有了。看了下控制台,发现上面的 uncaught(in promise) 的错误没有了,只剩下了一个错误。如下图

image.png

貌似是因为升级 react-router 之后,把原来 webpack 中通过 require.ensure 的方式动态获取组件的方式替换成了 React.lazy(import()) 导致的。然后这个错误可能不会冒泡抛出了,所以上面的捕获失效了。导致这个小功能也就失效了。

查看 webpack 的文档,文档中说的也是 require.ensure 是依赖 promise 的,所以应该是这个原因。

// 原来的动态引入方式
import {injectReducer} from '../../store/reducers';

export default store => ({
  path: 'manage-intel',
  /*  Async getComponent is only invoked when route matches   */
  getComponent(nextState, cb) {
    /*  Webpack - use 'require.ensure' to create a split point
        and embed an async module loader (jsonp) when bundling   */
    require.ensure(
      [],
      require => {
        /*  Webpack - use require callback to define
          dependencies for bundling   */
        const component = require('./components').default;
        const reducer = require('./reducers').default;

        /*  Add the reducer to the store on key 'counter'  */
        injectReducer(store, {key: 'manageIntelData', reducer});

        /*  Return getComponent   */
        cb(null, component);

        /* Webpack named bundle   */
      },
      'manageIntel'
    );
  }
});
// 现在的动态引入方式
import React, {lazy, Suspense} from 'react';
import PropTypes from 'prop-types';
import {Loading} from 'tdp-ui';
import {injectReducer} from '../../store/reducers';
import reducer from './reducers/index';

const ManageIntel = lazy(() => import(/* webpackChunkName: "manageIntel" */ './components'));

export default function ManageIntelPage({store, title}) {
  injectReducer(store, {key: 'manageIntelData', reducer});
  return (
    <Suspense fallback={<Loading />}>
      <ManageIntel store={store} title={title} />
    </Suspense>
  );
}

ManageIntelPage.propTypes = {
  store: PropTypes.object,
  title: PropTypes.oneOfType([PropTypes.array, PropTypes.string])
};

查了一些文章,有些是通过 window.addEventListener('error', fn) 来进行处理的,但是我这里也捕获不到。所以只能通过 ErrorBoundary 来进行提示了。当然也可以在 import() 后面添加 catch 来进行捕获,但是这样的话,每一个 import 都要进行添加,感觉有点费劲,所以目前还是决定用 ErrorBoundary 来进行添加。

后续的后续

升级 react-router 之后,把动态引用的方式也顺便改了一下,从 require.ensure 改成了 React.lazy 的方式,因为 require.ensure 使用的是 promise,所以会报 promise 的错误。然后通过捕获 promise 的错误可以实现最开始想要的效果。现在的错误是直接显示 error,通过 window.addEventListener('error', callback) 发现捕获不到,原因是在最外层的 container 加了 errorDec,导致错误无法上升到 container,所以捕获不到。然后把原来在 layout 中的异常捕获提升到了最外层的 container 组件中。通过把对 promise 的异常捕获换成 error 捕获就可以了。

新的错误:

image.png

image.png

捕获错误效果和最后的展示效果如下图

image.png

代码如下:

// src/container/AppContainer.js
// Uncaught ChunkLoadError 对应 js 加载错误,也就是对应的 js 发生了更新
// Loading CSS chunk .* failed 对应 css 加载错误,也就是对应的 css 发生了更新
   componentDidMount() {
     window.addEventListener('error', evt => {
       if (/Uncaught ChunkLoadError|Loading CSS chunk .* failed/.test(evt?.message)) {
         Modal.alert(alertContent, alertConfig);
       }
     });
   }

参考

rollbar.com/blog/javasc… medium.com/@botfather/… gist.github.com/raphael-leg… raphael-leger.medium.com/react-webpa…