【前端监控】前端异常捕获与处理

本文涉及的产品
应用实时监控服务ARMS - 应用监控,每月50GB免费额度
日志服务 SLS,月写入数据量 50GB 1个月
简介: 【前端监控】前端异常捕获与处理

image.png


原理和教程可以看参考资料,这篇主要是快速写出监听文件

整体异常处理方案需要实现的效果:

  • 上报监控系统,能及时早发现、定位、解决问题
  • 提升用户体验(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();
});

也可以通过重写 XMLHttpRequestfetch 方法实现,主要目的是在请求状态改变的时候调用 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 用来捕获异常:getDerivedStateFromErrorcomponentDidCatch.

通过这两个 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>


参考资料:

目录
相关文章
|
9月前
|
Web App开发 前端开发
【前端异常】Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.
【前端异常】Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.
539 0
|
24天前
|
缓存 监控 前端开发
前端性能监控:从Lighthouse到Real User Monitoring
**前端性能监控关乎用户体验。Lighthouse是自动化审计工具,评估网页性能、最佳实践、可访问性等,通过CLI或Chrome DevTools使用。RUM则实时监控用户与网站互动,收集性能数据。两者结合,从开发到生产环境,全面优化前端性能,包括资源加载、代码优化、网络性能和用户体验。使用Lighthouse和RUM数据,结合CI/CD,持续改进并设定性能预算,采用SSR、Service Worker、Code Splitting等高级策略,确保高性能和用户满意度。**
24 2
|
5天前
|
Web App开发 存储 监控
如何使用 Chrome DevTools 进行前端性能监控和调试?
如何使用 Chrome DevTools 进行前端性能监控和调试?
|
1月前
|
监控 前端开发 JavaScript
|
2月前
|
JSON JavaScript 前端开发
vue的 blob文件下载文件时,后端自定义异常,并返回json错误提示信息,前端捕获信息并展示给用户
vue的 blob文件下载文件时,后端自定义异常,并返回json错误提示信息,前端捕获信息并展示给用户
|
2月前
|
前端开发 JavaScript 索引
【Web 前端】JS的几种具体异常类型(报错)
【4月更文挑战第22天】【Web 前端】JS的几种具体异常类型(报错)
|
9月前
|
前端开发
【前端异常】Module build failed: Error: ENOENT: no such file or directory, scandir ‘G:\OPWeb\public\node_m
【前端异常】Module build failed: Error: ENOENT: no such file or directory, scandir ‘G:\OPWeb\public\node_m
118 0
|
9月前
|
前端开发
【前端异常】解决前端引入Bootstrap的dropdowns 菜单时报错,Uncaught TypeError: Bootstrap‘s dropdowns require Popper.js
【前端异常】解决前端引入Bootstrap的dropdowns 菜单时报错,Uncaught TypeError: Bootstrap‘s dropdowns require Popper.js
78 0
|
11月前
|
数据采集 监控 前端开发
前端性能和错误监控(一)
前端性能和错误监控
101 0
|
2月前
|
存储 监控 前端开发
JavaScript手册:公司员工电脑监控软件前端交互的代码设计
在当今信息时代,随着公司对员工电脑活动的监控需求不断增加,前端交互的代码设计变得尤为关键。本手册将深入探讨JavaScript编写的公司员工电脑监控软件监控代码,着重介绍如何设计能够在不引起怀疑的情况下,实现对员工电脑活动的细致监控。
239 2

热门文章

最新文章