本文由小芭乐发表javascript
前端的同窗若是用 window.onerror 事件作过监控,应该知道,跨域的脚本会给出 "Script Error." 提示,拿不到具体的错误信息和堆栈信息。html
这里读者能够跟我一块儿作一个实验,来深刻了解这个事情。先作一下实验准备:前端
建立一个 Node APP,只作静态服务器,提供两个端口用于作跨域实验。java
const express = require('express');
const app = express();
app.use(express.static('./public'));
app.listen(3000);
app.listen(4000);
复制代码
建立一个静态页面,监听 window.onerror
事件,而且输出事件的堆栈。同时分别加载两个域的 JS 文件。node
<!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>
复制代码
建立一个在 3000 端口执行的脚本,监听 3000 按钮的点击事件,而且抛出一个异常:express
const btn3k = document.querySelector('#btn-3000');
btn3k.addEventListener('click', () => {
throw new Error('Fail 3000');
});
复制代码
一样的,建立一个在 4000 端口执行的脚本:跨域
const btn4k = document.querySelector('#btn-4000');
btn4k.addEventListener('click', () => {
throw new Error('Fail 4000');
});
复制代码
这个时候,咱们启动 Node APP:node app.js
,而后访问 http://127.0.0.1:3000
。浏览器
分别点击按钮 3000 和 4000,咱们发现,同域下面的 3000 按钮点击后,异常消息能够捕获到。而跨域的 4000 按钮,只有一个 Script Error。服务器
咱们复现了 "Script Error."!app
有同窗举手,我知道,只要加一个跨域头就能够了!
没错,咱们能够给静态文件服务器加上跨域协议头:
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 按钮,咱们点击都能得到异常信息。
可是,这个方案有两个致命的弱点:
crossorigin="anonymous"
可是响应头没有正确,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 按钮事件处理器的执行异常信息。
若是你以为神奇,请点赞后,继续往下阅读。这个魔法 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);
}
复制代码
原理也非笔者原创,而是从这篇文章学习而来。
简单解释一下:
实际上,利用包装 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 送你一份技术课程大礼包!