解决 "Script Error" 的另类思路

qcloudcommunity · · 1766 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

> 本文由小芭乐发表 前端的同学如果用 window.onerror 事件做过监控,应该知道,跨域的脚本会给出 "Script Error." 提示,拿不到具体的错误信息和堆栈信息。 这里读者可以跟我一起做一个实验,来深入了解这个事情。先做一下实验准备: ## app.js 创建一个 Node APP,只做静态服务器,提供两个端口用于做跨域实验。 ```js const express = require('express'); const app = express(); app.use(express.static('./public')); app.listen(3000); app.listen(4000); ``` ## public/index.html 创建一个静态页面,监听 `window.onerror` 事件,并且输出事件的堆栈。同时分别加载两个域的 JS 文件。 ```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 按钮的点击事件,并且抛出一个异常: ```js const btn3k = document.querySelector('#btn-3000'); btn3k.addEventListener('click', () => { throw new Error('Fail 3000'); }); ``` ## public/at4000.js 同样的,创建一个在 4000 端口执行的脚本: ```js 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。 ![img](https://ask.qcloudimg.com/draft/1000005/afevzrld2y.png?imageView2/2/w/1620)点击 3000 按钮 ![img](https://ask.qcloudimg.com/draft/1000005/0jd95zny52.png?imageView2/2/w/1620)点击 4000 按钮 我们复现了 "Script Error."! 有同学举手,我知道,只要加一个跨域头就可以了! ## Access-Control-Allow-Origin 没错,我们可以给静态文件服务器加上跨域协议头: ```js 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 的时候,加上跨域声明: ```js <script src="http://127.0.0.1:4000/at4000.js" crossorigin="anonymous"></script> ``` 这样,无论 3000 还是 4000 按钮,我们点击都能获得异常信息。 但是,这个方案有两个致命的弱点: - 如果 JS 声明了 `crossorigin="anonymous"` 但是响应头没有正确,JS 会**直接无法执行** - 我们并不总是有静态服务器的配置权限,跨域头不是想加就能加 ![img](https://ask.qcloudimg.com/draft/1000005/snekcjdq0b.png?imageView2/2/w/1620)声明了 crossorigin 但是没有响应跨域头的 JS ## 另类思路 如果我告诉你,可以不加跨域头,只是在 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 按钮事件处理器的执行异常信息。 ![img](https://ask.qcloudimg.com/draft/1000005/ogvubwyy9n.png?imageView2/2/w/1620)点击 3000 ![img](https://ask.qcloudimg.com/draft/1000005/1r72nmy7v6.png?imageView2/2/w/1620)点击 4000 如果你觉得神奇,请点赞后,继续往下阅读。这个魔法 JS,其实也很简单: ```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); } ``` 原理也非笔者原创,而是从[这篇文章](https://blog.sentry.io/2016/01/04/client-javascript-reporting-window-onerror)学习而来。 简单解释一下: - 改写了 EventTarget 的 addEventListener 方法; - 对传入的 listener 进行包装,返回包装过的 listener,对其执行进行 try-catch; - 浏览器不会对 try-catch 起来的异常进行跨域拦截,所以 catch 到的时候,是有堆栈信息的; - 重新 throw 出来异常的时候,执行的是同域代码,所以 window.onerror 捕获的时候不会丢失堆栈信息; 实际上,利用包装 addEventListener,我们还可以达到「扩展堆栈」的效果: ![img](https://ask.qcloudimg.com/draft/1000005/5f5ms4pqbp.png?imageView2/2/w/1620)堆栈扩展效果 我们不仅知道异常堆栈,而且还知道导致该异常的事件处理器,是在何处添加进去的。实现这个效果,也很简单: ```js (() => { 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 做这样的拦截,得到一些我们本来得不到的信息。 **此文已由作者授权腾讯云+社区发布,更多原文请[点击](https://cloud.tencent.com/developer/article/1367170?fromSource=waitui )** **搜索关注公众号「云加社区」,第一时间获取技术干货,关注后回复1024 送你一份技术课程大礼包!**

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1766 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传