サイトに CSS・JS が効いていない時にミラーの CSS・JS ファイルを読み込んでフォールバックさせるスクリプトを作った
このブログ Corredor で、iPhone から閲覧している場合に時々発生していたのだが、ブログの CSS・JS ファイルが読み込めずにレイアウトが崩れていることがあった。
このブログの CSS と JS ファイルは、GitHub Pages でホスティングしているモノを「はてなブログ」管理画面の「設定」にて、head
要素内で読み込むようにしている。
<!-- はてなブログ管理画面の「設定」→「詳細設定」タブ→「head に要素を追加」欄に、以下のように記載している -->
<link rel="stylesheet" href="https://neos21.github.io/HatenaBlogs/dist/styles/Corredor.css">
<script src="https://neos21.github.io/HatenaBlogs/dist/scripts/Corredor.js"></script>
どういうワケか、iPhone Safari で見る時だけこのファイルが上手く読み込まれず、レイアウトが崩れることがあったので、試しにフォールバック処理を組み込んでみることにした。
目次
- CSS・JS ファイルのフォールバック処理とは
- CSS・JS ファイルをホスティングするなら npm の CDN を使うのが手っ取り早い
- CSS のフォールバック処理
- JS ファイルのフォールバック処理
- 最後にフォールバック用スクリプトの全量
CSS・JS ファイルのフォールバック処理とは
ココでいう「フォールバック」とは、上述の HTML ソースで読み込ませようとした CSS ファイルや JS ファイルが読み込めなかった時に、別サイトにホスティングしている同じファイルを読み込ませる、ということだ。
当然ながら、CSS ファイル、JS ファイルを予めミラーサイトにホスティングしておく必要がある。
CSS・JS ファイルをホスティングするなら npm の CDN を使うのが手っ取り早い
CSS や JS のような静的なファイルをホスティングするなら、npm パッケージとして npm publish
し、unpkg や jsdelivr のような CDN サービスを利用して読み込むのが良いだろう。
unpkg や jsdelivr に対しては特に登録の必要はない。ただ npm publish
するだけで使えるようになるので、お手軽だ。
その他、サービス終了がアナウンスされているが、「Raw Git」やその類似サービスなど、GitHub 上にあるファイルに Content-Type を付けて返してくれるサービスも使えるだろう。GitHub Pages として配布するのとは少し違って、GitHub リポジトリの Raw ファイルを直接参照するっぽいので、フォールバックとして使えるだろう。
CSS のフォールバック処理
それではまず、CSS のフォールバック処理を用意してみる。
1. CSS 読み込みチェック用の HTML 要素を置く
まず、サイト内に CSS 読み込みチェック用の HTML 要素を置く。自分の場合は、以下のような空の要素をページの最下部など邪魔にならないところに置いておく。
<span id="n-check"></span>
2. CSS ファイルにて読み込みチェック用の要素にスタイルを当てる
次に、CSS ファイルで、読み込みチェック用の要素にスタイルを当てる。
/* CSS 読込が正常に行えているか確認するための検証用要素 */
#n-check {
display: none;
font-size: 0;
}
display: none
は念のため、要素を非表示にするために指定。ココでは font-size: 0
と指定してある。
3. 最初に読み込む CSS ファイルの link
要素に onload
・onerror
属性を付与する
前述の link
要素を基に、onload
・onerror
属性を追加する。
<link rel="stylesheet" href="https://neos21.github.io/HatenaBlogs/dist/styles/Corredor.css" onload="Neos21.styles(-1);" onerror="Neos21.styles(-1);">
コレで、この CSS ファイルが読み込まれた時、もしくは読み込みに失敗した時、いずれの場合でも、window.Neos21.styles()
という関数が実行されることになる。この関数の中身はコレから実装する。
4. CSS 読み込みチェックおよびフォールバック処理用のスクリプトを実装する
CSS ファイルの読み込みチェックと、読み込めていなさそうだった時にミラーサイトから CSS ファイルを読み込み直す、一連のスクリプトを以下のように実装する。
/**
* CSS・JS ファイルのフォールバック用グローバルオブジェクト
*/
Neos21 = {
/**
* link 要素または script 要素を head 要素に追加する
*
* @param isLink true なら link 要素・false なら script 要素を生成する
* @param nextIndex 添字
* @param nextUrl 読み込むファイル URL
*/
append: function(isLink, nextIndex, nextUrl) {
var d = document;
var s = 'setAttribute';
var eventValue = 'Neos21.' + (isLink ? 'styles' : 'scripts') + '(' + nextIndex + ');';
var elem = d.createElement(isLink ? 'link' : 'script');
if(isLink) {
elem[s]('rel', 'stylesheet');
}
elem[s](isLink ? 'href' : 'src', nextUrl);
elem[s]('onload' , eventValue);
elem[s]('onerror', eventValue);
d.querySelector('head').appendChild(elem);
},
/**
* CSS ファイルが読み込めているか検証し、必要に応じてフォールバック処理を行う
*
* @param index フォールバック URL の添字
*/
styles: function(index) {
// 第1引数が number 型で指定されていなければ中止する
if(typeof index !== 'number') {
return;
}
var w = window;
var d = document;
// 検証用要素の読み込みを待つため再呼び出しして終了する
if(!d.readyState || d.readyState === 'interactive') {
w.addEventListener('load', function() {
Neos21.styles(index);
});
return;
}
else if(d.readyState === 'loading') {
d.addEventListener('DOMContentLoaded', function() {
Neos21.styles(index);
});
return;
}
// フォールバック URL の定義
var urls = [
// unpkg ホスティングのミラーファイル
'https://unpkg.com/@neos21/hatena-blogs@1.0.4/dist/styles/Corredor.css',
// jsdelivr
'https://cdn.jsdelivr.net/npm/@neos21/hatena-blogs@1.0.4/dist/styles/Corredor.css',
// GitHack
'https://raw.githack.com/Neos21/HatenaBlogs/master/dist/styles/Corredor.css',
'https://rawcdn.githack.com/Neos21/HatenaBlogs/a9a8c7d78b940f1d90a8b07e40f04418f407c469/dist/styles/Corredor.css',
// Raw GitHub
'https://raw.githubusercontent.com/Neos21/HatenaBlogs/master/dist/styles/Corredor.css'
];
// 検証用要素
var check = d.getElementById('n-check');
if(!check) {
// 検証用要素なし・中止
return;
}
// 検証用要素にスタイルが適用されているか確認する
var checkStyle = parseInt(w.getComputedStyle(check).fontSize);
if(checkStyle === 0) {
// スタイル適用済なら正常・何もしない
return;
}
// フォールバックが必要
// 次の Index 値
var nextIndex = index + 1;
// フォールバック用 URL がなかったら対応不可・終了
if(nextIndex >= urls.length) {
return;
}
// link 要素を生成し挿入する
Neos21.append(true, nextIndex, urls[nextIndex]);
}
};
このフォールバック処理は window.Neos21
というグローバルオブジェクト内に styles()
という関数で定義している。
最初のブロックでは引数のチェックをしている。引数 index
は、このあと定義しているフォールバック用 CSS ファイルの URL を定義した配列の添字をズラすためのモノ。link
要素の onload
・onerror
属性で Neos21.styles(-1)
と指定したとおり、最初の CSS が読み込まれた時は -1
が指定されており、フォールバックが必要な場合はインクリメントして配列の 0
番目の URL からファイルを再読込しようとする。
次のブロックで、ページの読み込み状況を確認し、読み込みが終わっていなければ DOMContentLoaded
なり load
なりに自身を再呼び出しするイベントを追加して終了している。ページの読み込みが完了していない段階で実行すると、上手く CSS の読み込み状況が拾えないことがあるからだ。
そのあと、var urls
でフォールバック用の URL を定義している。link
要素で直接指定した CSS ファイルと同じ内容のモノを、ミラーサイト (CDN) に置いておき、その URL を配列に控えておく。要はこれらの URL から CSS ファイルを順に読み込んでいけば、どれかしら正常に読み込めるんじゃね? という魂胆だ。
さて、CSS ファイルが正常に読み込めているかどうかの検証だが、window.getComputedStyle()
という関数を使う。この関数は CSS が適用されて実際にどんなスタイルが当たっているのかを返してくれるモノで、この関数を使って検証用の HTML 要素 document.getElementById('n-check')
の font-size
をチェックすればよかろう、というワケだ。結果は '0px'
という文字列で返ってくるはずで、それを parseInt()
すれば 0
が取れるはず。もし CSS ファイルが当たっていなければ、デフォルトスタイルで '16px'
などの値が返ってくるはずなので、ココで読み込みチェックができる、ということ。必ずしも font-size
値で検証しないといけないワケではないが、display: none
にしていても影響がなく、color
のように期待値が分かりづらくなるプロパティを避けただけ。
検証用の関数に CSS が適用されていないようであれば、変数 urls
から次のフォールバック用 URL を取得し、link
要素を生成して head
要素に埋め込んでいる。この時も onload
・onerror
属性を付与しているので、続けざまにフォールバックが可能になっている。
5. フォールバック用スクリプトを元の link
要素より上にベタ書きする
このようなスクリプトを、link
要素より上にベタ書きする。
<!-- 先程のスクリプトを Uglify したもの -->
<script>
Neos21={append:function(e,t,s){var o=document,r="setAttribute",a="Neos21."+(e?"styles":"scripts")+"("+t+");",n=o.createElement(e?"link":"script");e&&n[r]("rel","stylesheet"),n[r](e?"href":"src",s),n[r]("onload",a),n[r]("onerror",a),o.querySelector("head").appendChild(n)},styles:function(e){if("number"==typeof e){var t=window,s=document;if(s.readyState&&"interactive"!==s.readyState)if("loading"!==s.readyState){var o=["https://unpkg.com/@neos21/hatena-blogs@1.0.4/dist/styles/Corredor.css","https://cdn.jsdelivr.net/npm/@neos21/hatena-blogs@1.0.4/dist/styles/Corredor.css","https://raw.githack.com/Neos21/HatenaBlogs/master/dist/styles/Corredor.css","https://rawcdn.githack.com/Neos21/HatenaBlogs/a9a8c7d78b940f1d90a8b07e40f04418f407c469/dist/styles/Corredor.css","https://raw.githubusercontent.com/Neos21/HatenaBlogs/master/dist/styles/Corredor.css"],r=s.getElementById("n-check");if(r)if(0!==parseInt(t.getComputedStyle(r).fontSize)){var a=e+1;o.length<=a||Neos21.append(!0,a,o[a])}}else s.addEventListener("DOMContentLoaded",function(){Neos21.styles(e)});else t.addEventListener("load",function(){Neos21.styles(e)})}}};
</script>
<!-- 最初に読み込ませる CSS ファイル -->
<link rel="stylesheet" href="https://neos21.github.io/HatenaBlogs/dist/styles/Corredor.css" onload="Neos21.styles(-1);" onerror="Neos21.styles(-1);">
フォールバック用スクリプトを外部ファイル読み込みにしてしまうと、そのスクリプトファイルが読み込めなかった時に対応しきれない。また、link
要素より先に書いておかないと、onload
・onerror
が先に発火してしまった時にグローバルオブジェクト window.Neos21
がまだ存在しなくてエラーになってしまう恐れがある。
以上で、CSS ファイルのフォールバック処理は完了だ。
JS ファイルのフォールバック処理
続いて JS ファイルも同様にフォールバック処理を用意していこう。
1. JS ファイルにて読み込みチェック用のグローバルオブジェクトを定義する
読み込む JS ファイル内で、グローバルオブジェクト window.Neos21
内に、JS ファイルが正常に読み込めていることを知らせるフラグ変数を用意する。
// JS ファイル内
// 本来の処理……省略……
// 検証用オブジェクト・プロパティを定義しておく
if(!window.Neos21) {
window.Neos21 = {};
}
// スクリプトが読み込めたことを知らせるフラグ変数を設定する
window.Neos21.scriptLoaded = true;
2. 最初に読み込む JS ファイルの script
要素に onload
・onerror
属性を設定する
前述の script
要素に、CSS ファイルの時に書いたような要領で onload
・onerror
属性を付与する。
<script src="https://neos21.github.io/HatenaBlogs/dist/scripts/Corredor.js" onload="Neos21.scripts(-1);" onerror="Neos21.scripts(-1);"></script>
コレで、この JS ファイルが読み込めた時も、読み込めなかった時も、window.Neos21.scripts()
関数が実行される、というワケ。
3. JS 読み込みチェックおよびフォールバック処理用のスクリプトを実装する
JS ファイルの読み込みチェックも、CSS ファイルの場合と似たような関数を用意して実現する。
/**
* CSS・JS ファイルのフォールバック用グローバルオブジェクト
*/
Neos21 = {
/**
* link 要素または script 要素を head 要素に追加する
*
* @param isLink true なら link 要素・false なら script 要素を生成する
* @param nextIndex 添字
* @param nextUrl 読み込むファイル URL
*/
append: function(isLink, nextIndex, nextUrl) {
var d = document;
var s = 'setAttribute';
var eventValue = 'Neos21.' + (isLink ? 'styles' : 'scripts') + '(' + nextIndex + ');';
var elem = d.createElement(isLink ? 'link' : 'script');
if(isLink) {
elem[s]('rel', 'stylesheet');
}
elem[s](isLink ? 'href' : 'src', nextUrl);
elem[s]('onload' , eventValue);
elem[s]('onerror', eventValue);
d.querySelector('head').appendChild(elem);
},
/**
* JS ファイルが読み込めているか検証し、必要に応じてフォールバック処理を行う
*
* @param index フォールバック URL の添字
*/
scripts: function(index) {
// 第1引数が number 型で指定されていること
if(typeof index !== 'number') {
return;
}
// フォールバック URL の定義
var urls = [
// https://unpkg.com/@neos21/hatena-blogs/
'https://unpkg.com/@neos21/hatena-blogs@1.0.4/dist/scripts/Corredor.js',
// https://www.jsdelivr.com/package/npm/@neos21/hatena-blogs
'https://cdn.jsdelivr.net/npm/@neos21/hatena-blogs@1.0.4/dist/scripts/Corredor.js',
// http://raw.githack.com/
'https://raw.githack.com/Neos21/HatenaBlogs/master/dist/scripts/Corredor.js',
'https://rawcdn.githack.com/Neos21/HatenaBlogs/a9a8c7d78b940f1d90a8b07e40f04418f407c469/dist/scripts/Corredor.js',
// Raw GitHub
'https://raw.githubusercontent.com/Neos21/HatenaBlogs/master/dist/scripts/Corredor.js'
];
// 検証用プロパティが存在しているか確認する
if(Neos21.scriptLoaded) {
// プロパティがあれば読込済・何もしない
return;
}
// フォールバックが必要
// 次の Index 値
var nextIndex = index + 1;
// フォールバック用 URL がなかったら対応不可・終了
if(nextIndex >= urls.length) {
return;
}
// script 要素を生成し挿入する
Neos21.append(false, nextIndex, urls[nextIndex]);
}
};
結局は window.Neos21.scriptLoaded
が true
かどうかをチェックして、true
でなければ別の URL から JS ファイルを読み込むよう、script
要素を生成して head
要素に追加しているだけ。
以上で JS ファイルもフォールバック処理させることができた。
最後にフォールバック用スクリプトの全量
最後に、掲載したフォールバック用スクリプトの全量を再掲載して終了する。
/**
* CSS・JS ファイルのフォールバック用グローバルオブジェクト
*/
Neos21 = {
/**
* link 要素または script 要素を head 要素に追加する
*
* @param isLink true なら link 要素・false なら script 要素を生成する
* @param nextIndex 添字
* @param nextUrl 読み込むファイル URL
*/
append: function(isLink, nextIndex, nextUrl) {
var d = document;
var s = 'setAttribute';
var eventValue = 'Neos21.' + (isLink ? 'styles' : 'scripts') + '(' + nextIndex + ');';
var elem = d.createElement(isLink ? 'link' : 'script');
if(isLink) {
elem[s]('rel', 'stylesheet');
}
elem[s](isLink ? 'href' : 'src', nextUrl);
elem[s]('onload' , eventValue);
elem[s]('onerror', eventValue);
d.querySelector('head').appendChild(elem);
},
/**
* CSS ファイルが読み込めているか検証し、必要に応じてフォールバック処理を行う
*
* @param index フォールバック URL の添字
*/
styles: function(index) {
// 第1引数が number 型で指定されていなければ中止する
if(typeof index !== 'number') {
return;
}
var w = window;
var d = document;
// 検証用要素の読み込みを待つため再呼び出しして終了する
if(!d.readyState || d.readyState === 'interactive') {
w.addEventListener('load', function() {
Neos21.styles(index);
});
return;
}
else if(d.readyState === 'loading') {
d.addEventListener('DOMContentLoaded', function() {
Neos21.styles(index);
});
return;
}
// フォールバック URL の定義
var urls = [
// https://unpkg.com/@neos21/hatena-blogs/
'https://unpkg.com/@neos21/hatena-blogs@1.0.4/dist/styles/Corredor.css',
// https://www.jsdelivr.com/package/npm/@neos21/hatena-blogs
'https://cdn.jsdelivr.net/npm/@neos21/hatena-blogs@1.0.4/dist/styles/Corredor.css',
// http://raw.githack.com/
'https://raw.githack.com/Neos21/HatenaBlogs/master/dist/styles/Corredor.css',
'https://rawcdn.githack.com/Neos21/HatenaBlogs/a9a8c7d78b940f1d90a8b07e40f04418f407c469/dist/styles/Corredor.css',
// Raw GitHub
'https://raw.githubusercontent.com/Neos21/HatenaBlogs/master/dist/styles/Corredor.css'
];
// 検証用要素
var check = d.getElementById('n-check');
if(!check) {
// 検証用要素なし・中止
return;
}
// 検証用要素にスタイルが適用されているか確認する
var checkStyle = parseInt(w.getComputedStyle(check).fontSize);
if(checkStyle === 0) {
// スタイル適用済なら正常・何もしない
return;
}
// フォールバックが必要
// 次の Index 値
var nextIndex = index + 1;
// フォールバック用 URL がなかったら対応不可・終了
if(nextIndex >= urls.length) {
return;
}
// link 要素を生成し挿入する
Neos21.append(true, nextIndex, urls[nextIndex]);
},
/**
* JS ファイルが読み込めているか検証し、必要に応じてフォールバック処理を行う
*
* @param index フォールバック URL の添字
*/
scripts: function(index) {
// 第1引数が number 型で指定されていること
if(typeof index !== 'number') {
return;
}
// フォールバック URL の定義
var urls = [
// https://unpkg.com/@neos21/hatena-blogs/
'https://unpkg.com/@neos21/hatena-blogs@1.0.4/dist/scripts/Corredor.js',
// https://www.jsdelivr.com/package/npm/@neos21/hatena-blogs
'https://cdn.jsdelivr.net/npm/@neos21/hatena-blogs@1.0.4/dist/scripts/Corredor.js',
// http://raw.githack.com/
'https://raw.githack.com/Neos21/HatenaBlogs/master/dist/scripts/Corredor.js',
'https://rawcdn.githack.com/Neos21/HatenaBlogs/a9a8c7d78b940f1d90a8b07e40f04418f407c469/dist/scripts/Corredor.js',
// Raw GitHub
'https://raw.githubusercontent.com/Neos21/HatenaBlogs/master/dist/scripts/Corredor.js'
];
// 検証用プロパティが存在しているか確認する
if(Neos21.scriptLoaded) {
// プロパティがあれば読込済・何もしない
return;
}
// フォールバックが必要
// 次の Index 値
var nextIndex = index + 1;
// フォールバック用 URL がなかったら対応不可・終了
if(nextIndex >= urls.length) {
return;
}
// script 要素を生成し挿入する
Neos21.append(false, nextIndex, urls[nextIndex]);
}
};
もう少しコードとしては削れそうなところがあるが、とりあえずやりたいことはできたので良き良き。
改変して利用いただく場合は、グローバルオブジェクト名 Neos21
を任意のモノに変えていただいたり、フォールバック用 URL 定義を忘れずに変更していただくぐらいだろうか。検証用のプロパティや要素の設定もコード量を削れそうな感はあるので、短い名称を使うなどして検証できればいいかしら。