into-piece / Step-By-Step

每天一题向前端架构师前进
4 stars 1 forks source link

项目异常捕获 #44

Open into-piece opened 3 years ago

into-piece commented 3 years ago

crm是一个多tag页面的SPA项目,打包发布完,用户在没有刷新页面的情况下使用会偶尔出现白屏或一直loading无响应,控制台出现load chunk fail error的报错,分析之后是因为切换页面会发起对对应页面分包代码资源的请求,而当前旧资源已经被清除了。

我引入了ErrorBoundary组件,通过新增的staic getderivedstatefromerror和componentDidcatch生命周期显示备用的样式fallback UI。当我用staic getderivedstatefromerror获取error堆栈信息,设置state中isError为true显示对应友好的报错页面,componentDidcatch进行错误上报,心满意足地觉得毫无破绽的时候,发现其实是没办法捕获到加载分包失败的错误的。

异常捕获是在渲染过程中workloop进行捕获的,而接口报错和事件处理器onClick对应回调内部的js报错则无法捕捉。

这个时候我们可以根据加载分包的loading组件来设置定时,若加载超过一定限制则报错,通过全局状态库保存error值,errorboundry组件获取值进行错误显示,显示替补样式,提醒用户进行刷新。

  const [isTimeout, setIsTimeout] = useState(false);
  useEffect(() => {
    let timer: NodeJS.Timeout;
    if (error) {
      // @ts-ignore
      clearInterval(timer);
    }
    if (!isTimeout) {
      timer = setTimeout(() => {
        setIsTimeout(true);
      }, 4000);
    }
    return () => {
      clearTimeout(timer);
    };
  }, [isTimeout, error]);

但又出现了新问题,我们多tag模式下刷新的话导致其他页面一起刷新,又要重新打开页面录入一堆信息,是你恼火不恼火?那么能不能刷新单个页面吗,重新请求当前页面的分包呢?但是我发现请求失败了如何重新请求分包呢?

我们用的是umi框架,进入源码可以看到代码分割的库是loadable,又是熟悉的老朋友,看一下代码分割实现的源码

umi/packages/runtime/src/dynamic/loadable.js


const LoadableComponent = (props, ref) => {
init();
const context = useContext(LoadableContext);
const state = useSubscription(subscription);

useImperativeHandle(ref, () => ({
  retry: subscription.retry,
}));

if (context && Array.isArray(opts.modules)) {
  opts.modules.forEach((moduleName) => {
    context(moduleName);
  });
}

if (state.loading || state.error) {
  if (process.env.NODE_ENV === 'development' && state.error) {
    console.error(`[@umijs/runtime] load component failed`, state.error);
  }
  return createElement(opts.loading, {
    isLoading: state.loading,
    pastDelay: state.pastDelay,
    timedOut: state.timedOut,
    error: state.error,
    retry: subscription.retry,
  });
} else if (state.loaded) {
  return opts.render(state.loaded, props);
} else {
  return null;
}

};

const LoadableComponentWithRef = forwardRef(LoadableComponent); // add static method in React.forwardRef // https://github.com/facebook/react/issues/17830 LoadableComponentWithRef.preload = () => init(); LoadableComponentWithRef.displayName = 'LoadableComponent';

return LoadableComponentWithRef; }


原来已经暴露了error信息和retry方法,利用retry方法我们可以提供重新刷新当前页面的能力,来重新请求当前分包。
into-piece commented 3 years ago

当然我们可以用lazy+suspense进行代码分割,这也是react官方推荐的方法,suspense组件fallback属性,可以设置对应的备用UI

into-piece commented 3 years ago

那react中没有两个生命周期组件,我们需要如何捕获项目中的异常呢?sentry的实现方法?

  1. try catch
  2. window.onError:setTimeout的报错是try catch无法捕获的,但可以用window.onError捕获
  3. Window: unhandledrejection event:promise的报错是封存在内部的,外部无法感知,我们可以监听unhandledrejection事件,当Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件