アーキテクチャ

06. ブラウザーの遷移

遷移先の読み込みから画面とブラウザー履歴の更新まで、ページ遷移の実装を読み解きます。

Reactのcommit、ブラウザー履歴のcommit、Flightの完了は別の出来事です。 遅延データのストリーミング中にもルートを表示できるため、遷移は表示中のツリーと未完了のリソースを両方追跡します。

hydrationは HTMLに埋め込んだFlight から始まります。 後続の応答も同じ サーバー生成のルートツリーモデル を運びますが、ドキュメントを新しく作らず既存のReactルートを更新します。

初期ツリーを確立し、クライアント遷移を選ぶ

client/application.ts の activateBrowser は初期Flightペイロードを読み、ReactDOMRenderer.hydrate を呼びます。 hydrationはReactへ formState を渡し、layout effectがツリーとstate更新関数で BrowserRenderer を初期化するまで待ちます。 その後でrefresh、Server Functionのcallback、対応していればクライアントルーターを登録します。

client/browser-capabilities.ts は、window.navigation と window.NavigationPrecommitController の両方がある場合だけクライアントルーティングを選びます。 それ以外では、hydrate済みのページもドキュメント遷移を使います。 両APIがあっても、一部のイベントはブラウザーに任せます。

packages/core/src/client/navigation-routing.ts の抜粋
const ReactTransitionNavigationInfo = "react-transition";

export const NativeDocumentNavigationInfo = "effront-native-document";

export const isRoutedNavigation = (event: NavigateEvent) =>
  event.canIntercept &&
  !event.hashChange &&
  event.downloadRequest === null &&
  event.formData === null &&
  event.info !== ReactTransitionNavigationInfo &&
  event.info !== NativeDocumentNavigationInfo &&
  event.navigationType !== "reload";

hashのみの変更、download、フォーム、reload、二つのマーカー付き遷移はinterceptしません。 ドキュメント用のマーカーは、Effrontのフォールバック読み込みの再interceptを防ぎます。

browserMain は Effect.scoped と Effect.never でサービスと購読を維持します。 初期Flightやhydration開始時の失敗は、ブラウザーの失敗画面を表示します。 React描画中のエラーはレンダラーのError Boundaryが扱います。

ルートを取得するか、ブラウザーに処理を戻す

client/route-loader.ts は履歴遷移で、移動先entryのIDを使ってキャッシュ済みツリーを再利用します。 それ以外の遷移とキャッシュミスでは、FlightClient.load が Accept: text/x-component を付けてGETします。 応答ごとのScopeは、初期ペイロードのデコード後も存続できます。

  • ドキュメントへのフォールバック:非2xxまたはFlight以外の応答は Document になります。 ルーターは応答を解放し、要求した移動先をドキュメントとして読み込みます。
  • 読み込みエラー:通信失敗、解決済みURLの欠落や不正、デコード失敗は FlightLoadError になり、理由はそれぞれ RequestFailed、UnexpectedResponse、DecodeFailed です。
  • Flight:結果にはペイロード、completed、release、resolvedUrl が含まれます。 ルートツリーのデコード完了は、ストリームの完了を意味しません。

公開前に client/client-router.ts が解決済みの移動先を検査します。 originが異なればドキュメント遷移が必要です。 同じoriginでもURLが変わる場合、履歴遷移かprecommit controllerがない状況ではドキュメントを置き換えます。 それ以外では、Reactのcommit後にcontrollerでredirectできます。 要求したhashを保つのは、解決済みURLにhashがなく、origin・path・queryが一致するときだけです。

描画の予約とcommitを区別する

非同期のTransition Actionが BrowserEffectRunner を通してルートを読み込みます。 その後、内側のTransitionがツリーを公開しますが、公開はcommitではありません。

packages/core/src/client/client-router.ts の抜粋
          const rendererNavigation = yield* Effect.sync(() => {
            let navigation!: BrowserRendererNavigation;
            startTransition(() => {
              const fromIndex =
                navigationApi.getTransition()?.from.index ??
                navigationApi.getCurrentEntry()?.index ??
                null;
              for (const type of getNavigationTransitionTypes(event, fromIndex)) {
                addTransitionType(type);
              }
              for (const type of linkTransitionTypes) {
                addTransitionType(type);
              }
              navigation = browserRenderer.navigate(command.resource.routeTree);
            });
            return navigation;
          });

browserRenderer.navigate はstate更新を予約し、三つのライフサイクル操作を返します。

  • committed は、client/react-dom-renderer.tsx のlayout effectが browserRenderer.commit(render) を呼ぶと解決します。
  • retired は、置き換えのcommitでツリーを保持する必要がなくなると解決します。
  • discard は、保留中の描画に対して現在のツリーの復元を依頼し、退役を待ちます。

キャンセル可能なイベントでは、event.intercept が precommitHandler を使います。 Reactのcommitを待ち、可能なredirectを適用し、履歴のcommitを記録するcallbackを addHandler に登録します。 キャンセル不能な履歴遷移は通常のhandlerを使うため、同じように履歴を遅らせられません。 この経路で準備が失敗するとドキュメントをreloadします。

Transition typeは遷移の種類と、判別できる場合には方向を表します。 リンクによるpushとreplaceは、重複と予約済みtypeを除いた data-effront-transition-types の値を加えられます。 このラベルはcommitや寿命の規則を変えません。 アプリケーションでの利用方法は クライアントナビゲーションとページ遷移 を参照してください。

処理中の候補は、表示中の遷移とは別に Loading → Publishing → Rendering を進みます。 generationのsymbolとAbortControllerにより、新しい遷移は表示中のツリーをすぐ解放せずに、保留中の処理をキャンセルできます。

古い読み込みは公開せず解放します。 予約済みの描画はdiscardし、退役後に解放します。BrowserRenderer は、表示中のツリーと、復元要求を含む保留中の公開が参照するツリーを保持します。 そのため、遷移のabortは描画の退役と同じではありません。 未公開または退役済みのcommitや不正なライフサイクル遷移は TypeError になります。

新たに読み込んで表示したルートのキャッシュには、履歴のcommitとFlightの正常完了の両方が必要です。NavigationEntryState と NavigationFlightState が独立に追跡します。

  • 履歴が先:Flightを待ち、キャッシュして解放します。
  • Flightが先:応答を解放し、履歴entryが分かるまでキャッシュ用callbackを保持します。
  • ストリームの失敗:キャッシュせずに解放します。

新しいgenerationが始まっていても、描画の退役はそのリソースを解放します。 キャッシュが保持するのはツリーであり、開いた応答ストリームではありません。RouteLoader は破棄された履歴entryを削除し、refresh時にキャッシュのMapを置き換えて、遅れたcallbackによる新キャッシュへの書き込みを防ぎます。Server Functionの応答 も、同じレンダラーのライフサイクルで現在のページを更新します。