做为一个前端,在开发过程即使十分当心,自测充分,在不一样用户复杂的操做下也不免会出现程序员意想不到的问题,给公司或我的带来巨大的损失。 这时一款可以及时上报错误和可以帮助程序员很好的解决错误的前端错误监控系统就必不可少了。 接下来咱们就聊聊常见的错误发生与处理。javascript
本文主要围绕如下几点讨论:html
问题:前端
能够阅读监控类库源码 errorWatch 来加深理解,也能够直接用于项目。java
例如,英文字符写成中文字符。通常容易在开发时被发现。git
语法错误没法被try
catch
处理程序员
try { const error = 'error'; // 圆角分号 } catch(e) { console.log('我感知不到错误'); } 复制代码
JS引擎在执行脚本时,把任务分块压入事件栈,轮询取出执行,每一个事件任务都有本身的上下文环境, 在当前上下文环境同步执行的代码发生错误都能被try
catch
捕获,保证后续的同步代码被执行。github
try { error } catch(e) { console.log(e); } 复制代码
常见的 setTimeout
等方法会建立新的事件任务插入事件栈中,待后续执行。 因此try
catch
没法捕获其余上下文的代码错误。web
try { setTimeout(() => { error // 异步错误 }) } catch(e) { console.log('我感知不到错误'); } 复制代码
为了便于分析发生的错误,通常利用 window.onerror
事件来监听错误的发生。 它比try
catch
的捕获错误信息的能力要强大。ajax
/** * @param {String} msg 错误描述 * @param {String} url 报错文件 * @param {Number} row 行号 * @param {Number} col 列号 * @param {Object} error 错误Error对象 */ window.onerror = function (msg, url, row, col, error) { console.log('我知道错误了'); // return true; // 返回 true 的时候,异常不会向上抛出,控制台不会输出错误 }; 复制代码
window.onerror
能够捕获常见语法、同步、异步错误等错误;window.onerror
没法捕获 Promise
错误、网络错误;window.onerror
应该在全部JS脚本以前被执行,以避免遗漏;window.onerror
容易被覆盖,在处理回调时应该考虑,被人也在使用该事件监听。因为网络请求异常不会冒泡,应此须要在事件捕获阶段才能获取到。 咱们能够利用 window.addEventListener
。好比代码、图片等重要 CDN
资源挂了,能及时得到反馈是极为重要的。chrome
window.addEventListener('error', (error) => { console.log('404 错误'); console.log(error); // return true; // 中断事件传播 }, true); 复制代码
对于这类资源加载错误,在事件对象中能得到足够的信息,配合短信、钉钉等第一时间通知开发者。
window.addEventListener('error', (e) => { if (e.target !== window) { // 避免重复上报 console.log({ url: window.location.href, // 引用资源地址 srcUrl: e.target.src, // 资源加载出错地址 }) } }, true); 复制代码
window.onerror
与 window.addEventListener
window.addEventListener
的好处,不怕回调被覆盖,能够监听多个回调函数,但记得销毁避免内存泄漏与错误。 但没法获取 window.onerror
那么丰富的信息。通常只用window.addEventListener
来监控资源加载错误。
若是你在使用 promise
时未 catch
的话,那么 onerror
也无能为力了。
Promise.reject('promise error'); new Promise((resolve, reject) => { reject('promise error'); }); new Promise((resolve) => { resolve(); }).then(() => { throw 'promise error'; }); 复制代码
一样你能够利用 window.onunhandledrejection
或 window.addEventListener("unhandledrejection")
来监控错误。 接收一个PromiseError对象,能够解析错误对象中的 reason
属性,有点相似 stack
。
具体兼容处理在 TraceKit.js 能够看到。
img
上报ajax
上报function report(errInfo) { new Image().src = 'http://your-api-website?data=' + errInfo; } 复制代码
ajax
应使用的类库而已,大同小异。
img
请求有长度限制,数据太大最好仍是用 ajax.post
。引用不一样域名的脚本,若是没有特殊处理,报错误了,通常浏览器处于安全考虑,不显示具体错误而是 Script error
. 例如他人别有用心引用你的线上非开源业务代码,你的脚本报错信息固然不想让他知道了。
若是解决自有脚本的跨域报错问题?
CDN
的优点。HTTP response header
中设置 CORS
。Access-Control-Allow-Origin: You-allow-origin
;crossorigin
属性,例如 <script src="http://www.xxx.com/index.js" crossorigin></script>
响应头和crossorigin
取值问题
crossorigin="anonymous"
(默认),CORS
不等于 You-allow-origin
,不能带 cookie
crossorigin="use-credentials"
且 Access-Control-Allow-Credentials: true
,CORS
不能设置为 *
,能带 cookie
。 若是 CORS
不等于 You-allow-origin
,浏览器不加载 js。当你对自由能掌握的资源作好了 cors
时,Script error
基本能够过滤掉,不上报。
讲了这么多,还有一个很是重要的主题,如何分析我能捕获的错误信息?
一个 JavaScript
错误一般由一下错误组成
开发者能够经过不一样方式来抛出一个JavaScript 错误:
推荐使用第二种,第三四种浏览器没法就以上两种方式生成追溯栈。
若是能解析每行追溯栈中的错误信息,行列在配合 SourceMap
不就能定位到每行具体源代码了吗。 问题在于不一样浏览器在以上信息给出中,并无一个通用标准的格式。难点就在于解决兼容性问题。
例如 window.onerror
第五个参数 error 对象是2013年加入到 WHATWG
规范中的。 早期Safari 和 IE10尚未,Firefox是从14版本加入Error对象的,chrome 也是 2013 年才新加的。
window.onerror
是捕获JS 错误最好的方法,当有一个合法的Error对象和追溯栈时才上报。 也能够避免一些没法干扰的错误,例如插件错误和跨域等一些信息不全的错误。
try catch
加强,抛出的错误信息较全,能够弥补 window.onerror
的不足。但就像先前说过的, try catch
没法捕获异步错误和promise
错误,也不利用 V8
引擎性能优化。
例如腾讯的 BadJS,对如下推荐进行了try catch
包裹
具体是否须要作到如此细粒度的包裹,仍是视状况而定。
例若有如下错误追溯栈(stack trace)
ReferenceError: thisIsAbug is not defined
at Object.makeError (http://localhost:7001/public/js/traceKit.min.js:1:9435)
at http://localhost:7001/public/demo.html:28:12
复制代码
可以解析成一下格式
[ { "args" : [], "url" : "http://localhost:7001/public/js/traceKit.min.js", "func" : "Object.makeError", "line" : 1, "column" : 9435, "context" : null }, { "args" : [], "url" : "http://localhost:7001/public/demo.html", "func" : "?", "line" : 28, "column" : 12, "context" : null } ] 复制代码
在有了行列和对应的 SourceMap
文件就能解析获取源代码信息了。
解析结果
处理代码以下:
import { SourceMapConsumer } from 'source-map'; // 必须初始化 SourceMapConsumer.initialize({ 'lib/mappings.wasm': 'https://unpkg.com/source-map@0.7.3/lib/mappings.wasm', }); /** * 根据sourceMap文件解析源代码 * @param {String} rawSourceMap sourceMap文件 * @param {Number} line 压缩代码报错行 * @param {Number} column 压缩代码报错列 * @param {Number} offset 设置返回临近行数 * @returns {Promise<{context: string, originLine: number | null, source: string | null}>} * context:源码错误行和上下附近的 offset 行,originLine:源码报错行,source:源码文件名 */ export const sourceMapDeal = async (rawSourceMap, line, column, offset) => { // 经过sourceMap库转换为sourceMapConsumer对象 const consumer = await new SourceMapConsumer(rawSourceMap); // 传入要查找的行列数,查找到压缩前的源文件及行列数 const sm = consumer.originalPositionFor({ line, // 压缩后的行数 column, // 压缩后的列数 }); // 压缩前的全部源文件列表 const { sources } = consumer; // 根据查到的source,到源文件列表中查找索引位置 const smIndex = sources.indexOf(sm.source); // 到源码列表中查到源代码 const smContent = consumer.sourcesContent[smIndex]; // 将源代码串按"行结束标记"拆分为数组形式 const rawLines = smContent.split(/\r?\n/g); let begin = sm.line - offset; const end = sm.line + offset + 1; begin = begin < 0 ? 0 : begin; const context = rawLines.slice(begin, end).join('\n'); // 记得销毁 consumer.destroy(); return { context, originLine: sm.line + 1, // line 是从 0 开始数,因此 +1 source: sm.source, } }; 复制代码
你们根据 SourceMap
文件的格式,就能很好的理解这段代码了。