BLOG

ページ遷移ライブラリswupを使って非同期遷移を実装する

Written by naganoma

INDEX

こんにちは!エンジニアのナガノマです!

今回はページ遷移ライブラリswupについてご紹介します!

swupとは

swupはJavaScriptを使用してページ間の遷移を制御するライブラリです。
従来のWebサイトの通信(MPA: Multi Page Application)をJavaScriptで制御(非同期通信)することで、シームレスなページ遷移(非同期遷移)を実現できます。

Next.jsやNuxt.jsにおけるSPA(Single Page Application)に近いWebサイトをフレームワークに依存せずに再現できるため、EVOWORXでも積極的に導入しています。

導入方法

swupの公式ドキュメントを参照しつつ、導入方法について解説していきます。

完成デモ

デモサイトはこちら

1. 準備

まずはswupをインストールします。今回はnpmを使用します。

npm install swup

2. マークアップ

swupを動作させるためには、どの部分を遷移させるかを示す「Content container」と、ページ遷移時にどれくらい待機させるかを制御する「transition styles」の2つが必要です。

まずはContent containerを実装します。
遷移させる対象に対して、#swupというid属性と、transition-**というクラス属性を追加します。(**の部分は任意の名前)

<div id="swup" class="transition-fade">
  <!-- コンテンツが入ります。 -->
</div>

次にtransition stylesを設定します。
上記のContent containerに対して、以下のCSSを記述します。

html {
 /* ページ訪問中の遷移期間 */
  &.is-changing {
    .transition-fade {
      opacity: 1;
      transition:
        opacity 0.3s ease,
        translate 0.4s ease;
    }
  }

 /* unloadされたページのスタイル */
  &.is-animating {
    .transition-fade {
      opacity: 0;
      translate: 0 5px;
    }
  }
}

なお、.is-changing.is-animatingはswup側が独自で定義しているライフサイクル管理用のクラスです。
後ほど詳しく解説します。

3. swupの初期化

マークアップの準備はできたので、あとはswupを実行するだけです。
JavaScriptで以下の処理を追加します。

import Swup from 'swup'

const swup = new Swup();

以上で完成です!
この方法以外でもページ遷移の制御が可能ですが、最小構成であればこれだけの処理でシームレスなページ遷移を再現することができます🎉

ライフサイクル

非同期通信を使用したアプリケーションにおける「古いデータの破棄〜新しいデータの生成」までの流れを、一般的にライフサイクルと呼びます。
swupでは「前のページが削除されたとき」、「次のページが表示された時」など、ページ遷移中におけるライフサイクルのフェーズを細かく検知することができます。

先ほどの「transition styles」で記述した.is-changing.is-animatingは、swup側がライフサイクルのフェーズを検知して、htmlに自動的に割り当てているクラス名となります。

https://swup.js.org/lifecycle/#lifecycle-diagram

クラス名

説明

is-changing

アニメーション開始前に追加。 アニメーション全体のプロセスが完了した後に削除。

is-animating

アニメーション開始前に追加。コンテンツが置き換えられた後で削除。
(unloadされたページのスタイルを定義するために使用)

is-leaving

アニメーション開始前に追加。コンテンツが置き換えられる直前に削除。

is-rendering

新しいページのレンダリング開始時に追加。新しいページが完全にレンダリングされた後に削除。

また、上記はcssを使用したライフサイクルの検知となりますが、JavaScript側で検知することも可能です。
JavaScriptで検知するにはswupのライフサイクルフックを使用します。

import Swup from 'swup'

const swup = new Swup();

/* ライフサイクルフックの呼び出し */
swup.hooks.on('page:view', (visit) => {
  console.log('新しいコンテンツが表示されました')
  console.log(visit.to.url) // 新しいページのURL
});

ライフサイクルフックを呼び出す際は、swup.hooks.on('フック名')の形で記述します。
上記における、page:viewはコンテンツの置換後、新しいコンテンツが表示されたタイミングを検知しています。

また、ライフサイクルフックでは、引数にvisitオブジェクトを受け取ることができます。
visitオブジェクトには遷移前/遷移後ページの情報(URL)など、遷移に関わるデータが格納されています。
格納されているデータはこちら

page:view以外にも、様々なライフサイクルフックが用意されていますので、詳細はswup公式のList of hooksをご参照ください。

非同期遷移の注意点

swupに限った話ではありませんが、非同期遷移を導入する際、MPAとは挙動が変わる箇所がいくつかあります。
代表的な落とし穴と解決方法について例を交えて解説していきます。

1. ページ遷移時にhead情報が更新されない

MPAでは、ブラウザからのWebサイトを表示するリクエストに対して、サーバー側が対象ページのHTMLをそのまま返すため、前のページと新しいページの差分に関して特段気を遣う必要がありません。

ただし、非同期遷移では、差分箇所をJavaScriptで指定して更新する形となるため、逆に言えば指定していない箇所は最初にロードしたページ情報のまま、ということになります。
つまり、<head>に記述しているmeta descriptionやogpなどの情報がページ遷移後に更新されません。

解決方法として、「ライフサイクルフックを使用して、visitオブジェクトから新しいページのhead情報を取得して置き換える」などの方法が挙げられますが、swup公式のSwupHeadPluginを使用することで、よりシンプルなコードで解決できます。

npm install @swup/head-plugin
import Swup from 'swup'
import SwupHeadPlugin from '@swup/head-plugin';

const swup = new Swup({
  plugins: [new SwupHeadPlugin()]
});

2. Google Analyticsの解析用スクリプトが機能しない

非同期遷移を導入するとページ遷移時にページビューのイベントが発生しないため、 Google Analyticsの解析用スクリプトが実行しているページビューのカウントがうまく機能しない可能性があります。

この問題についてもSwupHeadPluginの時と同様、 Google Analytics用のプラグイン(SwupGaPlugin)がswup公式で用意されています。

npm install @swup/ga-plugin
import Swup from 'swup'
import SwupGaPlugin from '@swup/ga-plugin'

const swup = new Swup({
  plugins: [
  new SwupGaPlugin()
 ]
});

また、Google Tag Manager用のプラグイン(SwupGtmPlugin)も用意されているため、導入している場合は同じく追加しておくことをオススメします。

3. ページ遷移時にJavaScriptが再実行されない

MPAではページ遷移するたびにJavascript(クライアントサイド)が再実行されますが、非同期遷移を導入している場合、Javascriptの処理が評価されるのは初回ロードのみで、ページ遷移した際に再評価されません。

例えばページAでのみ実行されるconsole.log('ページA スクロール中')、とページBでのみ実行されるconsole.log('ページB スクロール中')という処理があったとします。
Webサイトの初期ロードがページAだった場合、console.log('ページA スクロール中')が実行されますが、ページAからページBに遷移した際に、console.log('ページB スクロール中')は実行されません。

解決策として一番シンプル方法は、ライフサイクルフックでページ表示時にJavaScriptの処理を再実行することです。

const pageInit = () => {
  const pageA = document.querySelector('.page-a')
  const pageB = document.querySelector('.page-b')

  if (pageA) {
    window.addEventListener('scroll', () => {
      console.log('ページA スクロール中')
    })
  }

  if (pageB) {
    window.addEventListener('scroll', () => {
      console.log('ページB スクロール中')
    })
  }
}

// 初期実行
pageInit()

const swup = new Swup();

// ページ遷移後に再実行
swup.hooks.on('page:view', (visit) => {
  pageInit()
})

ただ、MPAではJavaScriptが再評価される際に、メモリ上に残っている処理を自動で破棄する処理(ガベージコレクション)が実行されるのですが、非同期遷移を導入している場合はこの処理が実行されず、メモリ上に処理が残ったままになる可能性があります。(メモリリーク)

上記の例のようにページAでaddEventListenerを使用してイベントリスナーを登録し、ページBに遷移した際にライフサイクルフックでJavaScriptを再評価した場合、ページAで登録されたイベントリスナーは破棄されず、ページ遷移するたびにメモリ上に処理が溜まっていくため、パフォーマンスに大きな影響を与えてしまいます。

swup公式でもこの危険性について言及されています。

Running scripts without destroying previous ones can cause memory leaks and potentially break your page.

よって、ページ遷移時に必要な処理のみを初期化、不要な処理は破棄するよう管理していく必要がありますが、処理が膨大になってくるとJavaScriptの設計も複雑になり、カバーしていくのが困難となります。

この問題の1つの解決策として、Alpine.jsなどのリアクティブフレームワークを導入するとスムーズに解決できます。
Alpine.jsについての記事はこちら

import Swup from 'swup'
import Alpine from 'alpinejs'

Alpine.data('pageA', () => ({
  init() {
    window.addEventListener('scroll', () => {
      console.log('ページA スクロール中')
    })
  },

  destroy() {
    window.removeEventListener('scroll', () => {
      console.log('ページA スクロール中')
    })
  }
}))

Alpine.data('pageB', () => ({
  init() {
    window.addEventListener('scroll', () => {
      console.log('ページB スクロール中')
    })
  },

  destroy() {
    window.removeEventListener('scroll', () => {
      console.log('ページB スクロール中')
    })
  }
}))

window.Alpine = Alpine
Alpine.start()

const swup = new Swup()

/* ライフサイクルフックを使用した再実行は不要 */

Alpine.jsでは、Alpineコンポーネント(x-dataで定義した処理)を使用してJavaScriptの処理を実行しますが、実行・破棄のタイミングについてはDOM上に該当のAlpineコンポーネントが存在するかどうかをAlpine.js側が都度検知してくれます。
よって、swupのライフサイクルフックを使用してJavaScriptの処理を再実行する必要はありません。

なお、Alpine.dataを使用する際のルールとして、初期化の処理はinit()メソッド内に、破棄の処理はdestroy()メソッド内に定義する必要があります。
詳細はAlpine.js公式ドキュメントをご参照ください。

プラグインを使用した応用例

ここまでの説明でswupのプラグインについて少し触れてきましたが、他にも様々なプラグインがあります。
全てはご紹介しきれないため、抜粋してご紹介します。

1. Fragment Plugin

デモサイトはこちら


swupではContent containerの部分が遷移の対象となりますが、Fragment Pluginを使用することで、Content container内の一部分のみを遷移対象として切り出すことができます。
上記デモでは、記事の絞り込みをFragment Pluginでコントロールすることで、ページ遷移させつつも、タブメニューのようなUIを再現しています。

実装方法

まずはプラグインをインストールします。

npm install @swup/fragment-plugin

次にプラグインを初期化します。
Fragment Pluginのオプションにrulesという配列オブジェクトを渡し、「①どのページ間(URL)にFragment Pluginを適用するかを設定するfrom/to」と、②「どの部分をFragment Pluginの遷移対象にするかを設定するcontainers」に値を渡します。

なお、from/toに渡すURLは正規表現を使用して部分一致などの適用も可能です。
正規表現はpath-to-regexpのルールに則って定義する必要があります。

import Swup from 'swup';
import SwupFragmentPlugin from '@swup/fragment-plugin';

const swup = new Swup({
  plugins: [
    new SwupFragmentPlugin({
      rules: [
        {
          from: [
            '/',
            '/category_:id([^/]+)/',
          ],
          to: [
            '/',
            '/category_:id([^/]+)/',
          ],
          containers: ['#blog-panel'],
        },
      ],
    }),
  ],
});

あとは、Fragment Pluginの遷移対象に対して、transition stylesを適用します。

#blog-panel {
  &.is-changing {
    transition-duration: 0.3s;

    .list {
      opacity: 1;
      transition: opacity 0.3s ease, translate 0.4s ease;
    }
  }

  &.is-animating {
    .list {
      opacity: 0;
      translate: 0 5px;
    }
  }
}

なお、今回はタブメニューのようなUIを再現しましたが、他にもモーダルウィンドウやカルーセルといった、ページ内で動的にコンテンツを切り替える系のUIは、基本的にFragment Pluginで制御が可能です。
コンテンツが表示された状態をページとして持たせることで、ユーザーにとってもアクセスしやすく、単一ページにコンテンツを詰め込まずに済むという点で、パフォーマンス面の改善も期待できます。

2. JS Plugin

デモサイトはこちら

ここまでのデモではCSSを使用して遷移アニメーションをコントロールしていましたが、JavaScriptを使用してコントロールすることも可能です。
JavaScriptで制御するには、JS Pluginを使用します。

実装方法

まずはプラグインをインストールします。

npm install @swup/js-plugin

次にプラグインを初期化します。
JS Pluginのオプションにanimationsという配列オブジェクトを渡し、「①どのページ間(URL)にJS Pluginを適用するかを設定するfrom/to」と「ページ削除時のアニメーション、ページ挿入時のアニメーションを設定するin/out」に処理を記述します。

なお、from/toに渡すURLはpath-to-regexpdata-swup-animation属性を使用した要素の指定など、様々な形式で記述できます。
詳細はJS PluginのChoosing the animationをご参照ください。

import Swup from 'swup'
import SwupJsPlugin from '@swup/js-plugin'
import gsap from 'gsap'

const swup = new Swup({
  plugins: [
    new SwupJsPlugin({
      animations: [
        {
          from: '(.*)',
          to: '(.*)',
          in: async () => {
            const timeline = gsap.timeline()
            await timeline
              .fromTo(
                '[data-transition-fish]',
                {
                  x: '-100vw',
                },
                {
                  x: '100vw',
                  duration: 0.8,
                  ease: 'expo.out',
                  stagger: {
                    each: 0.03,
                    from: 'random',
                  },
                },
              )
              .fromTo(
                '#swup',
                {
                  opacity: 0,
                },
                {
                  opacity: 1,
                  duration: 0.2,
                },
                '<+=0.45',
              )
          },
        },
      ],
    }),
  ],
})

in/outのアニメーションの処理は通常のJavaScriptの構文で自由に記述できるため、今回はGSAPを使用してカツオの群れを泳がせてみました。
JS Pluginを使用することで、より高度な遷移アニメーションのコントロールが可能となります。

おわりに

ページ遷移ライブラリswupについてご紹介しました。
非同期遷移の導入は初学者の方にとってはハードルが高いかもしれませんが、リッチかつユーザビリティの高いアプリケーションライクなWebサイトの実装が可能となりますので、気になった方はぜひ試してみて下さい!

ご拝読ありがとうございました!

参考文献

swup

Alpine.js

path-to-regexp