Браузер позволяет отслеживать загрузку сторонних ресурсов: скриптов, ифреймов, изображений и др.
Для этого существуют два события:
load
– успешная загрузка,error
– во время загрузки произошла ошибка.
Загрузка скриптов
Допустим, нам нужно загрузить сторонний скрипт и вызвать функцию, которая объявлена в этом скрипте.
Мы можем загрузить этот скрипт динамически:
let script = document.createElement('script');
script.src = "my.js";
document.head.append(script);
…Но как нам вызвать функцию, которая объявлена внутри того скрипта? Нам нужно подождать, пока скрипт загрузится, и только потом мы можем её вызвать.
Для наших собственных скриптов мы можем использовать JavaScript-модули, но они не слишком широко распространены в сторонних библиотеках.
script.onload
Главный помощник – это событие load
. Оно срабатывает после того, как скрипт был загружен и выполнен.
Например:
let script = document.createElement('script');
// мы можем загрузить любой скрипт с любого домена
script.src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"
document.head.append(script);
script.onload = function() {
// в скрипте создаётся вспомогательная переменная с именем "_"
alert(_.VERSION); // отображает версию библиотеки
};
Таким образом, в обработчике onload
мы можем использовать переменные, вызывать функции и т.д., которые предоставляет нам сторонний скрипт.
…А что если во время загрузки произошла ошибка? Например, такого скрипта нет (ошибка 404), или сервер был недоступен.
script.onerror
Ошибки, которые возникают во время загрузки скрипта, могут быть отслежены с помощью события error
.
Например, давайте запросим скрипт, которого не существует:
let script = document.createElement('script');
script.src = "https://example.com/404.js"; // такого файла не существует
document.head.append(script);
script.onerror = function() {
alert("Ошибка загрузки " + this.src); // Ошибка загрузки https://example.com/404.js
};
Обратите внимание, что мы не можем получить описание HTTP-ошибки. Мы не знаем, была ли это ошибка 404 или 500, или какая-то другая. Знаем только, что во время загрузки произошла ошибка.
Обработчики onload
/onerror
отслеживают только сам процесс загрузки.
Ошибки обработки и выполнения загруженного скрипта ими не отслеживаются. Чтобы «поймать» ошибки в скрипте, нужно воспользоваться глобальным обработчиком window.onerror
.
Другие ресурсы
События load
и error
также срабатывают и для других ресурсов, а вообще, для любых ресурсов, у которых есть внешний src
.
Например:
let img = document.createElement('img');
img.src = "https://js.cx/clipart/train.gif"; // (*)
img.onload = function() {
alert(`Изображение загружено, размеры ${img.width}x${img.height}`);
};
img.onerror = function() {
alert("Ошибка во время загрузки изображения");
};
Однако есть некоторые особенности:
- Большинство ресурсов начинают загружаться после их добавления в документ. За исключением тега
<img>
. Изображения начинают загружаться, когда получаютsrc (*)
. - Для
<iframe>
событиеload
срабатывает по окончании загрузки как в случае успеха, так и в случае ошибки.
Такое поведение сложилось по историческим причинам.
Ошибка в скрипте с другого источника
Есть правило: скрипты с одного сайта не могут получить доступ к содержимому другого сайта. Например, скрипт с https://facebook.com
не может прочитать почту пользователя на https://gmail.com
.
Или, если быть более точным, один источник (домен/порт/протокол) не может получить доступ к содержимому с другого источника. Даже поддомен или просто другой порт будут считаться разными источниками, не имеющими доступа друг к другу.
Это правило также касается ресурсов с других доменов.
Если мы используем скрипт с другого домена, и в нем имеется ошибка, мы не сможем узнать детали этой ошибки.
Для примера давайте возьмём мини-скрипт error.js
, который состоит из одного-единственного вызова функции, которой не существует:
// 📁 error.js
noSuchFunction();
Теперь загрузим этот скрипт с того же сайта, на котором он лежит:
<script>
window.onerror = function(message, url, line, col, errorObj) {
alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script src="/article/onload-onerror/crossorigin/error.js"></script>
Мы видим нормальный отчёт об ошибке:
Uncaught ReferenceError: noSuchFunction is not defined
https://javascript.info/article/onload-onerror/crossorigin/error.js, 1:1
А теперь загрузим этот же скрипт с другого домена:
<script>
window.onerror = function(message, url, line, col, errorObj) {
alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script src="https://cors.javascript.info/article/onload-onerror/crossorigin/error.js"></script>
Отчёт отличается:
Script error.
, 0:0
Детали отчёта могут варьироваться в зависимости от браузера, но основная идея остаётся неизменной: любая информация о внутреннем устройстве скрипта, включая стек ошибки, спрятана. Именно потому, что скрипт загружен с другого домена.
Зачем нам могут быть нужны детали ошибки?
Существует много сервисов (и мы можем сделать наш собственный), которые обрабатывают глобальные ошибки при помощи window.onerror
, сохраняют отчёт о них и предоставляют доступ к этому отчёту для анализа. Это здорово, потому что мы можем увидеть реальные ошибки, которые случились у наших пользователей. Но если скрипт – с другого домена, то информации об ошибках в нём почти нет, как мы только что видели.
Похожая кросс-доменная политика (CORS) внедрена и в отношении других ресурсов.
Чтобы разрешить кросс-доменный доступ, нам нужно поставить тегу <script>
атрибут crossorigin
, и, кроме того, удалённый сервер должен поставить специальные заголовки.
Существует три уровня кросс-доменного доступа:
- Атрибут
crossorigin
отсутствует – доступ запрещён. crossorigin="anonymous"
– доступ разрешён, если сервер отвечает с заголовкомAccess-Control-Allow-Origin
со значениями*
или наш домен. Браузер не отправляет авторизационную информацию и куки на удалённый сервер.crossorigin="use-credentials"
– доступ разрешён, если сервер отвечает с заголовкамиAccess-Control-Allow-Origin
со значением наш домен иAccess-Control-Allow-Credentials: true
. Браузер отправляет авторизационную информацию и куки на удалённый сервер.
Почитать больше о кросс-доменных доступах вы можете в главе Fetch: запросы на другие сайты. Там описан метод fetch
для сетевых запросов, но политика там точно такая же.
Такое понятие как «куки» (cookies) не рассматривается в текущей главе, но вы можете почитать о них в главе Куки, document.cookie.
В нашем случае атрибут crossorigin
отсутствовал. Поэтому кросс-доменный доступ был запрещён. Давайте добавим его.
Мы можем выбрать "anonymous"
(куки не отправляются, требуется один серверный заголовок) или "use-credentials"
(куки отправляются, требуются два серверных заголовка) в качестве значения атрибута.
Если куки нас не волнуют, тогда смело выбираем "anonymous"
:
<script>
window.onerror = function(message, url, line, col, errorObj) {
alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script crossorigin="anonymous" src="https://cors.javascript.info/article/onload-onerror/crossorigin/error.js"></script>
Теперь при условии, что сервер предоставил заголовок Access-Control-Allow-Origin
, всё хорошо. У нас есть полный отчёт по ошибкам.
Итого
Изображения <img>
, внешние стили, скрипты и другие ресурсы предоставляют события load
и error
для отслеживания загрузки:
load
срабатывает при успешной загрузке,error
срабатывает при ошибке загрузки.
Единственное исключение – это <iframe>
: по историческим причинам срабатывает всегда load
вне зависимости от того, как завершилась загрузка, даже если страница не была найдена.
Событие readystatechange
также работает для ресурсов, но используется редко, потому что события load/error
проще в использовании.