Files
Native-Router-Vue/docs/architecture.md
2026-07-22 11:37:46 +00:00

119 lines
8.7 KiB
Markdown

# Architecture
## Ownership boundary
Vue Router owns matching, lazy components, committed routes, redirects, guards, URL serialization, and browser history. Native Vue Router owns a separate visual ledger containing mounted route entries, scroll/focus state, cache status, and the current interactive transaction.
A forward drag calls `router.resolve()` and Vue Router's public `loadRouteLocation()`, then renders the location through `<RouterView :route>` without calling `push()`. The URL and application history remain unchanged until the gesture commits.
Each preview subtree receives a scoped `routeLocationKey`, so `useRoute()` returns preview params even though the global route is not committed. A normal `router.push()` or `replace()` runs only after the gesture chooses to commit. A guard failure cancels the transaction and removes the preview.
Preview routes can contain ordinary Vue `<Suspense>` boundaries. A route may therefore become a live navigation surface immediately, show its fallback while async child setup continues, and preserve the resolved child when the route later moves into the mounted cache. Route guards remain commit-time authority and can still reject that cached destination on a later entry attempt.
## Transaction lifecycle
Transactions move through `interactive`, `committing`, `settling`, and cancellation states. They expose normalized progress and velocity plus `fromKey`, `toKey`, direction, presentation, and optional source geometry.
The runtime deliberately keeps two ledgers. The navigation stack mirrors committed push/replace/pop semantics and is the only source of predictive-back targets. The view cache owns mounted component instances independently, so a replaced tab can be reused without becoming an accidental back destination.
Pointer movement changes only a CSS progress variable. Built-in presentations restrict active-frame work to compositor-friendly properties. When a destination needs a new component tree, the runtime resolves its lazy route, mounts it, and gives the browser a preparation frame before motion starts; cached destinations skip that wait. Velocity is expressed as normalized route progress per second, so gesture behavior remains consistent across screen sizes. Release uses distance/velocity intent and a damped spring whose settling rate follows the user's flick speed. Reduced-motion mode settles immediately.
Settling animations are interruptible. A new button navigation or recognized gesture waits only for any in-flight Vue Router guard/history commit, immediately finalizes the old visual transaction, and begins from the newly authoritative route. It never waits for the previous spring to finish. Leading-edge back recognition runs in the navigator capture phase so partially visible component layers cannot steal the physical back edge.
### Runtime domains
`runtime.ts` is the public transaction coordinator. Its supporting domains live
under `packages/core/src/runtime`:
- `vue-router-bridge.ts` contains all Vue Router-specific integration: route
resolution and loading, commit operations, `afterEach` reconciliation,
browser-history completion, and the scoped Options API `$route` bridge.
- `history-ledger.ts` owns native push, replace, and pop history semantics.
- `view-store.ts` owns mounted route entries, active state, preview reuse,
eviction, underlay protection, and cache statistics.
- `animation.ts` owns gesture commit policy, view preparation, and spring
settling.
- `presentations.ts` owns built-in and application-defined presentations.
- `diagnostics.ts` owns runtime diagnostic subscriptions and event emission.
- `route-entry.ts` owns route-entry construction, labels, timestamps, and
sibling direction.
Keeping the Vue Router adapter separate makes the compatibility work visible
and prevents history, cache, rendering, and animation policy from accumulating
inside the integration layer.
Component gesture owners stop propagation before a containing navigator can claim the same pointer. Navigator gestures wait for horizontal intent, use pointer capture, respect form controls and `data-native-gesture="ignore"`, and preserve normal vertical scrolling through `touch-action: pan-y`.
## Route metadata
```ts
interface NativeRouteOptions {
navigator?: string;
presentation?:
"push" | "reveal" | "slide" | "fade" | "modal" | "sheet" | string;
parent?: RouteLocationRaw | ((route) => RouteLocationRaw);
siblingGroup?: string;
siblingOrder?: number;
siblingHistory?: "push" | "replace";
cache?: boolean | "pin";
gesture?: boolean | "edge" | "full";
}
```
`parent` supplies a predictive back target when a deep link starts without an in-memory predecessor. Sibling routes replace history by default and use the built-in `slide` presentation, which moves both pages one-to-one as adjacent surfaces. Direction comes from `siblingOrder`. Set `siblingHistory: 'push'` when browser back should visit prior sibling selections.
## Custom presentations
```ts
nativeRouter.registerPresentation(
definePresentation({
name: "scale-fade",
axis: "x",
layerStyle({ role, progress }) {
return role === "to"
? { opacity: progress, transform: `scale(${0.92 + progress * 0.08})` }
: { opacity: 1 - progress * 0.4 };
},
}),
);
```
Applications can call `beginInteractive()`, `updateInteractive()`, and `finishInteractive()` to drive the same transaction engine from a bespoke recognizer.
## Cache semantics
The cache is lazy: application startup mounts the current route, not every sibling. A replace-style sibling is created when it is first visited or previewed and can then remain mounted without becoming a browser-back entry. Recent history targets can also stay warm so predictive Back restores component-local state such as a scrolled list immediately.
The default limit is four inactive views per runtime. `cache: false` always unmounts an inactive route, while `cache: 'pin'` exempts it from ordinary LRU and default bulk trimming. A pushed detail route that is popped or dismissed is unmounted after its exit animation unless it is explicitly pinned. If a guard rejects a cached destination, that component tree is evicted because it is no longer a valid navigation target. Older entries keep lightweight route descriptors and are lazily reconstructed if history reaches them again.
Applications can explicitly release an inactive location with `nativeRouter.unload('/stories')`. It returns the number of component instances unmounted, retains history descriptors, and refuses to unload the active route or either side of an in-progress transition. `trimCache()` remains the bulk operation.
This is deliberately not implemented with a single Vue `<KeepAlive>`. An interactive transition must render the current and destination route instances concurrently, while one `<KeepAlive>` outlet normally activates one selected child. Separate temporary wrappers would themselves be removed and lose their caches. The runtime therefore owns the small multi-view cache and exposes equivalent route-aware lifecycle signals:
```ts
import {
onNativeViewActivate,
onNativeViewDeactivate,
onNativeViewEvict,
useNativeViewActiveEffect,
useNativeViewLifecycle,
} from "@native-vue-router/core";
const view = useNativeViewLifecycle();
useNativeViewActiveEffect(() => {
const timer = startPolling();
return () => stopPolling(timer);
});
onNativeViewEvict((reason) => saveDraft(view.route.value, reason));
```
`isActive` means the route is authoritative. `isVisible` also includes either side of an in-progress transition and the visible, inert route beneath a partial sheet. Use `useNativeViewActiveEffect` for polling and other work that should pause in a cached tab, or `useNativeViewVisibleEffect` for work needed during the animation or while painted beneath a sheet. Application data that must survive eviction belongs in an application store.
## Optional performance profiler
`createNativeNavigationProfiler(runtime)` correlates `requestAnimationFrame` intervals with timing-safe runtime events: route loading, cold view preparation, transaction start/commit/end, and eviction. It estimates the device's actual refresh interval instead of assuming 60 Hz, then flags frame gaps larger than 1.5 times that baseline. Reports also include Long Tasks, layout shifts, event timing, and resource timing when the host implements those Performance Observer entry types.
Safari does not currently expose every Chromium performance entry, so rAF cadence and native-router events are the portable ground truth. Visibility changes are retained because backgrounding or the share sheet can throttle rAF and would otherwise resemble dropped frames. Sampling is completely opt-in and capped at 30,000 frames by default. Route labels use record names or declared path patterns, never params or query values.