🕌

純粋な静的ファイルのサイトで共通レイアウトを実現する

2024/09/24に公開

モチベーション

情報の表示が中心のシンプルなサイトを作っていると、フロントエンドのフレームワークやビルドプロセスが過剰に感じて、純粋な静的HTMLファイルだけでサイトを構築したい衝動にかられます。しかしその場合に難しくなるが共通レイアウトの適用です。

フレームワークのように共通レイアウトのHTMLを定義したひとつのファイルを各ページに適用させることは、たんに表示させるだけなら難しくはないが、遅延やちらつきなく表示させるとなるとちょっと難しい。

ということで外部ファイル化した共通レイアウトの取得して遅延・ちらつきなく表示するライブラリを作ってみました。

デモ https://ksk1015.github.io/render-layout-js/
リポジトリ https://github.com/ksk1015/render-layout-js

使い方

レイアウトのhtmlファイルを用意して、適用させたいページのhtmlのheadでrenderLayout.jsを読み込むscriptタグのdata-path属性にレイアウトのhtmlファイルのパスを記述するだけです。

layout.html
<header>
  <h1>RenderLayoutJs</h1>
</header>
<main>
  <!-- slotタグがbody内のコンテンツに置き換わる -->
  <slot></slot>
</main>
<footer>
  <small>&copy; RenderLayoutJs</small>
</footer>
page.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>RenderLayoutJs</title>
    <script src="./renderLayout.js" data-path="./layout.html"></script>
  </head>
  <body>
    <p>Hello, this is content.</p>
  </body>
</html>

するとbody内にlayout.htmlが描画されて、slotタグのところには元々body内に記述されていたhtmlが表示されます。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>RenderLayoutJs</title>
    <script src="./renderLayout.js" data-path="./_layout.html"></script>
  </head>
  <body>
    <header>
      <h1>RenderLayoutJs</h1>
    </header>
    <main>
      <p>Hello, this is content.</p>
    </main>
    <footer>
      <small>&copy; RenderLayoutJs</small>
    </footer>
  </body>
</html>

name付きのslotとtemplate

たんにbody内のhtmlをslotタグのところに表示させるだけでなく、name属性を付けたslotとtemplateタグを使ったコンテンツ表示もできます。

layout.html
<header>
  <h1>RenderLayoutJs</h1>
</header>
<main>
  <!-- temaplte#main内に置き換わる -->
  <slot name="main"></slot>
</main>
<aside>
  <!-- temaplte#aside内に置き換わる -->
  <slot name="aside"></slot>
</aside>
<footer>
  <small>&copy; RenderLayoutJs</small>
</footer>
page.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>RenderLayoutJs</title>
    <script src="./renderLayout.js" data-path="./layout.html"></script>
  </head>
  <body>
    <template id="main">
      <p>Hello, this is main.</p>
    </template>
    <template id="aside">
      <p>Hello, this is aside.</p>
    </template>
  </body>
</html>

page.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>RenderLayoutJs</title>
    <script src="./renderLayout.js" data-path="./layout.html"></script>
  </head>
  <body>
    <header>
      <h1>RenderLayoutJs</h1>
    </header>
    <main>
      <p>Hello, this is main.</p>
    </main>
    <aside>
      <p>Hello, this is aside.</p>
    </aside>
    <footer>
      <small>&copy; RenderLayoutJs</small>
    </footer>
  </body>
</html>

ちらつき無く描画したい

レイアウトの描画処理をちらつきなくおこなう一番シンプルな方法はbody終了タグ直前に実行することですが、body内にはコンテンツデータのみにしたいお気持ちがあったので、headで読み込ませても問題なく描画されるようにしたい。

ブラウザの初期レンダリング時のイベントの流れは以下のようになります。

  1. document readystatechange ('loading' -> 'interactive')
  2. document readystatechange ('interactive' -> 'complete')
  3. document DOMContentLoaded
  4. window load

SafariやFirefoxでは単にDOMContentLoadedのタイミングでもちらつきなく描画してくれるが、ChromeのみDOMContentLoadedの前にレンダリングが走ってちらつきが発生してしまう。DOMContentLoadedの前に発火するreadystatechangeのタイミングでも変わらずにちらつきが発生してしまう。

いろいろ試してみた結果、headで同期的に実行したrequestAnimationFrameだと初期レンダリングの前にレイアウトを描画してくれました!

  // 描画を呼ぶところの処理

  const onReadyStateChange = () => {
    cancelAnimationFrame(frameId)
    render()
  }
  document.addEventListener('readystatechange', onReadyStateChange, {
    once: true,
  })

  const frameId = requestAnimationFrame(() => {
    if (hasReady()) {
      document.removeEventListener('readystatechange', onReadyStateChange)
      render()
    }
  })

近年はブラウザの差異が小さくなってますが、こういう細かい仕事をしようとするとブラウザの差異がでてきておもろいです。

Discussion