Files
Native-Router-Vue/docs/architecture.md
2026-07-21 21:59:26 +10:00

88 lines
6.6 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.
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. Use `useNativeViewActiveEffect` for polling and other work that should pause in a cached tab, or `useNativeViewVisibleEffect` for work needed during the animation. Application data that must survive eviction belongs in an application store.