原理和教程可以看参考资料,这篇主要是快速写出监听文件
整体异常处理方案需要实现的效果:
- 上报监控系统,能及时早发现、定位、解决问题
- 提升用户体验(UI降级)
项目中经常遇到的异常场景
- 语法错误
- 事件异常
- HTTP请求异常
- 静态资源加载异常
- Promise 异常
- Iframe 异常
- 页面崩溃
JS 七种错误类型
- Error 基类
- EvalError 表示全局函数
eval()
中发生的错误。 - RangeError 表示当一个值不在允许值的集合或范围内时出现错误。
arr.length = -1
- ReferenceError 当引用不存在的变量时,该对象表示错误。 xxx is not defined
- SyntaxError 不符合语言语法。 const = 222
- TypeError 参数类型不对。
- URIError decodeURIComponent 使用方式报错
decodeURIComponent('%')
const handleError = (error: any, type: "requestError" | "sourceError") => { let err_data: any = null; if (type === "requestError") { // 此时的 error 响应,它的 config 字段中包含请求信息 let { url, method, params, data } = error.config; err_data = { url, method, params: { query: params, body: data }, error: error.data?.message || JSON.stringify(error.data), }; } else if (type === "sourceError") { // 监测 error 是否是标准类型 if (error instanceof Error) { let { name, message } = error; err_data = { type: name, error: message, }; } else { err_data = { type: "other", error: JSON.stringify(error), }; } } };
事件/语法异常
使用 addEventListener
而不是直接使用 window.error
的原因是 window.error
无法捕捉 资源加载异常 ,这类异常只会在当前标签触发,无法冒泡到 window
,也就监听不到。但是在捕获过程中可以捕捉到。
// 运行错误 window.onerror = (message, source, lineno, colno, error) => { console.info({ message, source, lineno, colno, error }) handleError(error); // true 阻止执行默认事件处理函数 return true; }; // 资源加载错误 window.addEventListener('error', (error) => { if (!(e instanceof ErrorEvent)) { // 资源路径 e.target.src || e.target.href // 资源类型 e.target.tagName console.log(error.target.tagName, error.target.src); } handleError(error); }, true);
请求异常
监控页面发出的接口请求的耗时和异常
一般都是通过 axios 设置请求拦截/响应拦截进行的。
// 响应拦截器 axios.interceptors.response.use(function (response) { // 对响应数据处理 return response; }, function (error) { // 响应错误 return Promise.reject(error) } ) // promise 错误捕获 相当于一个全局的 Promise 异常兜底方案 window.addEventListener('unhandledrejection', (error) => { // 打印异常原因 console.log(error.reason); handleError(error); // 阻止控制台打印 error.preventDefault(); });
也可以通过重写 XMLHttpRequest
和 fetch
方法实现,主要目的是在请求状态改变的时候调用 handleError
方法。
const oldXMLHttpRequest = window.XMLHttpRequest; const newXMLHttpRequest = function XMLHttpRequest(props) { const xhr = new oldXMLHttpRequest(props); const send = xhr.send; const open = xhr.open; xhr.open = function () { // ... open.apply(xhr, arguments) } xhr.send = function () { // ... send.apply(xhr, arguments) } xhr.addEventListener('readystatechange', function (e) { if (!original_url || xhr.readyState !== 4) return; // 发送日志 handleError() }) return xhr } newXMLHttpRequest.prototype = oldXMLHttpRequest.prototype; window.XMLHttpRequest = newXMLHttpRequest const oldFetch = window.fetch; window.fetch = function () { // ... return oldFetch.apply(window, arguments).then(() => { // ... // handleError() }, () => { // ... // handleError() }) }
iframe 异常
// iframe 异常 window.frames[0].onerror = function (message, source, lineno, colno, error) { console.log('捕获到 iframe 异常:', { message, source, lineno, colno, error }); handleError(error); return true; };
React 处理异常
react 提供了 api 用来捕获异常:getDerivedStateFromError
和 componentDidCatch
.
通过这两个 api 就能简单实现一个符合要求的异常组件:
export default class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // 更新 state 使下一次渲染能够显示降级后的 UI return { hasError: true }; } componentDidCatch(error, errorInfo) { // 日志上报 console.log(error, errorInfo); } render() { if (this.state.hasError) { // 你可以自定义降级后的 UI 并渲染 return <h1>Something went wrong.</h1>; } return this.props.children; } }
TS
升级版,参考 react-error-boundary
实现一个多种传参方式,并且自带重置功能的组件。
import React, { Component, isValidElement } from "react"; const initialState = { error: null, }; const changedArray = (a = [], b = []) => { return ( a.length !== b.length || a.some((item, index) => !Object.is(item, b[index])) ); }; export interface ErrorBoundaryProps { children: React.ReactNode; resetKeys?: any; onResetKeysChange?: (prevKeys: any, key: any) => void; onError?: (error: Error, errorInfo: any) => void; onReset?: () => void; // ReactElement <div>出错啦</div> fallback?: () => void; // Fallback 组件 <Error /> FallbackComponent?: any; // 渲染 fallback 元素的函数 fallbackRender?: any; } export interface ErrorBoundaryState { hasError: boolean; error?: Error | undefined | null; } export default class ErrorBoundary extends Component< ErrorBoundaryProps, ErrorBoundaryState > { updatedWithError: boolean; public constructor(props: ErrorBoundaryProps) { super(props); this.updatedWithError = false; this.state = { hasError: false, error: null, }; } static getDerivedStateFromError(error: Error) { // 更新 state 使下一次渲染能够显示降级后的 UI return { error }; } componentDidCatch(error: Error, errorInfo: any) { // 错误日志上报 if (this.props.onError) { this.props.onError(error, errorInfo.componentStack); } } componentDidUpdate(prevProps: ErrorBoundaryProps) { const { error } = this.state; const { resetKeys, onResetKeysChange } = this.props; // 已经存在错误,并且是第一次由于 error 而引发的 render/update,那么设置 flag=true,不会重置 if (error !== null && !this.updatedWithError) { this.updatedWithError = true; return; } // 已经存在错误,并且是普通的组件 render,则检查 resetKeys 是否有改动,改了就重置 if (error !== null && changedArray(prevProps.resetKeys, resetKeys)) { if (onResetKeysChange) { onResetKeysChange(prevProps.resetKeys, resetKeys); } this.reset(); } } reset = () => { this.updatedWithError = false; this.setState(initialState); }; resetErrorBoundary = () => { // 允许用户点一下 fallback 里的一个按钮来重新加载出错组件,不需要重刷页面 if (this.props.onReset) { this.props.onReset(); } this.reset(); }; render() { const { fallback, FallbackComponent, fallbackRender } = this.props; const { error } = this.state; // 多种 fallback 的判断 if (error !== null) { const fallbackProps = { error, // 将 resetErrorBoundary 传入 fallback resetErrorBoundary: this.resetErrorBoundary, }; // 判断 fallback 是否为合法的 Element if (isValidElement(fallback)) { return fallback; } // 判断 render 是否为函数 if (typeof fallbackRender === "function") { return fallbackRender(fallbackProps); } // 判断是否存在 FallbackComponent if (FallbackComponent) { return <FallbackComponent {...fallbackProps} />; } } return this.props.children; } } // 使用 const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => { return ( <div role="alert"> <p>出错啦</p> <pre>{error.message}</pre> <button onClick={resetErrorBoundary}>Try again</button> </div> ); }; const onError = (error: Error) => { console.log(error); setHasError(true); }; const ErrorFallbackFn = ({ error, resetErrorBoundary }: FallbackProps) => { return ( <div role="alert"> <p>出错啦</p> <pre>{error.message}</pre> <button onClick={resetErrorBoundary}>Try again</button> </div> ); }; const Example = () => { const [hasError, setHasError] = useState(false); const [retry, setRetry] = useState<number>(0); const onError = (error: Error) => { console.log(error); // 日志上报 setHasError(true); }; const onReset = () => { console.log("尝试恢复错误"); setHasError(false); }; return ( <div> <button onClick={() => setRetry(retry + 1)}>retry</button> <ErrorBoundary FallbackComponent={ErrorFallback} onError={onError} onReset={onReset} resetKeys={[retry]} fallback={<div>出错啦</div>} fallbackRender={(fallbackProps) => <ErrorFallbackFn {...fallbackProps} />} > <Component /> </ErrorBoundary> </div> ); };
Vue 处理异常
Vue 异常处理通常有两种方式
- 最常用的一种是在全局
errorHandler
中写报错的回调函数 - 或者像 react 一样,全局定义一个 ErrorBoundary 组件,新组件外包裹一层
error-boundary
.
// main.js app.config.errorHandler = function (err, vm, info) { // handle error // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子 } Vue.component('ErrorBoundary', { data: () => ({ error: null }), errorCaptured(err, vm, info) { this.error = `${err.stack}\n\nfound in ${info} of component` return false }, render(h) { if (this.error) { return h('pre', { style: { color: 'red' } }, this.error) } // ignoring edge cases for the sake of demonstration return this.$slots.default[0] } }) // 使用 // <error-boundary> // <component/> // </error-boundary>
参考资料: