Rails: フルスタックRailsの未来(2)Turboのビュートランジション(翻訳)
はじめに
Webは急ピッチで進化を繰り返しています。新しいエキサイティングな機能がいくつも提案されては定期的にWebブラウザに採用されています。2023年に広く利用可能になる新しいブラウザAPIのうち、最も注目を集めているのはビュートランジションです。この未来のテクノロジーを活用してTurboアプリケーションを強化する方法を見てみることにしましょう。
シリーズ構成
- Rails: フルスタックRailsの未来(1)Turbo Morph Drive
- Rails: フルスタックRailsの未来(2)Turboのビュートランジション(本記事)
本シリーズ記事では、Ruby on Railsのフルスタックアプリケーションが未来ではどのような姿になるのか、Hotwireの先にはどんなテクノロジーが使われるのか、それらがユーザー体験にどう影響するのかについて想像を巡らせます。前回の記事ではTurbo Music Driveアプリケーションを紹介し、DOMモーフィング技術でアプリケーションを強化してさらにスムーズなUXを提供しました。今回は次のレベルに進み、滑らかなアニメーション効果を追加します。
以下のアプリを実際に動かして、動作を確認してください🎧。
最初は、ビュートランジションAPIについて簡単に説明します。
🔗 ビュートランジションとは何か
ビュートランジション(View Transitions)は、新しいブラウザAPIの一種です(まだ実験的機能と銘打たれてはいますが)。さて、そもそもビュートランジションとは一体何でしょうか?
ビュートランジションとは、ドキュメントをあるステート(状態)から別のステートに遷移させる操作のことです。たとえば、Webのあるページから別のページへのナビゲーションは遷移(transition)ですが、ページの一部を更新する(例: モーダルウィンドウやサイドバーパネルのコンテンツを更新する)ことも、一種の遷移と見なせます。
Webページに生命を吹き込んで(つまり文字通り"animate"させて)遷移を実現するツールは既に存在しています。CSSアニメーションにCSSトランジション、最近導入されたWebアニメーションAPI、そして膨大なJavaScriptベースのソリューションなど、さまざまなツールがあり、どのツールもページ上のDOMツリー要素をアニメーション化するときに利用できます。だとすると、新しいAPIが必要な理由とは何でしょうか?
ビュートランジションは、そうした従来の技術とは異なるレベルで動作します。実際のHTML要素をアニメーション化するのではなく、ページの古いステートと新しいステートをそれぞれスクリーンショットでキャプチャする形でアニメーション化します。この方法のメリットは、DOMツリー自体を変更せずに両方のステートにアクセスできることです。
ビュートランジションのもうひとつのメリットは、SPA(Single-Page Apps)に限定されないことです。つまり、通常のマルチページアプリでも遷移をアニメーション化できるのです。この機能は現在Chromeブラウザでのみサポートされていますが、デフォルトでは無効になっています(chrome://flags#view-transition-on-navigation
で有効にできます)。
トランジションのスクリーンショットは、疑似要素(::view-transition
、::view-transition-new
、::view-transition-old
など)の形でDOMツリーに追加されます(ここでは詳しく説明いたしませんが、MDNドキュメントに詳しい情報があります)。
ここで大事なのは、これらの疑似要素のアニメーションをCSSで制御できるようになることです。この後すぐに実例をお見せします。
DOMツリー内にあるビュートランジション疑似要素
🔗 Turbo Driveとビュートランジションの出会い
理論はここまでにして実践に移ります。Turboドリブンのページ遷移をビュートランジションで強化する方法を見ていくことにしましょう。
ブラウザが提供する実際のAPIはごく最小限で、document.startViewTransition
というたった1個の関数です。この関数が引数として受け取るのは、ページ更新を実行するコールバック関数だけであり、それだけで良いのです。これを考慮すれば、Turboのレンダリングプロセスに小さなカスタムロジックを注入すれば済むことになります。そのために、前回の記事でモーフィングを導入するのに使ったのと同じturbo:before-render
イベントリスナーを利用できます。
document.addEventListener("turbo:before-render", (event) => {
event.detail.render = async (prevEl, newEl) => {
await new Promise((resolve) => setTimeout(() => resolve(), 0));
morphRender(prevEl, newEl);
};
if (document.startViewTransition) {
// レンダリングがstartViewTransition呼び出しの境界で
// 確実に実行されるようにするため、レンダリングを同期的にすること
event.detail.render = (prevEl, newEl) => {
morphRender(prevEl, newEl);
};
event.preventDefault();
document.startViewTransition(() => {
event.detail.resume();
});
}
});
document.addEventListener("turbo:load", () => {
if (document.head.querySelector('meta[name="view-transition"]'))
Turbo.cache.exemptPageFromCache();
});
- 最初に、このAPIが利用可能かどうかをチェックします。
- 次に、
event.preventDefault()
を呼び出してデフォルトのTurboレンダリングを一時停止します。 - 最後に、
document.startViewTransition
を呼び出して、event.detail.resume()
呼び出しをラップします(Turboでは、こうすることでレンダリングを再開できます)。
また、turbo:load
リスナーも追加しています。
このリスナーは、meta[name="view-transition"]
タグが含まれている場合にそのページをTurboキャッシュから除外するためのものです。このmeta[name="view-transition"]
というメタタグは、マルチページのビュートランジションAPIに準拠するためのもので、そのページをアニメーション化するかどうかはこれによって決定されます。
ここでキャッシュをオフにする理由がおわかりでしょうか?Turboがページ遷移を行うときは、最初にコンテンツを対象ページのキャッシュバージョンで置き換え(存在する場合)、対応するHTTPリクエストが完了した場合にのみ新しいステートをレンダリングします。つまり、startViewTransition
ではDOM更新が2つ発生してアニメーションが中断する可能性があるのです。
それでは、startViewTransition
呼び出しにページ更新をシンプルにラップすると、何かが変わるかどうかを見てみましょう。
完全なページ遷移、およびChrome DevToolsでアニメーションを制御する様子
ご覧の通り、ページ遷移がアニメーション化されました!ビュートランジションは、デフォルトでは、ページの古いステートをフェードアウトして、新しいステートをフェードインします。この振る舞いをデフォルトにしたのはうまい感じですね。
上の動画では、Chrome DevToolsでアニメーションのスピードはもちろん、アニメーションの一時停止まで行えることもわかります。これはビュートランジションで実験するときにとても便利です。
私たちのアプリに追加したこの機能は、今後Turbo 8の一部となる予定です(#935は少し前にマージ済み)。しかしビュートランジションの使いみちはこれだけではありません。ページの特定部分だけをアニメーション化することも可能なのです。その方法と、HTMLドリブンアプリケーションならではの課題について見ていきましょう。
🔗 turbo-view-transitionsライブラリでページフラグメントをアニメーション化する
ビュートランジションを使えば、view-transition-name
スタイルを定義することでページ内のさまざまな部品を個別にアニメーション化できます。この値は一意のidでなければなりません。このidは、ブラウザがそのページフラグメントの古いステートと新しいステートを照合するのに使われます。
たとえば、homeページの<h2>
要素とartistページの対応する要素にそれぞれstyle="view-transition-name: title
を追加してみると、タイトルが以下のようにアニメーション化されるのがわかります。
タイトルの遷移がアニメーション化される様子
少々奇妙な振る舞いに見えますが、何が行われているかはわかりますね。
ここで、ページ上でアルバムのジャケット(cover
)のアニメーションを実行する方法を考えてみましょう(ページ上にアルバムジャケットが複数あることが前提)。view-transition-name
は一意でなければならないので、同じview-transition-name
を使うわけにはいきません。そこで、アルバムジャケットごとに一意の名前を生成することにしましょう。
<div style="view-transition-name:<%= dom_id(album, :cover) %>">
<!-- ... -->
</div>
当然ですが、これでちゃんと動きます。
アルバムジャケットの基本的なアニメーション
ただし、この方法には1つ重要な制約があります。トランジションの名前が事前にわからないので、CSSではアニメーションのカスタムロジックを定義できないのです。
そこで、この制約を克服するためにこんなアイデアを思いつきました。カスタム属性を使ってトランジション名を定義しておき、古いステートと新しいステートの両方にそれが存在する場合にのみview-transition-name
をアタッチし、遷移が完了したら、すべての要素を無効にすればいいのです(つまりview-transition-name
を削除する)。これがきっかけで、turbo-view-transitionsというライブラリが誕生しました。
turbo-view-transitions
を使えば、アニメーションすべき要素をdata-turbo-transition
属性で個別に宣言できるようになります。属性の値は、トランジション名(カスタマイズが必要な場合)か、空の値(この場合トランジション名は要素のidから推論されます)かのどちらかです。このライブラリをTurboに統合するには、以下のようにレンダリングロジックを更新しておく必要があります。
import {
shouldPerformTransition,
performTransition,
} from "turbo-view-transitions";
document.addEventListener("turbo:before-render", (event) => {
// ... デフォルトのレンダリングセットアップ
if (shouldPerformTransition()) {
// ... トランジション用のレンダリングセットアップ
event.preventDefault();
performTransition(document.body, event.detail.newBody, async () => {
await event.detail.resume();
});
}
});
この変更は最小限のものです。
ここではライブラリのshouldPerformTransition
関数とperformTransition
関数を使っています。前者は、このAPIが利用可能かどうか、view-transition
が存在するかどうかををチェックしています。後者は、トランジションすべき要素を特定して背後でstartViewTransition
を呼び出します。
これを実際に動かしてみましょう。以下のようにアルバムジャケットにdata-turbo-transition
を追加します。
<div id="<%= dom_id(album, :cover) %>" data-turbo-transition="cover">
<!-- ... -->
</div>
このとき、id
属性も追加する必要がある点にご注意ください。これによって、アニメーション化すべき要素を計算するときに様々なカバーを区別できるようになります。
アルバムジャケットをアニメーションするカスタムCSSも定義します。
@keyframes shake {
0% {
transform: translateX(0);
}
30% {
transform: translateX(-10px);
}
60% {
transform: translateX(10px);
}
90% {
transform: translateX(-10px);
}
100% {
transform: translateX(0);
}
}
::view-transition-new(cover) {
animation: 300ms ease-in 0ms both shake;
}
それでは、アルバムジャケットがぶるっと身震いするところを見てみましょう。
アルバムジャケットが一瞬震えるアニメーション
やりました!Turboアプリケーションにdata-*
属性をたった1つ導入しただけでオブジェクトが遷移するようになったのは大勝利です。
さて、ここまではTurbo Driveについてのみ説明してきましたが、Turbo FramesやTurbo Streamsでもページを部分更新できるのでしょうか?ビュートランジションを駆使してアニメーション化もできるのでしょうか?
はい、できますとも。
🔗 Turbo Streamsとビュートランジションの出会い
Turbo Music Driveアプリケーションで私が特にアニメーション化したい操作のひとつは、プレーヤー自体です。ユーザーがトラックを選択したら、常にプレーヤーのHTMLコンテンツをTurbo Streams経由のHTMLで置き換える形で更新します。この更新をアニメーションできたらいいですよね。
turbo:before-render
イベントやturbo:before-frame-render
イベントと同様に、turbo:before-stream-render
というイベントも用意されているので、これを用いて以下のようにレンダリングをトランジションで強化できます。
document.addEventListener("turbo:before-stream-render", (event) => {
if (shouldPerformTransition()) {
const fallbackToDefaultActions = event.detail.render;
event.detail.render = (streamEl) => {
if (streamEl.action == "update" || streamEl.action == "replace") {
const [target] = streamEl.targetElements;
if (target) {
return performTransition(
target,
streamEl.templateElement.content,
async () => {
await fallbackToDefaultActions(streamEl);
},
{ transitionAttr: "data-turbo-stream-transition" }
);
}
}
return fallbackToDefaultActions(streamEl);
};
}
});
上のコードは、Turbo Driveのときよりも少々複雑に見えます。ここでは、アクションの種別も考慮する必要がありますし(すべてのアクションがトランジション可能とは限らないので)、レンダリングのロジックも異なります(event.detail.resume
はありません)が、デフォルトのレンダリングロジックをperformTransition
にラップするという考え方は同じです。
ただし、ひとつ重要な違いがあります。トランジションすべき要素を探索するための属性としてdata-turbo-transition
ではなくdata-turbo-stream-transition
を使う点です。data-turbo-stream-transition
を使う理由は、通常のナビゲーション中にTurbo Stream経由で更新される要素でトランジションを有効にしたくないからです。
後は、プレーヤーのコンテナにdata-turbo-stream-transition="player"
を追加して、対応するCSSアニメーションを定義すれば、以下のようにプレーヤーの更新がアニメーション化されます。
プレーヤーの更新がアニメーション化された様子
以上でおしまいです。Turbo Framesの更新にもトランジションを追加可能かどうかについては、読者の皆さんの検討にお任せいたします。
「フルスタックRailsの未来」シリーズ記事もそろそろおしまいです。
このシリーズ記事では、最新のWeb技術を活用して、Turboドリブンアプリケーションを次のレベルに引き上げる方法を学びました。しかも、Turbo 8(でも9でもXでも)の登場を待たなくてもできるのです!
Hotwireツールには、そのための十分な柔軟性が既に備わっているので、オーブンで焼き上がったばかりのホットな技術をこうして試せます。つまり、未来は私たちの手で自ら築けるのです!
シリーズ記事1と2のソースコードは、以下のGitHubリポジトリでご覧いただけます。
Evil Martiansは、成長段階のスタートアップ企業をユニコーン企業に飛躍させるためにサポートいたします。開発ツールの構築やオープンソース製品の開発も行っています。ワープの準備が整ったお客様、ぜひフォームまでご相談をお寄せください!
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。
View Transitionsの訳語はMDNに合わせました。