☀️

<script> を含む Astro 製サイトで View Transitions を使う

2023/09/14に公開

まえがき

Astro v3 で View Transitions が正式版になりました!

https://docs.astro.build/ja/guides/view-transitions/

これは <script> を含む Astro 製サイトで View Transitions を使ってみてハマったところを紹介する記事です。

環境

  • Astro: v3.0.7

実装

  • ページにビュートランジションを追加する
  • サイト全体でのビュートランジション(SPAモード)を有効にする

のどちらもできますが、私はサイト全体のほうを試したいと思います。

サイト全体のビュートランジションを有効にする

<ViewTransitions /> をインポートし、共有のレイアウトコンポーネントに追加しました。

src/layout/BaseLayout.astro
  ---
+ import { ViewTransitions } from 'astro:transitions';
  // 中略
  ---

  <!doctype html>
  <html lang="ja" prefix="og: https://ogp.me/ns#">
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <!-- 中略  -->
+     <ViewTransitions />
    </head>
  <!-- 以下略  -->

これだけで OK のはずです! 早速動かしてみます。(gif の画質が粗くてすみません)

😖😖😖

お分かりいただけたでしょうか……。

ダークモードの適用がページ遷移時に解除されてしまっています。

ページ遷移時に毎回実行してほしい処理の扱い

ダークモードは <head> 内に <script is:inline> で書いているスクリプトで localStorage の値を読み取って制御しています。

src/layout/BaseLayout.astro
<script is:inline>
  function setMode() {
    if (localStorage.getItem("dark")) {
      document.documentElement.classList.add("dark");
    }
  };
  setMode();
</script>

このスクリプトがページ遷移時に動いていないようです。

この事象の解決のために astro:after-swap を使います。

astro:after-swap
旧ページが新ページに置き換わった直後に発生するイベントです。documentでこのイベントをリッスンできます。
このイベントは、新しいページに引き継ぐ必要があるDOM上の状態を復元するのに役立ちます。
たとえばダークモードのサポートを実装している場合、このイベントを使用してページロード間で状態を復元できます。

先ほどの <script> の最終行で astro:after-swap イベントでもダークモード切り替えが動くようにします。

src/layout/BaseLayout.astro
   setMode();
+  document.addEventListener('astro:after-swap', setMode);

😊😊😊

ページ遷移後に addEventListner が消える事象への対応

良さそうなので続けて動作確認です。
ページ遷移後にダークモードを切り替えてみます。

🥺🥺🥺

ページ遷移後にダークモードの切替ボタンを連打していますが、切り替わりません……。

ダークモードの切替ボタンは Astro コンポーネントで実装していて、<script> 内でボタンにクリックイベントを追加しています。

src/components/Header/ModeButton.astro
<button
  type="button"
  id="ModeButton--dark"
>
  <!---->
</button>
<button
  type="button"
  id="ModeButton--light"
>
  <!---->
</button>
<script>
  function switchMode(changeDark: boolean) {
    // 略
  }
  const light = document.getElementById("ModeButton--light");
  light?.addEventListener("click", () => {
    switchMode(false);
  });
  const dark = document.getElementById("ModeButton--dark");	
  dark?.addEventListener("click", () => {
    switchMode(true);
  });
</script>

そして Astro コンポーネント内に書いた <script>スクリプトのバンドルに記載の通り type="module" として読み込まれます。

処理されたスクリプトは、type="module"としてページの <head> に挿入されます。

 

ページ遷移時のスクリプトの動作はページナビゲーション中のスクリプトの動作で説明されています。

モジュールスクリプトは、ブラウザがロード済みのモジュールを追跡しているため、常に1回だけ実行されます。これらのスクリプトでは再実行の心配は必要ありません。

つまりページ遷移後に HTML 要素の <button> は入れ替わるのですが、addEventLisener が書いてあるモジュールスクリプトは再実行されません。
そのため新しい <button> にクリックイベントが追加されることはなく、動かないボタンになってしまいます。

これに対処するには transition:persist を使います。状態を保持するで説明されています。

transition:persist ディレクティブを使用すると、ページ間のナビゲーションでコンポーネントとHTML要素を(置き換えるのではなく)保持できます。

 

私はここでちょっとハマったのですが、たとえば ModeButton.astro というファイルに <button> 及びクリックイベント追加用の <script> が書いてあって、Header.astro というファイル内で

src/components/Header/Header.astro
---
import ModeButton from "./ModeButton.astro";
---

<div class="mx-4 mb-1 flex items-center gap-5 pt-1">
  <!-- 略 -->
  <ModeButton />
</div>

のように読み込んでいたとします。

このとき transition:persistHeader.astro 内の <ModeButton /> に付けても駄目です。

src/components/Header/Header.astro
  ---
  import ModeButton from "./ModeButton.astro";
  ---

  <div class="mx-4 mb-1 flex items-center gap-5 pt-1">
    <!-- 略 -->
-   <ModeButton />
+   <!-- このように書き換えても意味ない -->
+   <ModeButton transition:persist />
  </div>

今回の場合、transition:persist をつけるべきなのは addEventLister でクリックイベントを追加した対象そのもの。

つまり ModeButton.astro 内の <button> です。

src/components/Header/ModeButton.astro
  <button
    type="button"
    id="ModeButton--dark"
+   transition:persist
  >
    <!---->
  </button>
  <button
    type="button"
    id="ModeButton--light"
+   transition:persist
  >
    <!---->
  </button>
  <script>
      // 略
  </script>

これでクリックイベントがついた状態の HTML 要素 <button> をページ遷移後に引き継ぐことができます。

ということでダークモードの切り替えをつかさどるボタンである ModeButton.astro 内の <button>transition:persist をつけました。

🥰🥰🥰🥰🥰🥰

おわりに

<ViewTransitions /> を読み込むだけで簡単! とはいきませんでした。

また今回は言及しませんでしたが、ドキュメントには

グローバルな状態を設定しているコードがある場合は、そのスクリプトが複数回実行される可能性があることを考慮する必要があります。<script> タグでグローバルな状態をチェックし、可能な限り条件付きでコードを実行してください。

という注意書きもあるので、スクリプトが書いてある部分はよく確認する必要がありそうです。

参考になれば幸いです!

chot Inc. tech blog

Discussion