見出し画像

iframeを使って疑似SPA!!

iframeはとても便利

objectやembedも便利ですが、iframeはとても便利。
ソース忘れましたが、使い分けはこう

iframe: htmlソースを読み込む
embed: pdfなどプラグインが必要なやつを読み込む
object: その他を読み込む、なので最終手段

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に読み込む。

1、iframeタグを作成
2、srcを指定
3、onloadで、html読み込み後の処理を指定→scripeタグを作成してjsを読み込む (必要ならcssも)
4、index.htmlのなかから指定したidの要素配下に、読み込む外部htmlのbodyを追加

ではきれいに書いてみよう。

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について知りたい? ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓