iframeを使って疑似SPA!!
iframeはとても便利
objectやembedも便利ですが、iframeはとても便利。
ソース忘れましたが、使い分けはこう
iframe知らない人向けに説明
一言でいえば、html上で、別のhtmlファイルを表示できるよってタグです。
こうやってGoogleMapが埋め込まれたサイトみたことない?
これ、HTMLのタグ1つでできるんだぜ。
iframeで自分のファイルの中から、他htmlを読み込む
フォルダ階層
root/
├ ref1/test1.html
├ ref2/test2.html
└ index.html
この状態で、index.htmlにtest1.htmlとtest2.htmlを読み込んでみよう。中身はこう
index.html
<!DOCTYPE HTML>
<html>
<body>
<iframe src="./ref1/test1.html"></iframe>
<iframe src="./ref2/test2.html"></iframe>
</body>
</html>
test1.html
てすとだよ
test2.html
うおおおおおお
へんな枠残っとるやんけ
そうなんですよね。オプションで枠自体は消せる。が、、、
devツールでみれば正体がわかる。
documentごと、iframeタグの子要素として埋め込んでるんです。
必要なのはbodyとかlinkのcss、scriptのjsだけなんじゃ!ほかは消えてくれ!っておもうじゃん?
必要な部分だけ抜き出そう
iframeはそのままhtmlに入れるんじゃなくて、iframeでhtmlを読み込んだdocumentオブジェクトを用意するだけ。
用意したdocumentのbody配下を抜き出して、htmlに追加してあげればいい。
こんな感じ
main.js
let iframe = document.createElement('iframe');
iframe.id = 'かぶらないようにつくったid';
iframe.src = './ref1/test1.html';
iframe.style.display = 'none'; // 非表示にしておこう
iframe.onload = () => {
// iframeタグがhtmlに追加されて、外部htmlを読み込み終わった後に呼ばれる
// いったん、iframeで読み込んだhtmlのbodyをconsoleに表示してみよう
let test = document.querySelector(`#かぶらないようにつくったid`)
.contentWindow.document.body.cloneNode(true).childNodes;
console.log(test);
};
document.body.appendChild(iframe);
main.jsをindex.htmlにいれて実行すると、body配下が表示される。
もちろんcross-originなアクセスだと、
.contentWindowで取得できないので
ローカルでお手軽に実験したいなら、GoogleChromeのローカルwebサーバがおすすめ。
やってみよう
注意点として、読み込むhtmlは、js読み込み後のものだが
ボタンに登録されたイベントなどは引き継げないのだ。
なので、html読み込み後に、jsも読み込む必要がある。
こういったフローで他htmlをindex.htmlに読み込む。
ではきれいに書いてみよう。
index.html
<!DOCTYPE HTML>
<html>
<body>
<div id="test"></div>
</body>
<script src="./main.js"></script>
</html>
main.js
/**
* コンポーネントをロードする.
* iframeとしてコンポーネントを親要素に追加し
* iframeロード完了後に, iframe配下をロード先要素に追加
*/
function load() {
// ロード先の要素id
const targetId = 'test'; // 外部ファイルや引数などに登録したいね
// 一時ロードされるiframe用のidを用意(重複しないようにしてね)
// documentからidで要素を取得し、あれば文字を変える、など。ここでは割愛
let iframeId = 'uniqueid';
// ソースを用意
const iframeSrc = './ref1/test1.html'; // 外部ファイルや引数などに登録したいね
const styleHref = './ref1/test1.css'; // 外部ファイルや引数などに登録したいね
const scriptSrc = './ref1/test1.js'; // 外部ファイルや引数などに登録したいね
// 1. ロード先配下をクリア
const clear = (v, r) => {
let elem = document.querySelector(`#${targetId}`);
while (elem.firstChild) {
elem.removeChild(elem.firstChild);
}
r(v);
};
// 2. iframeロード後, iframe要素をクローンしてロード先に移動
const setDom = (v, r) => {
let elemArray = document.querySelector(`#${iframeId}`)
.contentWindow.document.body.cloneNode(true).childNodes;
let elem = document.querySelector(`#${targetId}`);
let i = 1;
while (0 < elemArray.length) {
// HTMLCollection#itemで要素を取得すると
// 元listから削除されるので, 固定index0
elem.appendChild(elemArray.item(0));
// 処理速度改善 型認識させるため|0を追記
i = (i + 1) | 0;
}
// 元要素は削除
document.querySelector(`#${iframeId}`).remove();
r(v);
};
// 3. CSSをセット
const setCss = (v, r) => {
// ほんとはここで、旧ソースを削除するなどするが、今回は割愛
// 追加
let el = document.createElement('link');
el.rel = 'stylesheet';
el.type = 'text/css';
el.href = styleHref;
// ソースが見つからない場合, appendChildが完了してからエラーが出るので, try-catchで拾えない。
// 今回はソースがない場合は考慮しない
document.head.appendChild(el);
r(v);
};
// 4. JSをセット
const setJs = (v, r) => {
// ほんとはここで、旧ソースを削除するなどするが、今回は割愛
// 追加
let el = document.createElement('script');
el.type = 'module';
el.src = scriptSrc;
document.body.appendChild(el);
r(v);
};
// 5. ロード完了後処理を実行
const _after = (v, r) => {
console.debug(`--Load End`);
// もしやりたければコールバック関数を入れて、ここで実行とかね。
r(v);
};
// ========================================
// ロードの実行順を定義
// ========================================
// iframeロード設定
const createIframe = (v, r) => {
let iframe = document.createElement('iframe');
iframe.id = iframeId;
iframe.src = iframeSrc;
iframe.style.display = 'none';
iframe.onload = () => seriesExe([setDom, setCss, setJs, _after]);
document.body.appendChild(iframe);
r(v);
};
console.debug(`--Load Start`);
seriesExe([clear, createIframe]);
}
/**
* 関数を直列に同期実行. 以下の形式の関数の配列を引数とする.
* (v, r) => {任意の実装, r(v)を実行したタイミングで完了}
* 例)
* const sampleFunction = (v, r) => {
* // 任意の実装
* setTimeout(() => {
* // 遅延して完了し, 次の関数へ
r(v);
* }, 1000);
* }
* [sampleFunction, sampleFunction, ...]が引数となる.
*
* @param arr 直列実行する関数の配列 (index順)
* @return arrを全て実行したあとのPromiseを返却
*/
function seriesExe(arr) {
return arr
.reduce((x, y) => x.then(v => new Promise(r => y(v, r))),
Promise.resolve());
}
load();
結果はこう。
まぁjsとcssは用意してないから404で当たり前だが、用意すればちゃんと読み込まれる。
これを複数回使ったりとかすれば、ずっとindex.htmlだけどページは切り替わる、みたいなことができるわけだ。
疑似SPAと言ってよいのでは!
欠点と対処法
1 jsやcssが読み込まれなくなる問題
複数回読み込んだり前htmlを削除したりするとおこるのですが
scriptのsrcやlinkのcssのhrefが、うまく読み込まれません。
ブラウザのキャッシュ見ちゃうのかな。
対処法として、
<script src="./ref1/test.js">
<script src="./ref1/test.js?v=1">
<script src="./ref1/test.js?v=2">
...
というように、srcの後ろに?v=1などとバージョン情報をつけることで、再読み込みされます。
しかしそうなると古い要素を消さない限り、読み込んだ回数だけつくられちゃうので注意。
<script src="./ref1/test.js?v=100000000">とかまで要素消されずにのこってたら笑うでしょ。
2 ページ更新されたとき
また、index.htmlしか表示してないので、ページを更新すると必ず初期画面になっちゃいますね。
なので、sessionStorageなどに、現在表示している要素の情報を持っておく必要がありますが、そうなるとフレームワークなのにsessionStorageつかうの?って感じでちょいかっこわるいですね。
まぁセッションってそういうもんなので別にいいと思いますが、いやなら、js内部でオブジェクトを作成してセッション扱いしましょう。
3 共通化しようとすると...
testというhtmlをロードする場合、testというjsと、testというcssを読み込む、などはできます。
しかし、ない場合読み込まない、といった処理には、「test.jsというファイルが存在するか」を判定する必要がある。
ファイルの有無を確認するためにはNodejsとか使わんと行けないので、jsフルスクラッチだけだとちょい厳しいかも?
それか、↓こんな感じであらかじめ定義しとけばできる。
または、絶対読み込む、として、必要ない場合は空のファイルでも置いておくとかね。
読み込むコンポーネント一覧を定義
{
"components": [
{
"html_name": "test",
"js_name": "test"
}
]
}
4 処理速度について
SPAのフレームワーク(ReactとかVueとか)がどんな実装になってるのかわかんないですけど(そもそもNodejsだと全く別?)
iframeにロードしてからhtmlにappendしまくる、っていうゴリ押し戦法がゆえに、たぶん、おそい。かも。
比較したわけではないのでわからないですが、わたしがこの方法で実装したWebアプリはふつうにスラスラ動いてます
あくまでちゃんとしたフレームワークに負けてるかもってだけで、無理ではない。欠点ですらないかも。
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ 採用サイトはこちら ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ALHについてはこちら ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ もっとALHについて知りたい? ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓