捕获代码错误的正确姿式(一)

image.png

前言

不知道小伙伴们是否有这样的体验,本地开发项目调试时一切正常,一旦发布到线上就会出现各类奇怪的问题,缘由也是多种多样,环境变量不一样,宿主环境不一样,接口返回的数据格式不一样,代码逻辑问题等等。css

有些错误在开发和测试环节能被发现,有些错误要到了线上才会被发现,此时傻乎乎的等着用户反馈未免太过被动,因此,一个收集可以捕获代码错误的监控系统就显得尤其重要。html

该系列文章目的并非让你们可以立马打造一个前端代码监控系统,是旨在介绍浏览器环境下捕获代码错误的经常使用方式,并经过咱们所熟知的方式,打造一个前端收集代码错误的插件,最后再简单介绍一下收集到了报错信息,如何快速定位错误。前端

常见错误类型

在开始咱们的课题以前,咱们首先须要了解一下常见的错误类型。vue

代码书写错误

此类错误是最容易排除的,咱们每每只须要借助编辑器的自动检测,或者 eslint tslint 等三方工具,在开发阶段就能解决。node

固然,若是有同窗是用文本编辑器书写代码的,请在收下我膝盖的同时,下载一个 vscode 体验更好的代码编写环境。react

function foo()
cont bar = "string"

代码执行错误

这个能够说是平时开发中遇到最多的了,好比下面这个例子,相信有很多同窗在使用 vue 或者 react 时,经过接口获取列表数据,若是后端同窗对于空数据没有返回 [],而是返回了 null 或者 undefined,前端展现就会出现问题。jquery

也有的同窗会说,ts 大法好,接口定义香香的(但其实这并不能解决接口返回不一致的问题),并且,若遇到同事类型定义皆 any 的状况。。。git

undefined.map((item) => console.log(item));

其余的错误类型,好比使用了一个未定义的变量,但其实这种错误均可以在开发阶段规避掉。github

console.log(helloWorld);

异步代码 reject

不知道小伙伴们在使用 promise 的时候有没有 catch reject 的习惯,我相信除了我大部分人都是有的(违心),那对于未主动 catchreject 咱们应该如何捕获并处理?ajax

const promise = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error("promise reject"));
    }, 1000);
  });
// 未使用 catch 捕获错误
promise().then(console.log);

资源加载错误

资源加载错误也是一个使人头秃的问题,若是只是 image 那还好,如果重要的 jscss 资源加载错误。。。

<!-- 你说这个资源能加载?嗯?你有问题,小老弟 -->
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.2/jquery.min.js"></script>

捕获代码错误

接下来就咱们说的几种报错类型来作捕获,有的同窗可能会说,哎呀我平时用的是 vue react,这个这么简单的场景我不可能会犯错的啦。

嘘,憋说话,框架们抛出错误的方式也是用那些简单的方法。

另外,下面说内容都有详细的代码和注释,小伙伴们能够直接 clone 代码在本地进行调试,具体说明能够看项目分支的 README.md

示例地址

git clone -b demo/catch-code-error https://github.com/jsjzh/tiny-codes.git

代码执行错误

在平时写代码的时候,若是遇到可能会执行错误的逻辑,咱们通常会用 try catch,包裹,而后单独作处理,可是对于一些意料以外的错误,好比上面的接口返回;还有异步执行的代码中若发生错误,经过 try catch 是没法捕获到错误的,这个时候,window.onerror 就派上用场了。

咱们能够这么理解,try catch 用来捕获能够预料到的错误,window.onerror 用来捕获预料以外的错,也就是兜底策略。

/**
 * message {String}: 错误信息
 * fileName {String}: 发生错误的文件
 * col {Number}: 错误代码的行
 * row {Number}: 错误代码的列
 * error {Error}: Error 对象
 */
window.onerror = (message, fileName, col, row, error) => {
  console.log("message: ", message);
  console.log("fileName: ", fileName);
  console.log("col: ", col);
  console.log("row: ", row);
  console.log("error.name: ", error.name);
  console.log("error.message: ", error.message);
  console.log("error: ", error);
};

主动 try catch

下面的代码咱们主动 try catch 了错误,说明咱们知道 try 的逻辑里面可能会出错,在 catch 里面作了处理,其实这种错误状况有多是不须要上报的,由于咱们已经作了对应的错误处理。

ps: 若认为仍旧须要上报错误,能够在 catch 中增长一段 throw error 的逻辑,这样就能够将错误抛出,被 window.onerror 捕获。
// 同步执行代码,经过 try catch 能够捕获错误
try {
  console.log(helloWorld);
} catch (error) {
  // 在这里作一些自定义处理
  // ...
  // 若作了自定义处理后,仍旧须要上报错误,增长 throw error
  // throw error;
}

意料以外的报错

如下这种状况,咱们没有预料到会报错,则报错会直接被 window.onerror 捕获。

ps: 这里在浏览器环境访问了 process 对象,这是 node 环境才有的全局变量。
// 未使用 try catch 捕获错误
// 说明咱们未预料到这段代码可能出错
console.log(process);

image.png

异步代码执行错误

异步的逻辑代码中如有代码执行错误,咱们经过 try catch 没法直接捕获到,这个时候有两种办法,第一种是给异步执行的函数包裹 try catch,这种咱们就不举例了,第二种方法是经过 window.onerror 来捕获错误。

const promise = () =>
  new Promise(() => {
    setTimeout(() => {
      console.log(helloWorld);
    }, 1000);
  });

try {
  promise().then();
} catch (error) {
  // 没法捕获到错误 error
  console.log("can't catch error");
}

image.png

能够看到上图,错误被 window.onerror 给捕获了。

异步代码 reject

在说异步代码 reject 前,咱们得有一个共识,promisecatch 没法捕获到代码执行错误,只有经过 reject() 的方式抛出的异常,才会被 promise.catch 捕获到,什么意思?看下面代码。

const promise = () =>
  new Promise(() => {
    setTimeout(() => {
      console.log(helloWorld);
    }, 1000);
  });

promise()
  .then()
  // 没法捕获到 helloWorld 的错误
  .catch(() => {
    console.log("can't catch error");
  });

image.png

promisecatch 能够捕获到什么错误呢?答案是经过 reject 抛出的错误,以下。

const promise2 = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      // 经过 reject 抛出错误
      reject(new Error("promise reject"));
    }, 1000);
  });

promise2()
  .then()
  .catch((error) => {
    // 可以捕获到错误
    console.log("error.name: ", error.name);
    console.log("error.message: ", error.message);
    console.log("error: ", error);
  });

image.png

上面的内容只是为了达成咱们之间的共识,接下来咱们来讲说如何捕获未 catchreject,经过 window.onunhandledrejection

ps: 有一点须要提醒的,对于抛出的自定义错误,尽可能使用 new Error(...),这样咱们不只能够获取到完整的错误信息,还能够获取到文件名,出错行以及出错列,也就是所谓的堆栈信息。

ps2: 若是是被 promise.catch 捕获的 reject,则不会被 window.onunhandledrejection 捕获,若仍想将此错误上报,能够经过 throw error 的方式,此时将被 window.onerror 捕获。

window.onunhandledrejection = (promiseRejectEvent) => {
  // Error 对象,由 reject(new Error()) 生成
  const reason = promiseRejectEvent.reason;
  if (reason) {
    console.log("reason.name: ", reason.name);
    console.log("reason.message: ", reason.message);
    console.log("reason.stack: ", reason.stack);
  }
};

const promise2 = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      // 经过 reject 抛出错误
      reject(new Error("promise reject"));
    }, 1000);
  });

// 未使用 catch,错误将被 window.onunhandledrejection 捕获
promise2().then();

image.png

资源加载错误

对于资源加载错误,不论是 jscssimage,咱们均可以经过 window.addEventListener("error") 来监听捕获错误。

可是对于使用 background-image: url(./error.png) 致使的资源加载错误,以及 new Image().src = "./error.png" 致使的资源加载错误,经过该方法没法捕获到。

ps: 如有小伙伴知道有什么办法能够将这些加载错误捕获到的,还劳烦告诉我,我自测成功后会更新到文章中。
window.addEventListener(
  "error",
  (sourceErrorEvent) => {
    const targetElement =
      sourceErrorEvent.target || sourceErrorEvent.srcElement;
    const url = targetElement.src || targetElement.href;

    console.log("sourceError: ", url);
  },
  true
);
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.0.2/jquery.min.js"></script>
<script src="./error.js"></script>
<link rel="stylesheet" href="./error.css" />
<img src="./error.png" />

image.png

意外的小问题

相信有了上面的三个方案,咱们已经可以捕获大部分报错了,可是,在调试的时候发现,当加载的 js 资源是跨域的,window.onerror 就只会收集到错误信息 Script error.,以下图。

image.png

为啥会这样?固然就是浏览器的策略。。。不过咱们也有对应的解决办法。

ps: 关于某些 html 标签资源跨域内容能够参见 MDN 的 crossOrigin 属性说明。

回到问题,咱们只须要作两件事,第一:

给跨域加载的 js 资源增长一个 crossOrigin="anonymous" 属性

<script src="http://127.0.0.1:7002/index.js" crossorigin="anonymous"></script>

第二:

保证跨域资源的服务器设置了 Access-Control-Allow-Origin: hosts 头,而且你的浏览器发送 Origin: host 在其列表内,嫌麻烦的话,服务端能够直接设置 Access-Control-Allow-Origin: *

ps: 其实不太建议设置成 *,若是你使用了 cookie,这会致使 cookie 没法正常工做,还须要配套设置一些其余的头部,才能使 cookie 生效。
固然,若是资源都是统一作了 CDN 配置的,那能够为静态资源的服务器单独配置头部。

ps2: 你不须要主动添加 Origin 头部,这是浏览器的自发行为

# Response Header
...
Access-Control-Allow-Origin: http://127.0.0.1:7001
...

# Request Header
...
Origin: http://127.0.0.1:7001
...

image.png

如上,咱们就能够获取到错误的详细信息了。

再小小的强调一下,以上的例子你均可以在以下示例中找到。

示例地址

git clone -b demo/catch-code-error https://github.com/jsjzh/tiny-codes.git

后语

至此,捕获代码错误的正确姿式(一)就结束了,在书写文章的时候了解了许许多多平时不会关注的点,对于神奇的前端代码监控系统也有了本身的一些心得体会。

固然,有的同窗要说了,sentry 不香嘛?为啥要费时费力本身瞎折腾?关于这点我认为,若是咱们只是为了解决问题,那现成的方案拿来用天然是最好的。但不折腾的话和咸鱼有啥区别?(逃

文章后续还有两个章节,第二章我会打造一个专门收集前端错误信息的插件,第三章我会对于如今很常见的代码压缩后如何定位问题提供一些本身不太成熟的方案。

另,文笔拙劣,方案幼稚,看官们如有任何意见或建议,欢迎在评论区留言,咱们一块儿讨论讨论。

页脚

代码即人生,我甘之如饴。

技术不断在变
头脑一直在线
前端路漫漫
咱们下期见

by --- 裤裆三重奏

我在这里 gayhub@jsjzh 欢迎你们来找我玩儿。

欢迎小伙伴们直接加我,拉你进群一块儿搞事情,记得备注一下你是从哪里看到文章的。

image.png

ps: 若是图片失效,能够加我 wechat: kimimi_king
相关文章
相关标签/搜索