Corredor

ウェブ、プログラミングの勉強メモ。

onerror イベントで img・script 要素の読み込みエラーをうまく検知できなかったら

これからWebをはじめる人のHTML&CSS、JavaScriptのきほんのきほん

これからWebをはじめる人のHTML&CSS、JavaScriptのきほんのきほん

img 要素や script 要素に対し、onerror 属性を設定したり、addEventListener('error') とイベントハンドラを設定したりすると、その外部ファイルが読み込めなかった時に特定の処理が行える…。という記事を以前書いた。

neos21.hatenablog.com

この中で、DOMContentLoaded イベント内で要素を特定して onerror イベントを一括指定すれば良い、といったことを書いたのだが、コレが上手く動作しない場合があるので、紹介しておく。

DOMContentLoaded イベントは遅すぎる…

HTML 中に onerror 属性をベタ書きするのは気が引けるので、DOM 構築が完了したところで、element.onerror に関数を代入しよう、という話だった。onerror への代入にせよ、addEventListener('error') で指定するにせよ、どちらでも同じだ。

<head>
  <!-- …中略… -->
  <script>
    // DOM 要素が構築できていない間にイベント追加はできないので DOMContentLoaded まで待つ
    document.addEventListener('DOMContentLoaded', () => {
      // 画像1
      document.getElementById('not-found-1').onerror = () => {
        console.log('画像が読み込めなかったよ');
      };
      
      // 画像2
      document.getElementById('not-found-2').addEventListener('error', () => {
        console.log('画像が読み込めなかったよ');
      });
    });
  </script>
</head>
<body>
  <img src="not-found-1.jpg" id="not-found-1" alt="画像1">
  <img src="not-found-2.jpg" id="not-found-2" alt="画像2">
</body>

DOMContentLoaded を待たないと、対象の要素が存在しないのにイベント設定しようとしてエラーになってしまうので、DOMContentLoaded で良いんじゃないか、と思いきや、これがうまくいかない場合がある。すなわち、この場合だとコンソールログが出力されない場合があるのだ。

どうやら、DOMContentLoaded イベントまで待つと、既に画像やスクリプトの読み込みが終わっており、Error イベントが発火し終わっている場合があるようで、うまく Error イベントを拾えないようだった。

body 要素末尾に script 要素を置くと軽減できる

そこで次のように、body 要素の最後に配置した script 要素にて、DOMContentLoaded イベントを待たずに DOM 操作してみた。コレなら、スクリプトを読み込む時点で対象の要素がみつからない、ということは回避できるだろう。

<body>
  <img src="not-found-1.jpg" id="not-found-1">
  <img src="not-found-2.jpg" id="not-found-2">
  
  <script src="not-found-3.js" id="not-found-3"></script>
  <script src="not-found-4.js" id="not-found-4"></script>
  
  <script>
    document.getElementById('not-found-1').onerror = () => {
      console.log('画像が読み込めなかったよ');
    };
    document.getElementById('not-found-2').addEventListener('error', () => {
      console.log('画像が読み込めなかったよ');
    });
    document.getElementById('not-found-3').onerror = () => {
      console.log('スクリプトが読み込めなかったよ');
    };
    document.getElementById('not-found-4').addEventListener('error', () => {
      console.log('スクリプトが読み込めなかったよ');
    });
  </script>
</body>

この場合、img 要素に関しては、コンソールログが正しく出力された。しかし、script 要素の方はコンソールログが出力されなかった。

script 要素は通常、script 要素が見つかった時点でスクリプトの読み込みを開始し、読み込みが完了するまで画面描画や後続のデータの読み込みをブロックする。そのため、イベントを追加するための script 要素が後ろにあると「もう遅い」のである。

一番安定しているのは onerror 属性値を HTML 中に書く方法だった

img 要素・script 要素いずれの場合でも必ずエラー処理ができるやり方は、HTML 中に onerror 属性を書いておくやり方だった。

<img src="not-found.jpg" onerror="console.log('画像が読み込めなかったよ');">
<script src="not-found.js" onerror="console.log('スクリプトが読み込めなかったよ');"></script>

さて、img 要素と script 要素とで、外部ファイルの読み込みタイミングが少し違うようだった。もう少し検証してみよう。

動的に要素を生成した時、外部ファイルはいつ読み込まれるか

次は、動的に img 要素や script 要素を生成し、src 属性を設定して、HTML 中に appendChild() してみる。この処理の中で、いつ画像ファイルやスクリプトファイルの読み込みが行われているのか、開発者ツールで見てみた。

// 画像の場合
let img;
function createImg1() { img = document.createElement('img'); }
function createImg2() { img.src = 'something.jpg'; }
function createImg3() { document.body.appendChild(img); }

// スクリプトの場合
let script;
function createScript1() { script = document.createElement('script'); }
function createScript2() { script.src = 'something.jpg'; }
function createScript3() { document.body.appendChild(script); }

これらの関数を1つずつ、画面上に配置したボタンから呼び出してみて、画像ファイルやスクリプトファイルが読み込まれるタイミングを調べた。

Chrome ブラウザで試した結果、img 要素の場合は createImg2()、つまり src 属性に値が代入された時に読み込みが開始した。一方 script 要素は、src 属性値が決まったタイミングではなく、appendChild() した時に初めて読み込みが始まった。

動的に追加する要素に Error イベントを仕込むべきタイミング

つまり、動的に追加する要素に対して Error イベントを仕込むには、img 要素は src 属性値を設定する前script 要素は HTML に追加する前でないと、意図したとおりに動作しないワケだ。

// 画像の場合
function createImg() {
  const img = document.createElement('img');
  img.onerror = console.log('画像が読み込めなかったよ');  // ← src 属性値が決まる前に設定しておく
  img.src = 'something.jpg';
  document.body.appendChild(img);
}

// スクリプトの場合
function createScript() {
  const script = document.createElement('script');
  script.src = 'something.jpg';
  script.onerror = console.log('スクリプトが読み込めなかったよ');  // ← HTML に追加する前に設定しておく
  document.body.appendChild(script);
}

以上

外部ファイルの読み込みに失敗した時に処理したいことは時々ありうるだろうが、地味に読み込みタイミングとイベントの設定タイミングがキモになるので、注意してほしい。