解决 "Script Error" 的另类思路
本文由小芭乐发表
前端的同学如果用 window.onerror 事件做过监控,应该知道,跨域的脚本会给出 "script error." 提示,拿不到具体的错误信息和堆栈信息。
这里读者可以跟我一起做一个实验,来深入了解这个事情。先做一下实验准备:
app.js
创建一个 node app,只做静态服务器,提供两个端口用于做跨域实验。
const express = require('express'); const app = express(); app.use(express.static('./public')); app.listen(3000); app.listen(4000);
public/index.html
创建一个静态页面,监听 window.onerror
事件,并且输出事件的堆栈。同时分别加载两个域的 js 文件。
<!doctype html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="x-ua-compatible" content="ie=edge"> <title>script error test</title> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body> <button id="btn-3000">3000</button> <button id="btn-4000">4000</button> <div> <pre id="info"></pre> </div> </body> <script> window.addeventlistener('error', evt => { const info = evt.error ? evt.error.stack : evt.message; document.queryselector('#info').textcontent = info; }); </script> <script src="http://127.0.0.1:3000/at3000.js"></script> <script src="http://127.0.0.1:4000/at4000.js"></script> </html>
public/at3000.js
创建一个在 3000 端口执行的脚本,监听 3000 按钮的点击事件,并且抛出一个异常:
const btn3k = document.queryselector('#btn-3000'); btn3k.addeventlistener('click', () => { throw new error('fail 3000'); });
public/at4000.js
同样的,创建一个在 4000 端口执行的脚本:
const btn4k = document.queryselector('#btn-4000'); btn4k.addeventlistener('click', () => { throw new error('fail 4000'); });
复现 script error
这个时候,我们启动 node app:node app.js
,然后访问 http://127.0.0.1:3000
。
分别点击按钮 3000 和 4000,我们发现,同域下面的 3000 按钮点击后,异常消息可以捕获到。而跨域的 4000 按钮,只有一个 script error。
点击 3000 按钮
点击 4000 按钮
我们复现了 "script error."!
有同学举手,我知道,只要加一个跨域头就可以了!
access-control-allow-origin
没错,我们可以给静态文件服务器加上跨域协议头:
app.use(express.static('./public', { setheaders(res) { res.set('access-control-allow-origin', res.req.get('origin')); res.set('access-control-allow-credentials', 'true'); } }));
同时,加载 js 的时候,加上跨域声明:
<script src="http://127.0.0.1:4000/at4000.js" crossorigin="anonymous"></script>
这样,无论 3000 还是 4000 按钮,我们点击都能获得异常信息。
但是,这个方案有两个致命的弱点:
- 如果 js 声明了
crossorigin="anonymous"
但是响应头没有正确,js 会直接无法执行 - 我们并不总是有静态服务器的配置权限,跨域头不是想加就能加
声明了 crossorigin 但是没有响应跨域头的 js
另类思路
如果我告诉你,可以不加跨域头,只是在 js 文件加载之前加载一个「特别的」js,一样可以达到目的,你信不信?
<script src="http://127.0.0.1:3000/inject-event-target.js"></script> <script src="http://127.0.0.1:3000/at3000.js"></script> <script src="http://127.0.0.1:4000/at4000.js"></script>
这个神奇的 inject-event-target.js
可以让我们在没有跨域头的情况下,拿到 4000 按钮事件处理器的执行异常信息。
点击 3000
点击 4000
如果你觉得神奇,请点赞后,继续往下阅读。这个魔法 js,其实也很简单:
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); }
原理也非笔者原创,而是从学习而来。
简单解释一下:
- 改写了 eventtarget 的 addeventlistener 方法;
- 对传入的 listener 进行包装,返回包装过的 listener,对其执行进行 try-catch;
- 浏览器不会对 try-catch 起来的异常进行跨域拦截,所以 catch 到的时候,是有堆栈信息的;
- 重新 throw 出来异常的时候,执行的是同域代码,所以 window.onerror 捕获的时候不会丢失堆栈信息;
实际上,利用包装 addeventlistener,我们还可以达到「扩展堆栈」的效果:
堆栈扩展效果
我们不仅知道异常堆栈,而且还知道导致该异常的事件处理器,是在何处添加进去的。实现这个效果,也很简单:
(() => { const originaddeventlistener = eventtarget.prototype.addeventlistener; eventtarget.prototype.addeventlistener = function (type, listener, options) { + // 捕获添加事件时的堆栈 + const addstack = new error(`event (${type})`).stack; const wrappedlistener = function (...args) { try { return listener.apply(this, args); } catch (err) { + // 异常发生时,扩展堆栈 + err.stack += '\n' + addstack; throw err; } } return originaddeventlistener.call(this, type, wrappedlistener, options); } })();
同样的道理,我们也可以对 settimeout、setinterval、requestanimationframe 甚至 xmlhttprequest 做这样的拦截,得到一些我们本来得不到的信息。
此文已由作者授权腾讯云+社区发布,更多原文请点击
搜索关注公众号「云加社区」,第一时间获取技术干货,关注后回复1024 送你一份技术课程大礼包!
上一篇: IT部门信息化正确打开方式
推荐阅读
-
winxp 安装MYSQL 出现Error 1045 access denied 的解决方法
-
Outlook提示503 Error: need EHLO and AUTH first的解决办法
-
丢失Android系统库或者Conversion to Dalvik format failed with error 1错误的解决方法
-
MYSQL ERROR 1045 (28000): Access denied for user (using password: YES)问题的解决
-
MySQL无法读表错误的解决方法(MySQL 1018 error)
-
Codeigniter出现错误提示Error with CACHE directory的解决方案
-
新装MySql后登录出现root帐号提示mysql ERROR 1045 (28000): Access denied for use的解决办法
-
sqlserver数据库出现置疑的解决思路
-
Outlook提示503 Error: need EHLO and AUTH first的解决办法
-
编译PHP报错configure error Cannot find libmysqlclient under usr的解决方法