iframe 要素を使ってヘッダー、フッターを共通管理する

はじめに

静的なウェブサイトを構築して配信するとき、ヘッダーやフッターなどのサイト共通パーツの管理が問題になる。

レガシーな手法だと Apache の Server Side Include (SSI) をもちいたり、PHP を導入したりして解決する。このような手法は手軽で要件が整えばとても簡単に導入できるのが利点だ。一方で配信環境が限られる問題がある。

ファイルインクルードを Gulp タスクに任せたり、静的サイトジェネレータ―を使うやりかたも一通り試してきた。状況が許すならば静的サイトジェネレータ―を使うのが一番いいだろう。実際のところ Eleventy の使い心地はかなりいい。

しかしそのための環境構築や技術的キャッチアップは手軽とはなかなかいいづらい。不慣れな人にとってはひとつのハードルになる。

そこで、 iframe を共通パーツのインクルードがわりに使うのはどうなのか? と思って試してみた。HTML 初学者ならば誰でも当たり前に思いつき、まともなウェブ開発者ならアホかと一蹴する手法だが、実際のところ本当に価値のない手法なのだろうか?

結論としては悪くはないかもしれない。うまく実装すれば、サーバーサイドの助けを得ることなくフロントエンドだけでパーツのインクルードができる。JavaScript が無効な環境でも動作する。

iframe を共通パーツの管理に使うデモ

iframe 要素を使ってヘッダー、フッターを共通管理する方法

動作概要

ファイルを分割する

最初に次のような HTML があったとする。header 要素が共通管理したいパーツだ。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Iframe Common Parts</title>
    <link rel="stylesheet" href="/test.css" />
  </head>
  <body>
    <header class="Header">
      <!-- 中略 -->
    </header>
    <main>
      <h1>Hello, World!</h1>
      <!-- 中略 -->
    </main>
  </body>
</html>

header 要素を別ファイルに分割する。DOCTYPE 宣言から全部書いているのは、iframe として読み込まれるため独立した HTML ファイルの体を成している必要があるから。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>Header</title>
    <link rel="stylesheet" href="/test.css" />
  </head>
  <body>
    <header class="Header">
      <!-- 中略 -->
    </header>
  </body>
</html>

読み込み元は iframe 要素に置き換える。id 属性をつけているのは JS で要素を取得するため。role 属性はつけておくと良いっぽかった(スクリーンリーダーで「フレームコンテンツ」と読み上げなくなった)

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Iframe Common Parts</title>
    <link rel="stylesheet" href="/test.css" />
  </head>
  <body>
    <iframe
      id="HeaderIframe"
      class="HeaderIframe"
      role="presentation"
      src="/common/header.html"
    ></iframe>
    <main>
      <h1>Hello, World!</h1>
      <!-- 中略 -->
    </main>
  </body>
</html>

CSS は雑だけどこんな感じ。

.HeaderIframe {
  display: block;
  width: 100%;
  height: 60px; /* 固定値で入れとく必要あり */
  border: 0;
}

JavaScript で iframe 要素と iframe の中身を置き換える

JS で iframe の内容を読み取り、読み込み元ページに挿入する。この操作をすることで、読み込み元のページには「iframe がもともとなかった」かのような状況をつくれる。

そのための JS 関数は以下。関数を呼び出したときの iframe の状態によらず、関数を呼び出せばうまく動くようにつくってある。

function inlineIframe(iframe) {
  var onLoad = function () {
    iframe.insertAdjacentHTML('afterend', iframe.contentDocument.body.innerHTML)
    iframe.parentNode.removeChild(iframe)
    iframe.onload = null
  }
  if (
    iframe.contentWindow.location.href === 'about:blank' ||
    iframe.contentDocument.readyState === 'loading'
  ) {
    iframe.onload = onLoad
  } else {
    onLoad()
  }
}

呼び出し方はこう。inlineIframe の引数には iframe の Element を渡す。

inlineIframe(document.querySelector('#HeaderIframe'))

完成品

iframe を共通パーツの管理に使うデモ

実装時のポイント

手法の考察

この手法のいちばん気になる点は、表示時のディレイと、iframe の高さを固定値で入れておかなくてはいけない点だ。特に表示時のディレイはユーザー体験におおきくかかわる部分なので慎重にならざるを得ない。

とはいえ、冒頭にあげた課題を解決する手段としては一定の価値がみとめられると思う。なにせほかの手段のハードルがどれも高いのだ。いや、今回紹介の手法もお手軽とはいいがたいけど……。

HTML から別の HTML を読み込む手法は歴史的にも不遇な一途をたどってきた。Frameset DTD の廃止にはじまり、HTML5 策定時に導入されかけた iframe の seamless 属性もついぞ導入されず、Web Components の一部分だった HTML Imports もなくなってしまった。ブラウザの実装上いろいろ困難はあるんだとおもうが、もうちょっと何とかならないのかな、と思う。

そのほかの懸念事項