原理和教程可以看参考资料,这篇主要是快速写出监听文件
整体异常处理方案需要实现的效果:
- 上报监控系统,能及时早发现、定位、解决问题
- 提升用户体验(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>
参考资料: