本文由小芭樂發表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。服務器
點擊 3000 按鈕 點擊 4000 按鈕咱們復現了 "Script Error."!markdown
有同窗舉手,我知道,只要加一個跨域頭就能夠了!
沒錯,咱們能夠給靜態文件服務器加上跨域協議頭:
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 送你一份技術課程大禮包!