css 加载层 搜集错误所有起因来源于错误,那我们如何进行错误捕获
搜集错误
所有起因来源于错误,那我们如何进行错误捕获。
try/catch
能捕获常规运行时错误,语法错误和异步错误不行
<pre data-tool="mdnice编辑器" style="margin-top: 10px;margin-bottom: 10px;border-radius: 5px;box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">// 常规运行时错误,可以捕获 ✅ try { console.log(notdefined); } catch(e) { console.log('捕获到异常:', e); } // 语法错误,不能捕获 ❌ try { const notdefined, } catch(e) { console.log('捕获到异常:', e); } // 异步错误,不能捕获 ❌ try { setTimeout(() => { console.log(notdefined); }, 0) } catch(e) { console.log('捕获到异常:',e); } 复制代码
</pre>
try/catch有它细致处理的优势,但缺点也比较明显。
window.onerror
pure js错误收集,window.onerror,当 JS 运行时错误发生时,window 会触发一个 ErrorEvent 接口的 error 事件。
<pre data-tool="mdnice编辑器" style="margin-top: 10px;margin-bottom: 10px;border-radius: 5px;box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;"> window.onerror = function(message, source, lineno, colno, error) { console.log('捕获到异常:', {message, source, lineno, colno, error}); } 复制代码
</pre>
先验证下几个错误是否可以捕获。
<pre data-tool="mdnice编辑器" style="margin-top: 10px;margin-bottom: 10px;border-radius: 5px;box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">// 常规运行时错误,可以捕获 ✅ window.onerror = function(message, source, lineno, colno, error) { console.log('捕获到异常:',{message, source, lineno, colno, error}); } console.log(notdefined); // 语法错误,不能捕获 ❌ window.onerror = function(message, source, lineno, colno, error) { console.log('捕获到异常:',{message, source, lineno, colno, error}); } const notdefined, // 异步错误,可以捕获 ✅ window.onerror = function(message, source, lineno, colno, error) { console.log('捕获到异常:',{message, source, lineno, colno, error}); } setTimeout(() => { console.log(notdefined); }, 0) // 资源错误,不能捕获 ❌ window.onerror = function(message, source, lineno, colno, error) { console.log('捕获到异常:',{message, source, lineno, colno, error}); return true; } age/kkk.png"> 复制代码
</pre>
window.onerror 不能捕获资源错误怎么办?
window.addEventListener
当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,这些 error 事件不会向上冒泡到 window,但能被捕获。而window.onerror不能监测捕获。
<pre data-tool="mdnice编辑器" style="margin-top: 10px;margin-bottom: 10px;border-radius: 5px;box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">// 图片、script、css加载错误,都能被捕获 ✅ window.addEventListener('error', (error) => { console.log('捕获到异常:', error); }, true) // new Image错误,不能捕获 ❌ window.addEventListener('error', (error) => { console.log('捕获到异常:', error); }, true) new Image().src = 'https://yun.tuia.cn/image/lll.png' // fetch错误,不能捕获 ❌ window.addEventListener('error', (error) => { console.log('捕获到异常:', error); }, true) fetch('https://tuia.cn/test') 复制代码
</pre>
new Image运用的比较少,可以单独自己处理自己的错误。
但通用的fetch怎么办呢,fetch返回Promise,但Promise的错误不能被捕获,怎么办呢?
Promise错误
普通Promise错误
try/catch不能捕获Promise中的错误
<pre data-tool="mdnice编辑器" style="margin-top: 10px;margin-bottom: 10px;border-radius: 5px;box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">// try/catch 不能处理 JSON.parse 的错误,因为它在 Promise 中 try { new Promise((resolve,reject) => { JSON.parse('') resolve(); }) } catch(err) { console.error('in try catch', err) } // 需要使用catch方法 new Promise((resolve,reject) => { JSON.parse('') resolve(); }).catch(err => { console.log('in catch fn', err) }) 复制代码
</pre>
async错误
try/catch不能捕获async包裹的错误
<pre data-tool="mdnice编辑器" style="margin-top: 10px;margin-bottom: 10px;border-radius: 5px;box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">const getJSON = async () => { throw new Error('inner error') } // 通过try/catch处理 const makeRequest = async () => { try { // 捕获不到 JSON.parse(getJSON()); } catch (err) { console.log('outer', err); } }; try { // try/catch不到 makeRequest() } catch(err) { console.error('in try catch', err) } try { // 需要await,才能捕获到 await makeRequest() } catch(err) { console.error('in try catch', err) } 复制代码
</pre>
import chunk错误
import其实返回的也是一个promise,因此使用如下两种方式捕获错误
<pre data-tool="mdnice编辑器" style="margin-top: 10px;margin-bottom: 10px;border-radius: 5px;box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">// Promise catch方法 import('./index').then(module => { module.default() }).catch((err) => { console.error('in catch fn', err) }) // await 方法,try catch try { const module = await import('./index'); module.default() } catch(err) { console.error('in try catch', err) } 复制代码
</pre>
小结:全局捕获Promise中的错误
以上三种其实归结为Promise类型错误,可以通过unhandledrejection捕获
<pre data-tool="mdnice编辑器" style="margin-top: 10px;margin-bottom: 10px;border-radius: 5px;box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">// 全局统一处理Promise window.addEventListener("unhandledrejection", function(e){ console.log('捕获到异常:', e); }); fetch('https://tuia.cn/test') 复制代码
</pre>
为了防止有漏掉的 Promise 异常,可通过unhandledrejection用来全局监听Uncaught Promise Error。
Vue错误
由于Vue会捕获所有Vue单文件组件或者Vue.extend继承的代码,所以在Vue里面出现的错误,并不会直接被window.onerror捕获,而是会抛给Vue.config.errorHandler。
<pre data-tool="mdnice编辑器" style="margin-top: 10px;margin-bottom: 10px;border-radius: 5px;box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;"> Vue.config.errorHandler = function (err) { setTimeout(() => { throw err }) } 复制代码
</pre>
React错误
react 通过componentDidCatch,声明一个错误边界的组件
<pre data-tool="mdnice编辑器" style="margin-top: 10px;margin-bottom: 10px;border-radius: 5px;box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // 更新 state 使下一次渲染能够显示降级后的 UI return { hasError: true }; } componentDidCatch(error, errorInfo) { // 你同样可以将错误日志上报给服务器 logErrorToMyService(error, errorInfo); } render() { if (this.state.hasError) { // 你可以自定义降级后的 UI 并渲染 return Something went wrong.; } return this.props.children; } } class App extends React.Component { render() { return ( ) } } 复制代码
</pre>
但error boundaries并不会捕捉以下错误:React事件处理,异步代码,error boundaries自己抛出的错误。
跨域问题
一般情况,如果出现 Script error 这样的错误,基本上可以确定是出现了跨域问题。
如果当前投放页面和云端JS所在不同域名,如果云端JS出现错误,window.onerror会出现Script Error。通过以下两种方法能给予解决。
<pre data-tool="mdnice编辑器" style="margin-top: 10px;margin-bottom: 10px;border-radius: 5px;box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;"> const script = document.createElement('script'); script.crossOrigin = 'anonymous'; script.src = 'http://yun.tuia.cn/test.js'; document.body.appendChild(script); 复制代码
</pre>
<pre data-tool="mdnice编辑器" style="margin-top: 10px;margin-bottom: 10px;border-radius: 5px;box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;"> Test page in http://test.com window.onerror = function (message, url, line, column, error) { console.log(message, url, line, column, error); } try { foo(); // 调用testerror.js中定义的foo方法 } catch (e) { throw e; } 复制代码
</pre>
会发现如果不加try catch,console.log就会打印script error。加上try catch就能捕获到。
我们捋一下场景,一般调用远端js,有下列三种常见情况。
调用方法场景
可以通过封装一个函数,能装饰原方法,使得其能被try/catch。
<pre data-tool="mdnice编辑器" style="margin-top: 10px;margin-bottom: 10px;border-radius: 5px;box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;"> Test page in http://test.com window.onerror = function (message, url, line, column, error) { console.log(message, url, line, column, error); } function wrapErrors(fn) { // don't wrap function more than once if (!fn.__wrapped__) { fn.__wrapped__ = function () { try { return fn.apply(this, arguments); } catch (e) { throw e; // re-throw the error } }; } return fn.__wrapped__; } wrapErrors(foo)() 复制代码
</pre>
大家可以尝试去掉wrapErrors感受下。
事件场景
可以劫持原生方法。
<pre data-tool="mdnice编辑器" style="margin-top: 10px;margin-bottom: 10px;border-radius: 5px;box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;"> Test page in http://test.com 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); } http://test.com window.onerror = function (message, url, line, column, error) { console.log(message, url, line, column, error); } 复制代码
</pre>
大家可以尝试去掉封装EventTarget.prototype.addEventListener的那段代码,感受下。
上报接口
为什么不能直接用GET/POST/HEAD请求接口进行上报?
这个比较容易想到原因。一般而言,打点域名都不是当前域名,所以所有的接口请求都会构成跨域。
为什么不能用请求其他的文件资源(js/css/ttf)的方式进行上报?
创建资源节点后只有将对象注入到浏览器DOM树后,浏览器才会实际发送资源请求。而且载入js/css资源还会阻塞页面渲染,影响用户体验。
构造图片打点不仅不用插入DOM,只要在js中new出Image对象就能发起请求,而且还没有阻塞问题,在没有js的浏览器环境中也能通过img标签正常打点。
使用new Image进行接口上报。最后一个问题,同样都是图片,上报时选用了1x1的透明GIF,而不是其他的PNG/JEPG/BMP文件。
首先,1x1像素是最小的合法图片。而且,因为是通过图片打点,所以图片最好是透明的,这样一来不会影响页面本身展示效果,二者表示图片透明只要使用一个二进制位标记图片是透明色即可,不用存储色彩空间数据,可以节约体积。因为需要透明色,所以可以直接排除JEPG。
同样的响应,GIF可以比BMP节约41%的流量,比PNG节约35%的流量。GIF才是最佳选择。
使用1*1的gif[1]
非阻塞加载
尽量避免SDK的js资源加载影响。
通过先把window.onerror的错误记录进行缓存,然后异步进行SDK的加载,再在SDK里面处理错误上报。
<pre data-tool="mdnice编辑器" style="margin-top: 10px;margin-bottom: 10px;border-radius: 5px;box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;"> (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 = './build/skyeye.js', // 取决于你存放的位置 sc.crossOrigin = "anonymous", sc.onerror = function() { times--, times > 0 && setTimeout(appendScript, 1500) }, document.head && document.head.appendChild(sc); }; setTimeout(appendScript, 1500); })(window); 这是一个测试页面(new) 复制代码
</pre>
采集聚合端(日志服务器)
这个环节,输入是借口接收到的错误记录,输出是有效的数据入库。核心功能需要对数据进行清洗css 加载层,顺带解决了过多的服务压力。另一个核心功能是对数据进行入库。
总体流程可以看为错误标识 -> 错误过滤 -> 错误接收 -> 错误存储。
错误标识(SDK配合)
聚合之前,我们需要有不同维度标识错误的能力,可以理解为定位单个错误条目,单个错误事件的能力。
单个错误条目
通过date和随机值生成一条对应的错误条目id。
<p><pre data-tool="mdnice编辑器" style="margin-top: 10px;margin-bottom: 10px;border-radius: 5px;box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">const errorKey =
${+new Date()}@${randomString(8)}
function randomString(len) { len = len || 32; let chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'; let maxPos = chars.length; let pwd = ''; for (let i = 0; i { setTimeout(() => { maxLimit = MAX_LIMIT; task(); }, TICK); }; task(); const check = () => { if (maxLimit
发表评论
热门文章
Spimes主题专为博客、自媒体、资讯类的网站设计....
一款个人简历主题,可以简单搭建一下,具体也比较简单....
仿制主题,Typecho博客主题,昼夜双版设计,可....
用于作品展示、资源下载,行业垂直性网站、个人博客,....
热评文章
最新评论
Z.
11月29日
博主你好,Deng插件,这个点击不进去,提示这个(Warning: require_once(/www/wwwroot/w.zzy2020.com/usr/plugins/Deng/Deng/html/profile.php): failed to open stream: No such file or directory in /www/wwwroot/w.zzy2020.com/Fresh/extending.php on line 26
Fatal error: require_once(): Failed opening required '/www/wwwroot/w.zzy2020.com/usr/plugins/Deng/Deng/html/profile.php' (include_path='.:/www/server/php/72/lib/php') in /www/wwwroot/w.zzy2020.com/Fresh/extending.php on line 26)
点都德
3天前
:喷::喜欢::怒::黑线: