LoginSignup
3
2

More than 1 year has passed since last update.

CDNからスクリプトが読めなくて困らないように考えてみた

Last updated at Posted at 2021-06-10

はじめに

CDNからスクリプトを読み込むようにしてて、CDNに障害が発生した場合
複数候補指定できたら解決するんじゃね?
というハナシ

ざっくり方法

スクリプトの候補を複数準備しておく
第1候補から繰り返す
1. 候補が存在するか確認する
2. 存在する場合、SCRIPTタグとして追加して終了

サンプル

構成

localhost:8082
│  index.html
├─test1
├─test2
│      hello.js
└─test3
       hello.js

URL

ソース

hello.js
const hello = () => {
    alert("Hello, world!");
}

"Hello, world!"というダイアログを表示するhello関数のみ!

index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8">
  <title>スクリプトを読み込むサンプル</title>

  <script>
    /**
     * スクリプトを読み込む
     * @param {string} src 読み込むソース
     * @return 読み込み結果
     */
    const loadScript = (src) => {
      // ソースの存在チェック
      const request = new XMLHttpRequest();
      request.open("GET", src, false);
      request.send(null);

      // ソースが存在した場合、スクリプトタグを追加
      if (request.status === 200) {
        let head = document.getElementsByTagName("head")[0];
        let script = document.createElement("script");
        script.src = src;
        head.appendChild(script);
        return true;
      }

      return false;
    };

    // スクリプトのソース一覧
    const srcArray = [
      "http://localhost:8082/test1/hello.js",
      "http://localhost:8082/test2/hello.js",
      "http://localhost:8082/test3/hello.js"
    ];

    // ソースの配列を先頭から、読み込めるまで読み込んでいく
    for (let i = 0; i < srcArray.length; i++) {
      if (loadScript(srcArray[i])) {
        console.log("Loaded script: " + srcArray[i]);
        break;
      }
    }

    window.onload = function () {
      hello();
    };
  </script>
</head>

<body>
  <button onclick="hello();">hello</button>
</body>

</html>

表示時とhelloボタン押下時に、読み込んだスクリプトのhello関数を実行する

サンプルを実行

画面

表示時

image.png

"Hello, world!"というダイアログが表示されている

helloボタン押下時

image.png

"Hello, world!"というダイアログが表示されている

コンソール

image.png

1行目のワーニングは、XMLHttpRequestを使って同期でリクエストを送るのは非推奨なので表示
2行目のエラーは、第1候補のスクリプトが存在しないため表示
3行目のログは、第2候補のスクリプトが読み込まれたという表示

かんそう

思いつきで書いたので、サンプルをそのまま使うのは厳しい…
折角なのでメモの代わりに記事として残しておきます。

追記(2021/06/16)

コメントを貰って、存在チェックはerrorイベントで良いと感じて書き直してみました!

サンプル

本体

scriptLoader.js
/**
 * スクリプトローダー
 * @class ScriptLoader
 */
class ScriptLoader {
    /**
     * コンストラクター
     * @memberof ScriptLoader
     */
    constructor() {
        this.index = 0;
        this.srcArray = [];
        this.callback = null;
    }

    /**
     * スクリプトのソースをセットする
     * @param {string} src ソース
     * @param {string} [backupSrc=null] ソースのロードに失敗した場合の、バックアップソース
     * @memberof ScriptLoader
     */
    setScript = (src, backupSrc = null) => {
        this.srcArray.push({src:src, backupSrc:backupSrc});
    }

    /**
     * セットしたスクリプトのソースを、スクリプトとしてロードする
     * @param {function} [callback=null] 完了時のコールバック
     * @memberof ScriptLoader
     */
    loadScripts = (callback = null) => {
        this.callback = callback;
        this.index = 0;
        this.loadScirpt(this.srcArray[this.index].src, this.srcArray[this.index].backupSrc);
    };

    /**
     * スクリプトをロードする(内部)
     * @param {*} src ソース
     * @param {*} backupSrc ソースのロードに失敗した場合の、バックアップソース
     * @memberof ScriptLoader
     */
    loadScirpt = (src, backupSrc) => {
        let script = document.createElement('script');
        script.src = src;

        script.addEventListener('load', {
            scriptLoader: this,
            handleEvent: function (event) {
                this.scriptLoader.index++;
                if (this.scriptLoader.index < this.scriptLoader.srcArray.length) {
                    // 次のソース配列が存在する場合
                    this.scriptLoader.loadScirpt(
                        this.scriptLoader.srcArray[this.scriptLoader.index].src,
                        this.scriptLoader.srcArray[this.scriptLoader.index].backupSrc);
                } else {
                    // 次のソース配列が存在しない場合
                    this.scriptLoader.srcArray.length = 0;
                    if (this.scriptLoader.callback && typeof this.scriptLoader.callback === 'function') {
                        this.scriptLoader.callback();
                    }
                }
            }
        }, false);

        script.addEventListener('error', {
            backupSrc: backupSrc,
            delete: script,
            scriptLoader: this,
            handleEvent: function (event) {
                if (this.backupSrc) {
                    // バックアップソースが設定されている場合
                    this.delete.remove();
                    this.scriptLoader.loadScirpt(this.backupSrc, null);
                } else {
                    // バックアップソースが設定されてない場合
                    this.scriptLoader.index++;
                    if (this.scriptLoader.index < this.scriptLoader.srcArray.length) {
                        // 次のソース配列が存在する場合
                        this.scriptLoader.loadScirpt(
                            this.scriptLoader.srcArray[this.scriptLoader.index].src,
                            this.scriptLoader.srcArray[this.scriptLoader.index].backupSrc);
                    } else {
                        // 次のソース配列が存在しない場合
                        this.scriptLoader.srcArray.length = 0;
                        if (this.scriptLoader.callback && typeof this.scriptLoader.callback === 'function') {
                            this.scriptLoader.callback();
                        }
                    }
                }
            }
        }, false);

        let head = document.getElementsByTagName('head')[0];
        head.appendChild(script);
    };
}

サンプルを実行

実行前
<head>
  <script src="scriptLoader.js"></script>
  <script>
    let loader = new ScriptLoader();
    loader.setScript('xxxxxxxxxx/hello.js', 'src/hello.js');
    loader.setScript('src/world.js');
    loader.loadScripts(() => {
      // スクリプト読み込み完了時に呼び出される
    });
  </script>
</head>

xxxxxxxxxx/hello.jsが読み込めた場合

実行後
<head>
  <script src="scriptLoader.js"></script>
  <script>
    let loader = new ScriptLoader();
    loader.setScript('http://xxxxxxxxxx/hello.js', 'src/hello.js');
    loader.setScript('src/world.js');
    loader.loadScripts(() => {
      // スクリプト読み込み完了時に呼び出される
    });
  </script>
+  <script src="http://xxxxxxxxxx/hello.js"></script>
+  <script src="src/world.js"></script>
</head>

xxxxxxxxxx/hello.jsが読み込めなかった場合

実行後
<head>
  <script src="scriptLoader.js"></script>
  <script>
    let loader = new ScriptLoader();
    loader.setScript('http://xxxxxxxxxx/hello.js', 'src/hello.js');
    loader.setScript('src/world.js');
    loader.loadScripts(() => {
      // スクリプト読み込み完了時に呼び出される
    });
  </script>
+  <script src="src/hello.js"></script>
+  <script src="src/world.js"></script>
</head>

かんそう2

割とちゃんと動くと思います。
ただ実用性があるかは謎のままです。

3
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2