86 lines
6.1 KiB
Markdown
86 lines
6.1 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. 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 manual 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.
|
|
|
|
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.
|