Альтернативный способ решения «Ошибки скрипта»

Node.js внешний интерфейс сервер JavaScript

Эта статья была опубликована Сяо Ба Ле

Если учащиеся внешнего интерфейса использовали для мониторинга событие 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 имеет только одну ошибку сценария.

img
Нажмите кнопку 3000

img
Нажмите кнопку 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 будетнапрямую невозможно выполнить
  • У нас не всегда есть права на настройку статических серверов, и при желании можно добавить междоменные заголовки.

img
объявляет перекрестное происхождение, но не отвечает на 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);
}

Принцип не оригинален автором, а изэта статьяприходите учиться.

Кратко объясните:

  • Перепишите метод addEventListener EventTarget;
  • Оберните входящий прослушиватель, верните обернутый прослушиватель и попробуйте перехватить его выполнение;
  • Браузер не будет перехватывать исключение, вызванное try-catch, поэтому, когда происходит перехват, информация о стеке сохраняется;
  • Когда повторный бросок является ненормальным, выполняется тот же код домена, поэтому информация стека не будет потеряна при захвате 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, чтобы получить некоторую информацию, которую мы не смогли получить.

Эта статья была разрешена автором для публикации в сообществе Tencent Cloud + Для получения дополнительных оригинальных текстов, пожалуйстанажмите

Найдите и подпишитесь на общедоступную учетную запись «Сообщество Yunjia», получите технические галантереи как можно скорее и ответьте на 1024 после подписки, чтобы отправить вам подарочный пакет технических курсов!