パブリックCDNからのロードに対して、フォールバックを行う

# 何かと便利な、パブリックCDN

CDNはコンテンツ・デリバリー・ネットワークの略で、配信を高速化することに特化したネットワークのことをいいます。

むろん、アクセスの多いサイトを運営している場合、商用のCDNを契約することがあるかもしれませんが、そうでなくても便利な場面があります。Google（[Google Hosted Libraries](https://developers.google.com/speed/libraries/)）やMicrosoft（[Microsoft Ajax Content Delivery Network](https://www.asp.net/ajax/cdn)）、MaxCDN（[BootstrapCDN](https://www.bootstrapcdn.com/)）などのCDN提供側の事業者が、オープンソースプロジェクトをホスティングさせていて、誰でも自由に利用可能となっているのです。これらを使うことで、

* CDN経由で高速な配信を、そのライブラリについて受けられる
* テスト程度に書き進める場合にも、ライブラリのファイルを用意せずに始められる
* 同じファイルを他のサイトでも使っていた場合、キャッシュを共用できる

など、各種のメリットがあります。ただ、一方で「万が一CDNが落ちた場合に、必要なライブラリが揃わなくなる」ということが起きます。

とはいえ、うまく工夫すれば、「ふだんはCDN経由で取得しておいて、読み損なった場合にのみローカルサーバのJS/CSSファイルへフォールバックする」ということが行なえます。

# JavaScriptの場合
JavaScriptの中にも、jQuery本体のように、直後のJavaScriptロードなど`DOMContentLoaded`前に必要なために「その場で読み込むのが適当なもの」と、`DOMContentLoaded`時点で揃っていればそれでいいものがありますので、その2つに分けて考えます。

## 読み込み時の動作
ブラウザで`<script src>`を読み出した場合、ロードと実行のタイミングは以下のようになります。

* `async`や`defer`なしの場合…その場でロードして実行[^1]
* `defer`あり…`DOMContentLoaded`の前に、（`defer`どうしは順番に従って[^2]）実行
* `async`あり…`onload`の前のどこかでロードして実行

ということで、これらに従って「正しく動作すれば読み込みが行われるはずのタイミング」でフォールバック処理を入れることとなります。

## その場でロードが必要なもの
`async`や`defer`なしの場合、次の行に`<script>`要素を置けば、その中ではロードが終わっているはずです。そして、ブラウザで使うJavaScriptにはたいていの場合、グローバルに現れるオブジェクトがあります（jQueryプラグインの場合は`$.fn.xxx`に現れる）ので、それの有無を検知して、なければ`document.write`で`<script>`を書き出してしまいましょう（`document.write`を入れるとDOMが作り直しになりますが、こういう場合はむしろ適切です）。

```html
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="/path/to/local/jquery.js"><\/script>')</script>
```

ここで1点注意ですが、`<script>`要素内のインラインスクリプトに`</script>`と書くと、**そこでスクリプトが途切れてしまいます**。回避するには、

* `<\/script>`のようにエスケープする
* `unescape(''%3C/script%3E'')`のようにエスケープから戻す
* `'<' + '/script>'`のように分ける

など、いくつか方法があります。

## `async`で読み込むもの
`async`をかけた場合、ロードのタイミングが「`onload`前のどこか」ということになります。ということで、`window.onload`にイベントを仕掛けて、読み込まれていなければ`<script>`要素をDOMで生成、という流れでいいでしょう。

# CSSの場合
CSSの場合、JavaScript以上に「正しく読み込まれているかをFeature Detectionで確認する」というのが面倒になります。もっと楽な方法として、「`<link>`要素に`onload`を仕掛けて、そこから処理」という方法が考えられます。なお、この方法に対応するのは、

* Firefox 9以上
* Chrome 19以上
* Edge
* IE 6以上
* Safari 6以上（iOSも含む）
* Opera 9以上（Blink版も含む）

となっていて、「Android標準ブラウザ」では動きませんので、注意しましょう（[情報元](https://pie.gd/test/script-link-events/)）。

また、CSSのロードは`onload`以前に行われれるもので、`DOMContentLoaded`はCSSのロードを待ちませんので、`onload`に仕掛ける必要があります。一方で、CSSどうしに（こっちをロードしておかないと別のがロードにも失敗する、という意味合いでの）依存関係は発生し得ないので、後から差し替えても問題なく表示できます。

簡潔に書けるようにしたものを、下に書いておきます。

```html
<head>
<!-- class="fallback" と data-fallback="URL"を追加 -->
<!-- ロードに成功したらclassを外す -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"
onload="this.className='';" class="fallback" data-fallback="/path/to/local/bootstrap.min.css">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"
onload="this.className='';" class="fallback" data-fallback="/path/to/local/font-awesome.min.css">
<!-- 中略 -->
<script>
// DOMで書くのが面倒なので、jQueryにしています
$(window).on('load', fcuntion(){
  $('link.fallback').each(function(){
    var $elem=$(this);
    $elem.attr('src', $elem.data('fallback'));
  });
});
</script>
</head>
```

# 参考ページ
* [DOMContentLoaded周りの処理を詳しく調べてみました](http://qiita.com/mamo/items/ff336b5cc0a1a95e03a7)

# 脚注
[^1]: ブラウザによっては最適化で先のファイルを読み始めることもありますが、動作としてはこう処理した時と同じ結果になります。
[^2]: 古いブラウザでは守ってくれないこともあるらしいです。