Open1

クライアントサイド JavaScript の呼び出し方の変遷

BugbearR PNBugbearR PN

歴史的経緯を踏まえて
自分の理解の確認を兼ねて
気が向いたら順次追加する方向で
歴史的経緯は多少前後しているかもしれない。

JavaScript と HTML は基本的に過去の資産を壊さないような方針で拡張されてきているので、過去のままでもある程度は動作してしまうが、現在での望ましい方法とは異なってしまう。
歴史的経緯を知ることは、どういう要望からその仕様が現れたのかが分かりやすいので、歴史をたどる方法で書く。

簡単に(2021年時点で)

  • prototype.js は絶対に使うな。
  • jQuery で起動させる必要性はもうない。むしろ逆効果。
  • script 要素を body 閉じタグの直前に書く必要性はもうない。むしろ逆効果。
  • document.write は使ってはならない。
  • script 要素の async/defer 属性を適切に使う。
    • async/await の async とはまったく無関係なので混同しないこと。
    • async 属性は DOM構築を待たずに処理を進めるのに使える。ただし他の async とも非同期になることに注意。
    • defer は DOM構築完了後、書かれた順で実行される。
  • type="module" 属性はまだ時期尚早か。
    • モジュールバンドラを使うなら要らない気がする。
  • DOM構築完了待ちは DOMContentLoaded イベントを使う。
  • リソース読み込み待ちは load イベントを使う。
    • 個別の要素でも各要素に load イベントがある。
  • モジュールバンドラを使った方がよい。
  • 共通ライブラリは CDN を使うことを検討する。
  • 自分のコードを CDN に置くことを検討する。

原初の JavaScript

JavaScript は HTML 内に script 要素として出現する。

インラインの script と、外部参照の script とが存在する。

インラインの script

<script>
alert("Hello, world!");
</script>

外部参照の script

<script src="hello.js"></script>

なお、HTML の script 要素は XML の空要素タグのように使えないので注意。
つまり、以下のように書くことがなぜかできない。
HTML4 の時には XML 記法自体が非合法だったが、XML 記法が認められた HTML5 でもなぜかダメ。

<script src="hello.js"/>

JavaScript は何も属性で指定しない場合、出現した時点でその場で解釈されて実行される。
なぜ、その場で解釈されたのかというと、当初は、document.write(string) で HTML 内に直接出力ができるという仕様だったため。(現在は望ましくないとされている。ブラウザによっては無視される。)
Document.write() https://developer.mozilla.org/ja/docs/Web/API/Document/write

問題は、以下の2点

  • JavaScript がロードされて完了するまでは、その次のドキュメント(およびリソース)を読み出せない。
    • ユーザーは白紙か描画が中途半端な画面を見続けることになる。
    • 複数の JavaScript (script 要素)がある場合、前の JavaScript のロードと実行が完了するまで、次の JavaScript がロードされない。
  • JavaScript が実行された時にはまだ DOM 構築が完了していない。
    • このため、DOM操作をすると、存在しない要素へのアクセスでエラーになる。

当初は、DOM構築完了を待つには onload イベントを待つのが常套手段だった。
以下のようなコードが一般的に書かれた。

window.onload = function () {
    // DOM関連初期化処理
}

問題は、以下の2点

  • onload イベントは HTML 内のリソースのすべてのロードが終わったときに呼ばれる。
    • これはかなり遅い。特に画像の読み込みで待たされる。
  • window.onload に初期化関数を代入する方法だと、1つしか登録できない。
    • 複数のスクリプト間で、初期化関数の奪い合いが発生してしまう。(後勝ちになってしまう。)

画面表示までの時間短縮とDOM構築完了待ちの時間短縮

原初の JavaScript の問題を解決すべく、様々な方法が考案された。

ブラウザ独自、ライブラリ、Web 標準としての対応策

基本的には以下の3つの対応となる。

  • 複数イベントハンドラ対応
  • ロードと起動タイミング対応
  • ブラウザ間互換性対応

script 要素を </body> の直前に書く方法

  • </body> の直前まで行けば、DOM構築は完了している。(厳密には完了していないが、実用上困らない。)
  • 画面の描画のための情報はここまで行けば完了しているので、ほぼ完全に描画できる。

IE での document.documentElement.doScroll('left'); のエラー判定の利用

DOMContentLoaded は IE 9 で対応された。
IE 8 までは、トリッキーな方法で判定されていた。
以下は原理を示す物で実際のコードではない。

function isDOMContentLoadedForOldIE() {
    try {
        document.documentElement.doScroll('left');
        return true;
    }
    catch (e) {
        return false;
    }
}

function onDOMContentLoadedForOldIE(fn) {
    if (isDOMContentLoadedForOldIE()) {
        fn();
        return;
    }
    // イベントではないため、ポーリングで待つしかない。
    setTimeout(function () { onDOMContentLoadedForOldIE(fn); }, 100);
}

なお、document.body を使うのはおそらく間違い。(一部にそういうコードが見られる。)
body タグが読み出される前は html 要素しか存在しないので document.documentElement が正しいと思われる。

複数イベント登録対応

イベントハンドラとして obj.onXXX に関数を設定する方法では複数の関数を登録できなかったため、代替方法が必要とされた。

IE による attachEvent メソッドの追加

IE 8 までは、attachEvent という独自メソッドを使用していた。

addEventlistener メソッドの登場

addEventListener メソッドが正式に採用された。
addEventListener https://caniuse.com/?search=addEventlistener

ブラウザ互換ライブラリによるイベント関数

IE 8 までは attachEvent しかなく addEventListener がないため互換関数が必要とされた。

  • prototype.js
    • Event.observe(オブジェクト, イベント名, 関数, キャプチャフェーズ);
Event.observe(window, "load", function () {
// ロード時処理
}, false);
  • jQuery
  • jQueryオブジェクト.on(イベント名, 関数)
$(window).on("load", function() {
// ロード時処理
});

IE による defer 属性の追加から Web 標準への正式採用

defer attribute for external scripts https://caniuse.com/script-defer

DOMContentLoaded イベントの登場

DOM 構築完了を示す、そのものズバリのイベントとして、DOMContentLoaded イベントが定義された。
つまり、DOM構築完了してから動作させたい関数は DOMContentLoaded イベントを待てばよい。

DOMContentLoaded https://caniuse.com/domcontentloaded

async 属性の登場

async attribute for external scripts https://caniuse.com/script-async

モジュール概念の登場 (type="module")

ECMAScript 6 (ES6)(ES2015) からモジュールが使えるようになった。

JavaScript modules via script tag https://caniuse.com/es6-module

<!--

ブラウザ互換ライブラリ関連

採用のタイミングの記述が難しいので別にする。
-->

ブラウザ互換ライブラリによるDOM構築完了イベント

DOMContentLoaded が全面採用されるまでは、互換関数が必要とされた。

  • jQuery
    • $(document).ready(関数)
    • $(関数)
$(function () {
// DOM構築完了時処理
});

モジュールバンドラによる JavaScript の一元化

  • 複雑度が増すと、JavaScript の数の多さが問題となることがある。
  • 元々の HTML ではセッション数として2程度しかないため、2つづつダウンロードされてきた。
  • これを1つのファイルにまとめてしまうことで、リクエストと応答が1回だけになる。

JavaScript の minify

  • プログラムを可能な限り小さくする方法。
  • 今はモジュールバンドラにその機能がまずある。
  • 名前を短くする。
  • 余計な空白、改行を取り除く。

CDNによる地域ごとのダウンロード共通化

  • そもそも主要なライブラリはどこでも共通であるため、ダウンロード先をCDNにすることで、キャッシュを有効活用することができる。
  • CDN は同じドメイン名でも地域ごとに適したサーバーが選ばれるようになっているため負荷分散される。
    • 地域外との通信は高コストなので地域内のサーバーと通信することでコストを下げられる。
  • デメリットとしてはCDNのダウンに巻き込まれてしまうことがある。

HTTP/2 の登場(JavaScriptとはあまり関係ないが)

  • 元々の HTTP では数々の問題があった
    • 1セッション1リクエストしか処理できなかった。
    • プレインテキストのため、通信量がやや多い
  • HTTP/2
    • 並行してリクエストができるように。
    • バイナリで通信する。
    • 圧縮される。