5 лютого 2022 р.

Завантаження ресурсів: onload та onerror

Браузер дозволяє нам відстежувати завантаження зовнішніх ресурсів – скриптів, фреймів, зображень тощо.

Для цього передбачено дві події:

  • onload – успішне завантаження,
  • onerror – виявлено помилку.

Завантаження скрипта

Скажімо, нам потрібно завантажити сторонній скрипт і викликати функцію, яка там знаходиться.

Ми можемо завантажити його динамічно, наприклад:

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 відстежують лише саме завантаження.

Помилки, які можуть виникнути під час обробки та виконання скрипта, виходять за рамки цих подій. Тобто: якщо скрипт завантажується успішно, то запускається onload, навіть якщо в ньому є помилки програмування. Щоб відстежувати помилки скрипта, можна використовувати глобальний обробник 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> подія iframe.onload запускається після завершення завантаження iframe, як для успішного завантаження, так і в разі помилки.

Це обумовлено історичними причинами.

Політика кросдоменних запитів

Існує правило: скрипти з одного сайту не можуть отримати доступ до вмісту іншого сайту. Отже, напр. скрипт на 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, а також віддалений сервер повинен надавати спеціальні заголовки.

Існує три рівні кросдоменного доступу:

  1. Немає атрибута crossorigin – доступ заборонено.
  2. crossorigin="anonymous" – доступ дозволений, якщо сервер відповідає із заголовком Access-Control-Allow-Origin з * або нашим джерелом. Браузер не надсилає інформацію про авторизацію та файли cookie на віддалений сервер.
  3. crossorigin="use-credentials" – доступ дозволений, якщо сервер надсилає назад заголовок Access-Control-Allow-Origin з нашим походженням та Access-Control-Allow-Credentials: true. Браузер надсилає інформацію про авторизацію та файли cookie на віддалений сервер.
Будь ласка, зверніть увагу:

Ви можете прочитати більше про доступ із різних джерел у розділі Fetch: Запити між різними джерелами. Він описує метод fetch для мережевих запитів, але політика точно така ж.

Таке поняття, як “cookies” не входить до нашої поточної теми, але ви можете прочитати про них у розділі Файли cookies, document.cookie.

У нашому випадку ми не мали атрибута crossorigin. Таким чином, кросдоменний доступ був заборонений. Додаймо його.

Ми можемо вибирати між "anonymous" (файли cookie не надсилаються, потрібен один заголовок на стороні сервера) та "use-credentials" (надсилає файли cookie, потрібні два заголовки на стороні сервера).

Якщо нам не важливі файли cookie, то нам підійде "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 простіші.

Завдання

важливість: 4

Зазвичай зображення завантажуються під час їх створення. Тому, коли ми додаємо <img> на сторінку, користувач не відразу бачить зображення. Спочатку його потрібно завантажити браузеру.

Щоб відразу показати зображення, ми можемо створити його “заздалегідь”, наприклад:

let img = document.createElement('img');
img.src = 'my.jpg';

Браузер починає завантажувати зображення і запам’ятовує його в кеші. Пізніше, коли те саме зображення з’являється в документі (незалежно від того, як), воно з’являється негайно.

Створіть функцію preloadImages(sources, callback), яка завантажує всі зображення з масиву sources і, коли буде готова, запускає callback.

Наприклад, після завантаження зображень буде відображатися alert:

function loaded() {
  alert("Зображення завантажені")
}

preloadImages(["1.jpg", "2.jpg", "3.jpg"], loaded);

У разі помилки функція все одно повинна вважати картинку “завантаженою”.

Іншими словами, callback виконується, коли всі зображення завантажуються або є помилка.

Ця функція корисна, наприклад, коли ми плануємо показати галерею з великою кількістю зображень, які можна прокручувати, і хочемо бути впевненими, що всі зображення завантажені.

У вихідному документі ви можете знайти посилання на тестові зображення, а також код, щоб перевірити, завантажені вони чи ні. Він має вивести 300.

Відкрити пісочницю для завдання.

Алгоритм:

  1. Зробіть img для кожного джерела.
  2. Додайте onload/onerror для кожного зображення.
  3. Збільште лічильник, коли спрацьовує onload або onerror.
  4. Коли значення лічильника дорівнює кількості джерел, ми закінчили: callback().

Відкрити рішення в пісочниці.

Навчальна карта