解决 Script Error 的另类思路
2019獨角獸企業重金招聘Python工程師標準>>>
本文由小芭樂發表
前端的同學如果用 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。
點擊 3000 按鈕
點擊 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 會直接無法執行
- 我們并不總是有靜態服務器的配置權限,跨域頭不是想加就能加
聲明了 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 按鈕事件處理器的執行異常信息。
點擊 3000
點擊 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,我們還可以達到「擴展堆?!沟男Ч?#xff1a;
堆棧擴展效果
我們不僅知道異常堆棧,而且還知道導致該異常的事件處理器,是在何處添加進去的。實現這個效果,也很簡單:
(() => {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 送你一份技術課程大禮包!
轉載于:https://my.oschina.net/qcloudcommunity/blog/2963894
總結
以上是生活随笔為你收集整理的解决 Script Error 的另类思路的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Notepad++的安装和基本使用
- 下一篇: 第一段Java程序_借助Win控制命令台