Эта статья была опубликована Сяо Ба Ле
Если учащиеся внешнего интерфейса использовали для мониторинга событие window.onerror, они должны знать, что междоменный сценарий выдаст сообщение «Ошибка сценария», и они не смогут получить конкретную информацию об ошибке и информацию о стеке.
Здесь читатели могут провести со мной эксперимент, чтобы глубже понять этот вопрос. Сначала подготовьтесь к эксперименту:
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');
});
Воспроизвести ошибку сценария
В это время мы запускаем Node APP:node app.js
, затем посетитеhttp://127.0.0.1:3000
.
Нажав кнопки 3000 и 4000 соответственно, мы обнаружили, что после нажатия кнопки 3000 в том же домене сообщение об исключении может быть перехвачено. Кросс-доменная кнопка 4000 имеет только одну ошибку сценария.
Нажмите кнопку 3000 Нажмите кнопку 4000Мы воспроизвели "Ошибка скрипта".!
Некоторые студенты подняли руки, я знаю, просто добавьте междоменный заголовок!
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 будетнапрямую невозможно выполнить - У нас не всегда есть права на настройку статических серверов, и при желании можно добавить междоменные заголовки.
Альтернативное мышление
Если я скажу вам, что вы можете просто загрузить «особый» 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 EventTarget;
- Оберните входящий прослушиватель, верните обернутый прослушиватель и попробуйте перехватить его выполнение;
- Браузер не будет перехватывать исключение, вызванное try-catch, поэтому, когда происходит перехват, информация о стеке сохраняется;
- Когда повторный бросок является ненормальным, выполняется тот же код домена, поэтому информация стека не будет потеряна при захвате Window.onError;
На самом деле, оборачивая 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, чтобы получить некоторую информацию, которую мы не смогли получить.
Эта статья была разрешена автором для публикации в сообществе Tencent Cloud + Для получения дополнительных оригинальных текстов, пожалуйстанажмите
Найдите и подпишитесь на общедоступную учетную запись «Сообщество Yunjia», получите технические галантереи как можно скорее и ответьте на 1024 после подписки, чтобы отправить вам подарочный пакет технических курсов!