Нередко бывает, что при загрузке веб-страницы какие-то изображения оказываются недоступны, и браузер услужливо рисует на их месте пустые контуры. В этой статье я расскажу, как обработать ошибки загрузки изображений и подставить вместо них другое изображение.
Допустим, у нас на сервере лежит файл: https://svgshare.com/i/7ck.svg
Рассмотрим следующий пример. Сколько изображений будет показано?
<html>
<head>
<meta charset="utf-8" content="text/html; charset=utf-8"/>
<style>
html, body {
height: 100px;
width: auto;
}
</style>
</head>
<body>
<div class="achievement-image">
<img src="https://svgshare.com/i/7ck.svg">
<img src="//svgshare.com/i/7ck.svg">
<img src="svgshare.com/i/7ck.svg">
<img src="7ck.svg">
</div>
</body>
</html>
Одно, максимум два.

Ответ зависит от того, как открыть этот код. Если разместить его на сервере, вроде https://myserver.com, мы увидим первые два изображения. Если же сохранить его локально и открыть в браузере html файл - только одно. Для ясности я покажу, во что превращаются ссылки из атрибута src в обоих случаях.
В случае сервера https://myserver.com имеем:
https://svgshare.com/i/7ck.svg
https://svgshare.com/i/7ck.svg
https://myserver.com/svgshare.com/i/7ck.svg
https://myserver.com/7ck.svg
В случае локального файла из file:///home/leksa/html/example.html имеем:
https://svgshare.com/i/7ck.svg
file://svgshare.com/i/7ck.svg
file:///home/leksa/html/svgshare.com/i/7ck.svg
file:///home/leksa/html/7ck.svg
Для отслеживания и реакции на успешную или неуспешную загрузку изображения HTML предлагает нам два события, доступных для тега img: onload
и onerror
. Как следует из названия, первое событие возникает, когда изображение было загружено, а второе, когда что-то пошло не так.
Модифицируем наш код и проверим работу этих событий в консоли браузера.
<div class="achievement-image">
<img src="https://svgshare.com/i/7ck.svg"
onload="console.log('load 1')" onerror="console.log('error 1')">
<img src="//svgshare.com/i/7ck.svg"
onload="console.log('load 2')" onerror="console.log('error 2')">
<img src="svgshare.com/i/7ck.svg"
onload="console.log('load 3')" onerror="console.log('error 3')">
<img src="7ck.svg"
onload="console.log('load 4')" onerror="console.log('error 4')">
</div>

Как и ожидалось, последние три изображения спровоцировали событие onerror. Назначим на него свой обработчик (потребуется подключить библиотеку JQuery) и подменим атрибут src на адрес желаемого изображения.
$(document).ready(function() {
$(".image-container img").each(function(key, item) {
$(item).on("error", function() {
showDefaultImage(this);
}).attr('src', $(item).attr('src'));
});
});
function showDefaultImage(img) {
$(img).attr('src', 'https://svgshare.com/i/7bo.svg');
$(img).off("error");
}
В интернете есть много примеров решения данной задачи, однако есть один важный момент, о котором часто забывают. Хочу обратить ваше внимание на вызов
$(item).on(...).attr('src', $(item).attr('src'))
после назначения обработчика. Атрибут src устанавливается в то же значение, в котором он изначально был. Зачем это нужно?
Для ответа на этот вопрос вернёмся к последовательности возникновения событий.
- Событие
DOMContentLoaded
возникает, когда построено дерево DOM, а также загружены и выполнены все скрипты. При этом внешние ресурсы, включая изображения и стили, могут быть ещё не загружены.
- Событие
load
объекта window возникает, когда страница со всеми ресурсами была загружена.
- Событие
ready
специфично для JQuery, согласно документации, оно аналогично событию DOMContentLoaded, то есть не дожидается загрузки мультимедийного контента.
Откроем код, приведённый ниже, и посмотрим на вывод в консоли браузера.
<html>
<head>
<meta charset="utf-8" content="text/html; charset=utf-8"/>
<style>
html, body {
height: 100px;
width: auto;
}
</style>
<script>
$(document).on("DOMContentLoaded", function() {
console.log("document loaded");
});
$(window).on("load", function() {
console.log("content loaded");
});
$(document).ready(function() {
console.log(“JQuery ready”);
});
</script>
</head>
<body>
<div class="image-container">
<img src="https://svgshare.com/i/7ck.svg"
onload="console.log('load 1')" onerror="console.log('error 1')">
<img src="//svgshare.com/i/7ck.svg"
onload="console.log('load 2')" onerror="console.log('error 2')">
<img src="svgshare.com/i/7ck.svg"
onload="console.log('load 3')" onerror="console.log('error 3')">
<img src="7ck.svg"
onload="console.log('load 4')" onerror="console.log('error 4')">
</div>
</body>
</html>

Тут есть кое-что интересное. Вопреки ожиданием, событие ready срабатывает после загрузки всего контента (включая изображения), аналогично событию load. А значит, на момент создания обработчиков, события error и load уже произошли, и обработчики никогда не будут вызваны.
В браузере firefox картина примерно такая же.

Это вынуждает прибегать к не совсем красивому решению, когда мы заставляем браузер загрузить изображения заново, уже после привязки обработчиков, путём установки атрибута src в то же самое значение.
Окончательный вариант кода приведён ниже (живой пример).
<html>
<head>
<meta charset="utf-8" content="text/html; charset=utf-8"/>
<style>
html, body {
height: 100px;
width: auto;
}
</style>
<script>
// $(document).on("DOMContentLoaded", function() {
$(document).ready(function() {
$(".image-container img").each(function(key, item) {
$(item).on("error", function() {
showDefaultImage(this);
}).attr('src', $(item).attr('src'));
});
});
function showDefaultImage(img) {
$(img).attr('src', 'https://svgshare.com/i/7bo.svg');
$(img).off("error");
}
</script>
</head>
<body>
<div class="image-container">
<img src="https://svgshare.com/i/7ck.svg">
<img src="//svgshare.com/i/7ck.svg">
<img src="svgshare.com/i/7ck.svg">
<img src="7ck.svg">
</div>
</body>
</html>
