解決 "Script Error" 的另類思路

騰訊雲加社群發表於2018-11-30

本文由小芭樂發表

前端的同學如果用 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 送你一份技術課程大禮包!

相關文章