CD's blog CD's blog
首页
  • HTMLCSS
  • JavaScript
  • Vue
  • TypeScript
  • React
  • Node
  • Webpack
  • Git
  • Nestjs
  • 小程序
  • 浏览器网络
  • 学习笔记

    • 《TypeScript 从零实现 axios》
    • Webpack笔记
  • JS/TS教程

    • 《现代JavaScript》教程
🔧工具方法
  • 网站
  • 资源
  • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

CD_wOw

内卷的行情,到不了的梦
首页
  • HTMLCSS
  • JavaScript
  • Vue
  • TypeScript
  • React
  • Node
  • Webpack
  • Git
  • Nestjs
  • 小程序
  • 浏览器网络
  • 学习笔记

    • 《TypeScript 从零实现 axios》
    • Webpack笔记
  • JS/TS教程

    • 《现代JavaScript》教程
🔧工具方法
  • 网站
  • 资源
  • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 基础原理及工具方法

    • JavaScript知识点
    • Javascript 事件流
    • 设计模式应用
    • 原型和继承
    • PromiseA+规范的实现
    • 前端模块化及进程
    • 前端性能异常监控
      • 性能监控
        • performance.timing 属性
        • performance.navigation 属性
        • performance.memory 属性
      • 前端包含的异常
      • 监控错误的流程
      • 异常的捕获
        • window.onerror
        • 静态资源加载异常
        • Promise 异常
        • Vue 异常
        • ajax 请求异常
        • 跨域问题
        • 错误存储
      • 监听用户行为
        • 监听点击事件
        • 监听 console
      • 监听页面的跳转
        • 单页面的 hash history
      • 实现上报的方式
        • XMLHttpRequest
        • 动态图片上报
        • sendBeacon
      • 错误日志缓存
      • 相关问题和文章
    • 工作用得到的一些JS方法
    • 项目代码层面优化方法
  • Babel

  • WebComponent

  • 专项知识汇总

  • JavaScript笔记
  • 基础原理及工具方法
CD
2021-09-01
目录

前端性能异常监控

# 性能监控

Performance 是一个做前端性能监控离不开的 API,最好在页面完全加载完成之后再使用,因为很多值必须在页面完全加载之后才能得到。最简单的办法是在 window.onload 事件中读取各种数据。

前端性能统计的数据大致有以下几个:

  • 白屏时间:从打开网站到有内容渲染出来的时间节点;
  • 首屏时间:首屏内容渲染完毕的时间节点;
  • 用户可操作时间节点:domready 触发节点;
  • 总下载时间:window.onload 的触发节点。

# performance.timing 属性

从输入 url 到用户可以使用页面的全过程时间统计,会返回一个 PerformanceTiming 对象,单位均为毫秒 performance

按触发顺序排列所有属性:

  • navigationStart:在同一个浏览器上下文中,前一个网页(与当前页面不一定同域)unload 的时间戳,如果无前一个网页 unload ,则与 fetchStart 值相等
  • unloadEventStart:前一个网页(与当前页面同域)unload 的时间戳,如果无前一个网页 unload 或者前一个网页与当前页面不同域,则值为 0
  • unloadEventEnd:和 unloadEventStart 相对应,返回前一个网页 unload 事件绑定的回调函数执行完毕的时间戳
  • redirectStart:第一个 HTTP 重定向发生时的时间。有跳转且是同域名内的重定向才算,否则值为 0
  • redirectEnd:最后一个 HTTP 重定向完成时的时间。有跳转且是同域名内的重定向才算,否则值为 0
  • fetchStart:浏览器准备好使用 HTTP 请求抓取文档的时间,这发生在检查本地缓存之前
  • domainLookupStart:DNS 域名查询开始的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
  • domainLookupEnd:DNS 域名查询完成的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
  • connectStart:HTTP(TCP) 开始建立连接的时间,如果是持久连接,则与 fetchStart 值相等,如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接开始的时间
  • connectEnd:HTTP(TCP) 完成建立连接的时间(完成握手),如果是持久连接,则与 fetchStart 值相等,如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接完成的时间

    注意:这里握手结束,包括安全连接建立完成、SOCKS 授权通过

  • secureConnectionStart:HTTPS 连接开始的时间,如果不是安全连接,则值为 0
  • requestStart:HTTP 请求读取真实文档开始的时间(完成建立连接),包括从本地读取缓存,连接错误重连时,这里显示的也是新建立连接的时间
  • responseStart:HTTP 开始接收响应的时间(获取到第一个字节),包括从本地读取缓存
  • responseEnd:HTTP 响应全部接收完成的时间(获取到最后一个字节),包括从本地读取缓存
  • domLoading:开始解析渲染 DOM 树的时间,此时 Document.readyState 变为 loading,并将抛出 readystatechange 相关事件
  • domInteractive:完成解析 DOM 树的时间,Document.readyState 变为 interactive,并将抛出 readystatechange 相关事件 注意:只是 DOM 树解析完成,这时候并没有开始加载网页内的资源
  • domContentLoadedEventStart:DOM 解析完成后,网页内资源加载开始的时间,文档发生 DOMContentLoaded 事件的时间
  • domContentLoadedEventEnd:DOM 解析完成后,网页内资源加载完成的时间(如 JS 脚本加载执行完毕),文档的 DOMContentLoaded 事件的结束时间
  • domComplete:DOM 树解析完成,且资源也准备就绪的时间,Document.readyState 变为 complete,并将抛出 readystatechange 相关事件
  • loadEventStart:load 事件发送给文档,也即 load 回调函数开始执行的时间,如果没有绑定 load 事件,值为 0
  • loadEventEnd:load 事件的回调函数执行完毕的时间,如果没有绑定 load 事件,值为 0

常用计算:

  • DNS 查询耗时 :domainLookupEnd - domainLookupStart
  • TCP 链接耗时 :connectEnd - connectStart
  • request 请求耗时 :responseEnd - responseStart
  • 解析 dom 树耗时 : domComplete - domInteractive
  • 白屏时间 :responseStart - navigationStart
  • domready 时间(用户可操作时间节点) :domContentLoadedEventEnd - navigationStart
  • onload 时间(总下载时间) :loadEventEnd - navigationStart

# performance.navigation 属性

告诉开发者当前页面是通过什么方式导航过来的,只有两个属性:type,redirectCount

type:标志页面导航类型

| type 常数 | 枚举值 | 描述 | | TYPE_NAVIGATE | 0 | 普通进入,包括:点击链接、在地址栏中输入 URL、表单提交、或者通过除下表中 TYPE_RELOAD 和 TYPE_BACK_FORWARD 的方式初始化脚本。 | | TYPE_RELOAD | 1 | 通过刷新进入,包括:浏览器的刷新按钮、快捷键刷新、location.reload()等方法。 | | TYPE_BACK_FORWARD | 2 | 通过操作历史记录进入,包括:浏览器的前进后退按钮、快捷键操作、history.forward()、history.back()、history.go(num)。 | | TYPE_UNDEFINED | 255 | 其他非以上类型的方式进入。 |

redirectCount:表示到达最终页面前,重定向的次数,但是这个接口有同源策略限制,即仅能检测同源的重定向

# performance.memory 属性

描述内存多少,是在 Chrome 中添加的一个非标准属性。

  • jsHeapSizeLimit: 内存大小限制
  • totalJSHeapSize: 可使用的内存
  • usedJSHeapSize: JS 对象(包括 V8 引擎内部对象)占用的内存,不能大于 totalJSHeapSize,如果大于,有可能出现了内存泄漏

# 前端包含的异常

  1. js 编译异常
  2. js 运行时异常
  3. 静态资源加载异常
  4. 接口请求异常

# 监控错误的流程

监控错误 -> 搜集错误 -> 存储错误 -> 分析错误 -> 错误报警-> 定位错误 -> 解决错误

# 异常的捕获

下面会分别写出对前端的各类异常的捕获说明及部分伪代码

# window.onerror

js 发生运行错误时(包括语法错误),window 会触发一个 ErrorEvent 接口的 error 事件,并执行 window.onerror()。它无法捕获静态资源异常和 js 代码错误

例如(下面的是伪代码):

/**
 * @param message 错误信息(字符串)。可用于HTML onerror=""处理程序中的event。
 * @param source 发生错误的脚本URL(字符串)
 * @param lineno 发生错误的行号(数字)
 * @param colno 发生错误的列号(数字)
 * @param error Error对象(对象)
 */
window.onerror = (message, source, lineno, colno, error) => {
  try {
    console.log("js报错:\n", error);
    this.errorType = ErrorEnums.JS_ERROR; //错误类型
    this.errorAlert = AlertEnums.WARN; //错误等级
    this.msg = message; //错误信息
    this.url = source; //错误信息地址
    this.line = lineno || ""; //行数
    this.col = colno || ""; //列数
    this.errorStack = error || ""; //错误堆栈
    this.handleReportError();
    return true; // 返回true异常才不会向上抛出
  } catch (error) {
    console.error("js异常错误", error);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 静态资源加载异常

通常我们通过监听 error 事件来进行对例如图片、文件资源加载失败时的异常。由于网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,缺点是不知道资源加载失败时 http 的状态。

例如:伪代码

window.addEventListener(
  "error",
  (e: any) => {
    try {
      if (!e) {
        return;
      }
      const target = e.target || e.srcElement;
      // 目前只考虑 e.target.localName : link/script/img 的资源加载错误
      if (
        !(
          target instanceof HTMLScriptElement ||
          target instanceof HTMLLinkElement ||
          target instanceof HTMLImageElement
        )
      ) {
        return;
      }
      console.log("resouce报错:\n", e);
      this.errorType = ErrorEnums.RESOURCE_ERROR;
      this.errorAlert = AlertEnums.ERROR;
      // 记录异常资源地址
      if (target instanceof HTMLLinkElement) {
        this.url = target.href;
      } else {
        this.url = target.src;
      }
      // 异常资源文件名
      this.msg = `资源${target.localName}引用错误`;
      this.errorStack = target;
      this.handleReportError();
    } catch (error) {
      console.log(error);
    }
  },
  true
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# Promise 异常

Promise 的异常不能通过 onerror 和 try catch 捕获,需要我们监听 unhandledrejection 在捕获的阶段进行。

window.addEventListener(
  "unhandledrejection",
  (e: PromiseRejectionEvent) => {
    try {
      if (!e || !e.reason) {
        return;
      }
      console.log("promise报错:\n", e);
      e.preventDefault();
      if (e.reason.config && e.reason.config.url) {
        this.url = e.reason.config.url;
      } else {
        this.url = window.location.href;
      }
      if (e.target) {
        const target: any = e.target;
        if (target?.cdreport?.method) {
          this.errorMethod = target.cdreport.method;
        }
        this.status = target.status || "";
        this.statusText = target.statusText || "";
      }
      this.errorAlert = AlertEnums.WARN;
      this.errorType = ErrorEnums.PROMISE_ERROR;
      this.msg = e.reason;
      this.handleReportError();
      return true;
    } catch (error) {
      console.log(error);
    }
  },
  true
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# Vue 异常

Vue 为我们提供了错误事件的方法: vue.config.errorHandler

function formatComponentName(vm: any) {
  if (vm.$root === vm) return "root";
  var name = vm._isVue
    ? (vm.$options && vm.$options.name) ||
      (vm.$options && vm.$options._componentTag)
    : vm.name;
  return (
    (name ? "component <" + name + ">" : "anonymous component") +
    (vm._isVue && vm.$options && vm.$options.__file
      ? " at " + (vm.$options && vm.$options.__file)
      : "")
  );
}
try {
  Vue.config.errorHandler = (err: Error, vm: any, info: any) => {
    this.errorType = ErrorEnums.VUE_ERROR;
    this.errorAlert = AlertEnums.WARN;
    const data: messageData = {
      stack: err.stack,
      info: info,
    };
    console.log("vue报错:\n", err);
    if (Object.prototype.toString.call(vm) === "[object Object]") {
      data.componentName = this.formatComponentName(vm);
      data.propsData = vm.$options.propsData;
    }
    this.msg = err.message;
    this.url = window.location.href;
    this.errorStack = data;
    this.handleReportError();
  };
} catch (error) {
  console.log("vue异常监听出错:", error);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# ajax 请求异常

Ajax 的监听主要是为了发现服务接口返回值的问题

/**
 * Ajax的监听主要是为了发现服务接口返回值的问题
 * abort:外部资源中止加载时(比如用户取消)触发。如果发生错误导致中止,不会触发该事件。
 * error:由于错误导致外部资源无法加载时触发。
 * load:外部资源加载成功时触发。
 * loadstart:外部资源开始加载时触发。
 * loadend:外部资源停止加载时触发,发生顺序排在error、abort、load等事件的后面。但却不提供进度结束的原因
 * progress:外部资源加载过程中不断触发。
 * timeout:加载超时时触发。
 */
// 重写send和open方法
const XmlSend = window.XMLHttpRequest.prototype.send;
const XmlOpen = window.XMLHttpRequest.prototype.open;
const _handleFunc = (e: ProgressEvent | Event) => {
  /**
   * currentTarget 返回当前触发事件的元素/ target 返回触发事件触发的源头元素
   */
  const target: any = e.target;
  if (target && target.status !== 200) {
    console.log("ajax报错:\n", e);
    this.errorType = ErrorEnums.AJAX_ERROR; //错误类型
    this.errorAlert = AlertEnums.WARN; //错误等级
    if (e.target) {
      const target: any = e.target;
      if (target?.cdreport?.method) {
        this.errorMethod = target.cdreport.method;
      }
      this.status = target.status || "";
      this.statusText = target.statusText || "";
    }
    const cdreport = target.cdreport || {};
    this.msg = target.response;
    this.url = target.responseURL || cdreport.rootUrl;
    this.errorStack = {
      status: target.status,
      statusText: target.statusText,
      eventType: e.type,
      timeStamp: e.timeStamp,
      ...cdreport,
    };
    this.handleReportError();
  }
};
XMLHttpRequest.prototype.open = function(...argument: any) {
  const [method, rootUrl] = argument;
  const _self: any = this;
  _self["cdreport"] = {
    method,
    rootUrl,
  };
  return XmlOpen.apply(this, argument);
};
XMLHttpRequest.prototype.send = function(...argument: any) {
  if (this.addEventListener) {
    this.addEventListener("error", _handleFunc, false);
    this.addEventListener("load", _handleFunc, false);
    this.addEventListener("abort", _handleFunc, false);
    this.addEventListener("timeout", _handleFunc, false);
  } else if (this.onreadystatechange) {
    // 重写onreadystatechange
    const tempStateChange = this.onreadystatechange;
    this.onreadystatechange = function(e) {
      tempStateChange.apply(this, argument);
      if (this.readyState === 4) {
        _handleFunc(e);
      }
    };
  }
  return XmlSend.apply(this, argument);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

# 跨域问题

实际上,当我们捕获到跨域的问题的时候,window.onerror 只会告知我们出现了一个 Script Error,并不能为我们准确的标识这个错误出现的位置,通常,我们可以通过前后端对跨域配置将其应允。若不能则需要考虑通过使用 try/catch 绕过,将错误抛出。一般调用远端 js,有下列三种常见情况

  • 调用远端 JS 的方法出错
  • 远端 JS 内部的事件出问题
  • setTimeout 等回调内出错

通常对于外部调用的方法,由于浏览器不会对 try-catch 起来的异常进行跨域拦截,所以我们采用劫持原生方法,将原生的方法用 try/catch 的函数包裹处理

这段代码来自讲透自研的前端错误监控 (opens new window) 当我们去掉 addEventListener 的重写时,会发现我们只会报一个 Script Error,但我们希望能获得完整的错误堆栈,所以我们采用劫持原生事件,将其进行 try/catch ,然后再抛出异常 throw error,重新抛出异常的时候执行的是同域代码,所以能拿到完整的堆栈错误信息。从而实现错误捕获上报。

<html>
  <head>
    <title>Test page in http://test.com</title>
  </head>
  <body>
    <script>
      const originAddEventListener = EventTarget.prototype.addEventListener;
      EventTarget.prototype.addEventListener = function(
        type,
        listener,
        options
      ) {
        const wrappedListener = function(...args) {
          try {
            return listener.apply(this, args);
          } catch (err) {
            throw err;
          }
        };
        return originAddEventListener.call(
          this,
          type,
          wrappedListener,
          options
        );
      };
    </script>
    <div style="height: 9999px;">http://test.com</div>
    <script src="https://yun.dui88.com/tuia/cdn/remote/error_scroll.js"></script>
    <script>
      window.onerror = function(message, url, line, column, error) {
        console.log(message, url, line, column, error);
      };
    </script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 错误存储

当我们

# 监听用户行为

这里只做监听点击事件和 console

# 监听点击事件

window.addEventListener("click", handleClick, true);
// handleClick事件定义
export function handleClick(event) {
  var target;
  try {
    target = event.target;
  } catch (u) {
    target = "<unknown>";
  }
  if (0 !== target.length) {
    var behavior: clickBehavior = {
      type: "ui.click",
      data: {
        message: (function(e) {
          if (!e || 1 !== e.nodeType) return "";
          for (
            var t = e || null, n = [], r = 0, a = 0, i = " > ".length, o = "";
            t &&
            r++ < 5 &&
            !(
              "html" === (o = normalTarget(t)) ||
              (r > 1 && a + n.length * i + o.length >= 80)
            );

          )
            n.push(o), (a += o.length), (t = t.parentNode);
          return n.reverse().join(" > ");
        })(target),
      },
    };
    // 空信息不上报
    if (!behavior.data.message) return;
    let commonMsg = getCommonMsg();
    let msg: behaviorMsg = {
      ...commonMsg,
      ...{
        t: "behavior",
        behavior,
      },
    };
    report(msg);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 监听 console

本质就是重写 console 事件,console 又包括 log\ debug\ info\ warn\ error

const consoleEnums = ["debug", "info", "warn", "log", "error"];
for (let i = 0; i < consoleEnums.length; i++) {
  const con = window.console[consoleEnums[i]];
  if (!con) continue;
  const result = Array.prototype.slice.apply(arguments);
  const report = {
    type: consoleEnums[i],
    mssage: JSON.stringify(result),
  };
  action && action.apply(null, result);
}
1
2
3
4
5
6
7
8
9
10
11

# 监听页面的跳转

# 单页面的 hash history

实质上是对 hash 的 hashchange 事件和 history 的 history.pushState\history.replaceState 事件进行重写,道理和 console 是一样的,就不写 demo 了。其 vue 和 react 的 router 也是基于对这些事件的重写来完成的。

# 实现上报的方式

首先,对于引入的性能、上报 sdk,不应阻塞到页面正常的加载,也就是阻塞页面的 js 资源,所以我们也需要支持异步加载的形式并处理错误上报

伪代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script>
      (function(w) {
        w._error_storage_ = [];
        function errorhandler() {
          // 用于记录当前的错误
          w._error_storage_ && w._error_storage_.push([].slice.call(arguments));
        }
        w.addEventListener && w.addEventListener("error", errorhandler, true);
        var times = 3,
          appendScript = function appendScript() {
            var sc = document.createElement("script");
            (sc.async = !0),
              (sc.src = "./s数据上报.js"), // 取决于你存放的位置
              (sc.onerror = function() {
                times--, times > 0 && setTimeout(appendScript, 1500);
              }),
              document.head && document.head.appendChild(sc);
          };
        setTimeout(appendScript, 1500);
      })(window);
    </script>
  </head>
  <body></body>
</html>

// fundebug 的 异步加载
<script type="text/javascript">
  function loadScript(url, apikey) {
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = url;
    script.setAttribute("apikey", apikey);
    document.body.appendChild(script);
  }

  loadScript("https://js.fundebug.cn/fundebug.2.8.0.min.js", "API-KEY");
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

对于日志的上报形式,有以下三种方案

  1. XMLHttpRequest 发送请求
  2. navigator.sendBeacon
  3. image 的形式

# XMLHttpRequest

通过 ajax 进行数据的发送,这个方式有一个缺陷,就是当页面正在卸载或是刷新时,未完成的上报任务可能会在未发送到服务器的时候就被浏览器 cancel 掉,导致异常的上报缺失,所以要采用 XMLHttpRequest 的方式进行同步请求,同步之后会阻塞页面关闭或重新加载的过程,请求是到了,缺陷是会造成阻塞影响用户体验

ajaxReport(url: string, data: any, method = "POST") {
    return new Promise((resolve, reject) => {
      const dataStr = JSON.stringify(data);
      const sa = new FormData()
      Object.entries(data).forEach((item:any) => {
        sa.append(item[0], item[1])
      })
      console.log(sa, data, dataStr)
      const xhr = window.XMLHttpRequest
        ? new XMLHttpRequest()
        : new ActiveXObject("Microsoft.XMLHTTP");
      if (method === "POST") {
        xhr.open(method, url, true);
        // xhr.setRequestHeader(
        //   "Content-Type",
        //   "application/x-www-form-urlencoded"
        // );
        xhr.send(dataStr);
      } else if (method === "GET") {
        xhr.open("GET", url + "?" + dataStr, true);
        xhr.send();
      }
      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 200) {
          resolve(JSON.parse(xhr.responseText));
        }
        if (xhr.readyState === 4 && xhr.status !== 200) {
          reject('ERROR:' + xhr.statusText);
        }
      }
    })
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 动态图片上报

当我们需要上报的地址是一个非同域的地址时,所有的接口都会造成跨域。使用图片不仅不用插入 DOM,只需要在 js 中 new 出 Image 对象就能发起请求, 通过创建一个图片并触发图片的 onload 事件,将数据拼接在 url 上传递到后端。因为绝大多数浏览器会延迟卸载以保证图片的载入,所以数据可以在卸载事件中发送。 但如果某些浏览器在实现上无法保证图片的载入,就会导致上报数据的丢失。 在使用的图片上通常都采用的是 1x1 的透明 gif 图,一来不会影响页面本身展示效果,二者表示图片透明只要使用一个二进制位标记图片是透明色即可,不用存储色彩空间数据,可以节约体积。因为需要透明色,所以可以直接排除 JEPG。​

这里多做一层解释:为什么要用gif呢?

  1. 在同样大小,不同格式的的图片中GIF大小是最小的,所以选择返回一张GIF,这样对性能的损耗更小;
  2. 如果返回204,会走到img的onerror事件,并抛出一个全局错误;如果返回200和一个空对象会有一个CORB的告警(当然如果不在意这个报错可以采取返回空对象,事实上也有一些工具是这样做的);
  3. 有一些埋点需要真实的加到页面上,比如垃圾邮件的发送者会添加这样一个隐藏标志来验证邮件是否被打开,如果返回204或者是200空对象会导致一个明显图片占位符
 public imageReport(url: string, data: any) {
    try {
      var img = new Image();
      img.src = url + "?error=" + this.formatParams(data);
    } catch (error) {
      console.log(error);
    }
  }
1
2
3
4
5
6
7
8

# sendBeacon

使用 sendBeacon 方法可以保证数据有效送达,且不会阻塞页面的卸载或加载,并且编码比起上述方法更加简单。

url 就是上报地址,data 可以是 ArrayBufferView,Blob,DOMString 或 Formdata,根据官方规范,需要 request header 为 CORS-safelisted-request-header,在这里则需要保证 Content-Type 为以下三种之一:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain
navigator.sendBeacon(url, data);

// blob
const blob = new Blob([
  JSON.stringify(data),
  {
    type: "application/x-www-form-urlencoded",
  },
]);
navigator.sendBeacon(url, blob);

// formData
const formData = new FormData();
Object.keys(data).forEach((key) => {
  let value = data[key];
  if (typeof value !== "string") {
    // formData只能append string 或 Blob
    value = JSON.stringify(value);
  }
  formData.append(key, value);
});
navigator.sendBeacon(url, formData);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 错误日志缓存

有时候,除了异常报错信息本身,我们可能还需要记录用户操作日志,以实现场景复原。无疑什么事都立即的上报会对服务器造成巨大的压力,相当于自造的 DDOS 攻击。因此还需要根据报错的类型规划不同的级别、选择不同的上报方案。首先,我们不能使用变量进行异常数据的存储,否则有可能会因为数据量过大而挤爆内存,并且刷新页面的时候这些数据就消失了。(当然市面上也有的做法是给定一个异常数据数组存储的阈值,达到一定的阈值之后统一上报,监听页面关闭时间,在关闭之前查看数组内是否有异常并上报)如果是采用持久化的思路的话,就可以采用到 Cookie、localStorage、sessionStorage、IndexedDB、webSQL 、FileSystem 等浏览器存储的形式了。

可以根据对这些缓存方式的了解得知,IndexedDB 的存储量多达 500M,具有异步的特性,不会对页面渲染造成阻塞。而且 IndexedDB 可以分库,分 store 并按照索引进行查询,具有完整的数据库管理思维,更适合做结构化数据管理。

这时候我们可以根据异常的紧急程度做上报:

  • 最紧急的异常:当发生异常后立即向后台推送异常,并确保异常上报成功,若不成功需要有一个循环机制。
  • 一般的异常:通常在 indexedDb 内存储 50-100 条数量大小的异常。每隔一定时间段定时将存储内的异常统一上报到服务器。
  • 不关键的异常或是数据则是可以随便找个时间上报出去

tips: 当我们一次性上传多条异常的时候数据量会很大,所以还要注意压缩上报数据,例如使用 lz-string 库来进行压缩

# 相关问题和文章

  • 如何实现 Web 页面录屏? (opens new window)

  • 搭建性能监控时,如遇到截图,录屏等需要,可能需要将一长串的 dom 字串通过 url 或者 data 的形式去传入到后台,这个时候我们就需要对字符串做压缩了。可以使用rrweb (opens new window)库来将 dom 转换为具有唯一标识符的可序列化数据结构

  • 前端的异常当然也包括了网页的卡顿和崩溃,网页都崩溃了,更别提上报了。我们需要用 window 的 load 和 beforeunload 事件来实现网页崩溃的监控。也可使用 Service Worker 来实现网页崩溃的监控:

    • Service Worker 有自己独立的工作线程,与网页区分开,网页崩溃了,Service Worker 一般情况下不会崩溃
    • Service Worker 生命周期一般要比网页还要长,可以用来监控网页的状态
    • 网页可以通过 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 发送消息
  • 不想让人发现自己的捕获代码时,可以用 JShaman 之类平台进行代码混淆加密,使代码成为不可识别的乱码。

编辑 (opens new window)
#JavaScript
上次更新: 2023/03/27, 10:50:38
前端模块化及进程
工作用得到的一些JS方法

← 前端模块化及进程 工作用得到的一些JS方法→

最近更新
01
gsap动画库学习笔记 - 持续~
06-05
02
远程组件加载方案笔记
05-03
03
小程序使用笔记
03-29
更多文章>
Theme by Vdoing | Copyright © 2020-2023 CD | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式