解决 "Script Error" 的另类思

作者: a3aac2d1b674 | 来源:发表于2018-11-30 10:47 被阅读1次

    本文由小芭乐发表

    前端的同学如果用 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。

    img

    点击 3000 按钮

    img

    点击 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 会直接无法执行
    • 我们并不总是有静态服务器的配置权限,跨域头不是想加就能加
    img

    声明了 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 按钮事件处理器的执行异常信息。

    img

    点击 3000

    img

    点击 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,我们还可以达到「扩展堆栈」的效果:

    img

    堆栈扩展效果

    我们不仅知道异常堆栈,而且还知道导致该异常的事件处理器,是在何处添加进去的。实现这个效果,也很简单:

     (() => {
       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 送你一份技术课程大礼包!

    相关文章

      网友评论

        本文标题:解决 "Script Error" 的另类思

        本文链接:https://www.haomeiwen.com/subject/ztbccqtx.html