js错误是第一指标,任何一个js错误都有可能致使阻塞,影响咱们页面的正常运转。html
本篇主要对js错误收集的分享前端
注: 了解异常发生的状况及影响, 有助于咱们选择合适方式进行异常捕获处理vue
任何一个js异常的发生都会致使当前代码块不能正常执行react
那么那些状况会致使js异常阻塞呢?git
咱们都知道, js是单线程的、基于事件循环机制的的语言, 接下来咱们分别讨论异常发生的状况及影响github
同一个线程中运行的代码,异常阻塞api
var a = 0
console.log("--step1")
a = a+b
console.log("--step2")
复制代码
--step1
ReferenceError: b is not defined跨域
当咱们在代码中使用多块scriptpromise
<body>
进入part1
<script>
console.log("====step1")
var a= 1
a = a + b
console.log("====step2")
</script>
进入part2
<script>
console.log("====step3")
</script>
</body>
复制代码
====step1
ReferenceError: b is not defined
====step3浏览器
多个外联script, s1,s2代码同上
<script src="./js/s1.js"></script>
<script src="./js/s2.js"></script>
复制代码
结果同状况2
====step1
ReferenceError: b is not defined
====step3
var a = 0
console.log("--step1")
setTimeout(() => {
console.log("--step3")
a = a+b
console.log("--step4")
},1000)
console.log("--step2")
复制代码
结果以下
--step1
--step2
--step3
ReferenceError: b is not defined
对异步代码进行异常捕获
window.addEventListener("error", function() {
console.log("全局错误捕获",arguments)
})
try{
var a = 1;
setTimeout(function() {
a = a+b
}, 100)
}catch(e) {
console.log("===未在try..catch捕获")
}
复制代码
对可能出现异常的代码加异常捕获
var a = 0
console.log("--step1")
try{
a = a+b
}catch(e){
console.log(e.message)
}
console.log("--step2")
复制代码
--step1
b is not defined
--step2
包含promise的操做, 分别运行下述代码中一、二、3的分支
async function a () {
await Promise.reject("===step1")
}
//1
a()
console.log("===step2")
//2
async function b() => {
await a()
console.log("===step3")
}
b()
// 3
async function c() {
try{
await a()
}catch(e) {
console.log(e)
}
console.log("===step4")
}
c()
复制代码
结果
分支1:
===step2
UnhandledPromiseRejectionWarning: ===step1
分支2:
UnhandledPromiseRejectionWarning:===step1
分支3:
===step1
===step4
window.addEventListener("error", function() {
console.log("全局错误捕获",arguments)
})
window.addEventListener("unhandledrejection", event => {
console.warn("promise Error",event.reason.message, event.reason.stack)
});
function a() {
return new Promise(function() {
var a = 1
a = a +b
})
}
a()
复制代码
测试结果
promise Error "b is not defined" "ReferenceError: b is not defined
at http://localhost:63342/jserror/test/thead9.html:20:20
at new Promise ()
以上测试,能够得出如下结论
a. 同步代码块异常会阻塞后续代码
b. 不一样的script标签之间互不影响
c. 异步代码只会影响当前异步代码块的后续代码
d.promise若是返回reject,须要使用catch捕获处理
f. 若是使用async、await, 能够转换成try..catch捕获
注: 异常抛出的内容,是咱们定位问题的关键
按1中异常出现的状况,咱们知道,异常信息主要分两类
一类是抛出Error相关错误
一类是promise reject未处理时异常
非同域下错误是什么样子呢?
看范例
<script>
window.onerror=function(e) {
console.log("全局错误:", arguments)
}
</script>
<script src="http://192.168.31.200:8080/js/s1.js"></script>
复制代码
Chrome下获得的结果是
Script error.
这是浏览器同源策略致使的, 会隐藏不一样源资源的错误详情
想处理以上状况,有两种方案,
对引入的资源加crossorigin="anonymous"
<script>
window.onerror=function(e) {
console.log("全局错误:", arguments)
}
</script>
<script src="http://10.10.47.38:8080/js/s1.js" crossorigin="anonymous"></script>
复制代码
Chrome下获得结果
Uncaught ReferenceError: b is not defined
注: 该方法局限性, 一是浏览器兼容性, 二是请求的资源须要添加CORS相关响应头
使用try..catch包装须要捕获错误的方法或代码块
如何肯定,哪些方法是咱们须要单独封装的?
咱们已经知道,异常是出如今外部js中,有多是cdn的资源或者引用网络上其余的资源,他们有个特色就是跨域, 一旦这些文件发生错误, 咱们没法获取到具体的错误信息,
那么除了crossorigin,咱们还有那些方法能够取到跨域js的异常呢?
a. 在同域js中调用跨域js的函数, 手动包装try..catch
// s1.js
function m1 () {
console.log("====step1")
var a = 1
a = a+b
console.log("====step2")
}
// test.html
try{
m1()
}catch(e){
throw e
}
复制代码
m1为跨域js提供的一个函数 这时候抛出的error等同于同域下的错误信息
b. 跨域js中有一些异步的代码, 如setTimeout、eventListener等
对于这一类,咱们能够对原生的方法进行封装, 对参数包裹try...catch, 能够达到手动包装的效果
如 setTimeout, 咱们对函数入参进行封装便可
// test.html
window.onerror=function(e) {
console.log("全局错误:", arguments[0])
}
var originTo = window.setTimeout
function wrap (originTo) {
return function(fun, arg) {
var fun2 = function() {
try{
fun.call(this, arguments)
}catch(e){
throw e
}
}
originTo(fun2, arg)
}
}
window.setTimeout = wrap(originTo)
m1()
// s5.js
function m1 () {
setTimeout(function() {
console.log("====step1")
var a = 1
a = a+b
console.log("====step2")
},100)
}
复制代码
输出结果为:
全局错误: Uncaught ReferenceError: b is not defined
咱们使用自定义方法能够对经常使用对象进行包裹, 可是并不能作到所有拦截, 如你们经常使用的sentry, 若是出现不在特定方法内的跨域错误, 会直接被sentry吞掉
基于以上思路, 咱们提供一个通用的封装方法,可参考sentry或者badjs, sentry代码以下
context: function(options, func, args) {
if (isFunction(options)) {
args = func || [];
func = options;
options = undefined;
}
return this.wrap(options, func).apply(this, args);
},
/* * Wrap code within a context and returns back a new function to be executed * * @param {object} options A specific set of options for this context [optional] * @param {function} func The function to be wrapped in a new context * @param {function} func A function to call before the try/catch wrapper [optional, private] * @return {function} The newly wrapped functions with a context */
wrap: function(options, func, _before) {
var self = this;
// 1 argument has been passed, and it's not a function
// so just return it
if (isUndefined(func) && !isFunction(options)) {
return options;
}
// options is optional
if (isFunction(options)) {
func = options;
options = undefined;
}
// At this point, we've passed along 2 arguments, and the second one
// is not a function either, so we'll just return the second argument.
if (!isFunction(func)) {
return func;
}
// We don't wanna wrap it twice!
try {
if (func.__raven__) {
return func;
}
// If this has already been wrapped in the past, return that
if (func.__raven_wrapper__) {
return func.__raven_wrapper__;
}
} catch (e) {
// Just accessing custom props in some Selenium environments
// can cause a "Permission denied" exception (see raven-js#495).
// Bail on wrapping and return the function as-is (defers to window.onerror).
return func;
}
function wrapped() {
var args = [],
i = arguments.length,
deep = !options || (options && options.deep !== false);
if (_before && isFunction(_before)) {
_before.apply(this, arguments);
}
// Recursively wrap all of a function's arguments that are
// functions themselves.
while (i--) args[i] = deep ? self.wrap(options, arguments[i]) : arguments[i];
try {
// Attempt to invoke user-land function
// NOTE: If you are a Sentry user, and you are seeing this stack frame, it
// means Raven caught an error invoking your application code. This is
// expected behavior and NOT indicative of a bug with Raven.js.
return func.apply(this, args);
} catch (e) {
self._ignoreNextOnError();
self.captureException(e, options);
throw e;
}
}
// copy over properties of the old function
for (var property in func) {
if (hasKey(func, property)) {
wrapped[property] = func[property];
}
}
wrapped.prototype = func.prototype;
func.__raven_wrapper__ = wrapped;
// Signal that this function has been wrapped already
// for both debugging and to prevent it to being wrapped twice
wrapped.__raven__ = true;
wrapped.__inner__ = func;
return wrapped;
}
复制代码
咱们能够调用wrap方法对函数进行封装
如项目中使用了requirejs,那咱们能够经过直接对require和define对象封装,从而达到对跨域文件全内容封装的目的
if (typeof define === 'function' && define.amd) {
window.define = wrap({deep: false}, define);
window.require = wrap({deep: false}, require);
}
复制代码
注: 该方法的局限性在于,须要开发者发现项目中的一些关键入口并手动封装
以上咱们讨论了Error的类型、出现缘由及如何捕获异常,然而上图中标识的错误字段兼容性也是一大问题
好在前人栽树后人乘凉
,有一个库能够帮助咱们处理该问题 TraceKit O(∩_∩)O~~
结合2中内容,再加上万能捕获window.onerror, 便可对错误信息进行有效的获取
若是你使用了traceKit库, 那么你能够直接使用下面代码
tracekit.report.subscribe(function(ex, options) {
report.captureException(ex, options)
})
复制代码
若是没有,那咱们能够直接从新onerror方法便可
var oldErrorHandler = window.onerror
window.onerror = function(){
// 上报错误信息
if(oldErrorHander){
oldErrorHandler.apply(this, argument)
}
}
复制代码
2图中promise被单独列出分支,由于咱们须要使用特定事件处理
if(window.addEventListener) {
window.addEventListener("unhandledrejection", function(event) {
report.captureException(event.reason || {message: "unhandlePromiseError"}, {frame: "promise"})
});
}
复制代码
针对如今流行的框架
经过errorHandler钩子处理
function formatComponentName(vm) {
if (vm.$root === vm) {
return 'root instance';
}
var name = vm._isVue ? vm.$options.name || vm.$options._componentTag : vm.name;
return (
(name ? 'component <' + name + '>' : 'anonymous component') +
(vm._isVue && vm.$options.__file ? ' at ' + vm.$options.__file : '')
);
}
function vuePlugin(Vue) {
return {
doApply: function() {
Vue = Vue || window.Vue;
// quit if Vue isn't on the page
if (!Vue || !Vue.config) return;
var self = this;
var _oldOnError = Vue.config.errorHandler;
Vue.config.errorHandler = function VueErrorHandler(error, vm, info) {
var metaData = {
componentName: formatComponentName(vm),
propsData: vm.$options.propsData
};
// lifecycleHook is not always available
if (typeof info !== 'undefined') {
metaData.lifecycleHook = info;
}
self.captureException(error, {
frame: "vue",
extra: JSON.stringify(metaData)
});
if (typeof _oldOnError === 'function') {
_oldOnError.call(this, error, vm, info);
}
};
}
}
}
复制代码
react16版本以后,引入错误边界,有些非阻塞异常会经过该钩子抛出
咱们能够
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染可以显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你一样能够将错误日志上报给服务器
report.captureException(error, {
frame: "react",
extra: JSON.stringify(errorInfo)
});
}
render() {
if (this.state.hasError) {
// 你能够自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
复制代码
至此,咱们就完成了对Error的信息获取, 为咱们作错误报警及堆栈还原作基础
-=======================-
前端监控实践系列