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

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

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

# サンプル
## 構成

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

## URL

* http://localhost:8082/index.html
本体

* http://localhost:8082/test1/hello.js
第１候補のスクリプト（存在しない）

* http://localhost:8082/test2/hello.js
第２候補のスクリプト

* http://localhost:8082/test3/hello.js
第３候補のスクリプト

## ソース
```js:hello.js
const hello = () => {
    alert("Hello, world!");
}
```
"Hello, world!"というダイアログを表示するhello関数のみ！

```html: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/089d4a90-6599-b470-dcb3-12354912098a.png)

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

### helloボタン押下時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/c96ea4bf-7540-368a-2fda-0ce10a1122c8.png)

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

## コンソール

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348583/424426c0-0748-5a19-5561-68850cd09140.png)

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

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

# 追記（2021/06/16）

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

# サンプル
## 本体

```js: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);
    };
}
```
# サンプルを実行

```html:実行前
<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が読み込めた場合
```diff_html:実行後
<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が読み込めなかった場合

```diff_html:実行後
<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>
```

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