810 lines
28 KiB
Markdown
810 lines
28 KiB
Markdown
# Native Vue Router usage guide
|
|
|
|
Native Vue Router adds gesture-driven, interruptible navigation to Vue 3 while
|
|
leaving Vue Router responsible for route matching, URLs, redirects, guards, and
|
|
browser history. It renders a provisional destination beside the current route,
|
|
lets a pointer gesture control the transition, and commits the Vue Router
|
|
navigation only when the gesture completes.
|
|
|
|
The current support contract is Vue 3.5+, Vue Router 5, and client-side DOM
|
|
rendering. SSR hydration of a live native view stack is not yet a complete
|
|
feature.
|
|
|
|
## Packages
|
|
|
|
| Package | Purpose |
|
|
| ---------------------------------- | ------------------------------------------------------------------------ |
|
|
| `@native-vue-router/core` | Runtime, route surfaces, gestures, caching, lifecycle APIs, and profiler |
|
|
| `@native-vue-router/preset-native` | Native-looking back and tab controls, safe-area CSS, and motion tokens |
|
|
| `@native-vue-router/capacitor` | Hardware Back, deep links, app lifecycle, root exit, and haptics |
|
|
| `@native-vue-router/electron` | Host back/forward integration and Chromium history-gesture suppression |
|
|
|
|
## Install
|
|
|
|
Install the core package alongside its peer dependencies:
|
|
|
|
```bash
|
|
npm install vue@^3.5 vue-router@^5 @native-vue-router/core
|
|
```
|
|
|
|
Add optional packages only when the application uses them:
|
|
|
|
```bash
|
|
npm install @native-vue-router/preset-native
|
|
npm install @native-vue-router/capacitor @capacitor/app @capacitor/core @capacitor/haptics
|
|
npm install @native-vue-router/electron
|
|
```
|
|
|
|
When consuming this repository directly, build the packages first and install
|
|
the required package directories or packed tarballs into the target project:
|
|
|
|
```bash
|
|
npm install
|
|
npm run build:packages
|
|
npm pack --workspace @native-vue-router/core
|
|
```
|
|
|
|
Import the core stylesheet once from the application entry point. Import the
|
|
preset stylesheet as well when using its controls:
|
|
|
|
```ts
|
|
import "@native-vue-router/core/style.css";
|
|
import "@native-vue-router/preset-native/style.css"; // optional
|
|
```
|
|
|
|
The elements containing the navigator must have a definite height. A typical
|
|
full-screen application uses:
|
|
|
|
```css
|
|
html,
|
|
body,
|
|
#app {
|
|
width: 100%;
|
|
height: 100%;
|
|
margin: 0;
|
|
}
|
|
```
|
|
|
|
## Minimal setup
|
|
|
|
Create the Vue Router first, then create and install the native runtime. Install
|
|
Vue Router before Native Vue Router so route injection and Options API `$route`
|
|
scoping are configured correctly.
|
|
|
|
```ts
|
|
// src/main.ts
|
|
import { createApp } from "vue";
|
|
import { createRouter, createWebHistory } from "vue-router";
|
|
import { createNativeRouter } from "@native-vue-router/core";
|
|
import App from "./App.vue";
|
|
import HomeView from "./views/HomeView.vue";
|
|
import ProductView from "./views/ProductView.vue";
|
|
import "@native-vue-router/core/style.css";
|
|
|
|
export const router = createRouter({
|
|
history: createWebHistory(),
|
|
routes: [
|
|
{ path: "/", name: "home", component: HomeView },
|
|
{
|
|
path: "/products/:id",
|
|
name: "product",
|
|
component: ProductView,
|
|
meta: {
|
|
native: {
|
|
presentation: "push",
|
|
parent: "/",
|
|
gesture: "edge",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
export const nativeRouter = createNativeRouter({
|
|
router,
|
|
cache: { maxInactive: 4 },
|
|
});
|
|
|
|
const app = createApp(App);
|
|
app.use(router);
|
|
app.use(nativeRouter);
|
|
|
|
await router.isReady();
|
|
app.mount("#app");
|
|
```
|
|
|
|
Replace the application-level `<RouterView>` with `<NativeRouterView>` and wrap
|
|
the navigation surface in `<NativeNavigator>` to enable predictive Back.
|
|
|
|
```vue
|
|
<!-- src/App.vue -->
|
|
<script setup lang="ts">
|
|
import { NativeNavigator, NativeRouterView } from "@native-vue-router/core";
|
|
</script>
|
|
|
|
<template>
|
|
<NativeNavigator>
|
|
<NativeRouterView />
|
|
</NativeNavigator>
|
|
</template>
|
|
```
|
|
|
|
Nested `<RouterView>` components inside route components continue to work. Use
|
|
one application-level `NativeRouterView` for one native runtime; independent
|
|
nested native navigators are not currently a complete feature.
|
|
|
|
## Route metadata
|
|
|
|
Declare presentation, topology, history, cache, and gesture policy next to each
|
|
route:
|
|
|
|
```ts
|
|
import type { RouteRecordRaw } from "vue-router";
|
|
|
|
export const routes: RouteRecordRaw[] = [
|
|
{
|
|
path: "/inbox",
|
|
name: "inbox",
|
|
component: () => import("./views/InboxView.vue"),
|
|
meta: {
|
|
native: {
|
|
siblingGroup: "primary",
|
|
siblingOrder: 0,
|
|
siblingHistory: "replace",
|
|
cache: "pin",
|
|
gesture: "full",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
path: "/chat/:id",
|
|
name: "chat",
|
|
component: () => import("./views/ChatView.vue"),
|
|
meta: {
|
|
native: {
|
|
presentation: "push",
|
|
parent: "/inbox",
|
|
gesture: "edge",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
path: "/chat/:id/details",
|
|
name: "chat-details",
|
|
component: () => import("./views/ChatDetailsView.vue"),
|
|
meta: {
|
|
native: {
|
|
parent: (route) => ({
|
|
name: "chat",
|
|
params: { id: route.params.id },
|
|
query: route.query,
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
];
|
|
```
|
|
|
|
| Option | Meaning |
|
|
| --------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
| `presentation` | `push`, `reveal`, `slide`, `fade`, `modal`, `sheet`, `none`, or a registered custom name |
|
|
| `transition` | Compatibility alias for `presentation`; prefer `presentation` in new code |
|
|
| `parent` | Logical Back target for a cold-start/deep-linked route; may be a location or a function of the current route |
|
|
| `siblingOrder` | Numeric visual order used to derive sibling direction |
|
|
| `siblingHistory` | `replace` keeps peer selections out of Back history; `push` makes them Back destinations |
|
|
| `cache` | `false` unmounts when inactive, `true` uses normal retention, and `pin` exempts the view from ordinary LRU trimming |
|
|
| `gesture` | `false` disables navigator gestures for the route; `edge` and `full` describe intended policy |
|
|
| `navigator`, `siblingGroup` | Reserved topology labels; the current navigator still receives its sibling list explicitly |
|
|
|
|
Declare `parent` for detail, settings, modal, and other routes that should have a
|
|
predictive destination when opened directly. It is a logical product
|
|
relationship, not proof that a matching browser history entry exists.
|
|
|
|
## Navigate
|
|
|
|
Use `useNativeRouter()` inside `setup()`:
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import { useNativeRouter } from "@native-vue-router/core";
|
|
|
|
const native = useNativeRouter();
|
|
|
|
async function openProduct(id: string) {
|
|
const committed = await native.push({
|
|
name: "product",
|
|
params: { id },
|
|
query: { source: "featured" },
|
|
hash: "#summary",
|
|
});
|
|
|
|
if (committed) {
|
|
// Vue Router accepted the navigation.
|
|
}
|
|
}
|
|
</script>
|
|
```
|
|
|
|
The same runtime is available as `this.$nativeRouter` in Options API
|
|
components.
|
|
|
|
| Method | Use |
|
|
| ---------------------------- | --------------------------------------------------------------------- |
|
|
| `push(to, options?)` | Add an animated history entry |
|
|
| `replace(to, options?)` | Replace the current history entry |
|
|
| `sibling(to, options?)` | Move between ordered peer routes |
|
|
| `pop()` | Navigate to the previous native history entry or declared parent |
|
|
| `present(to, presentation?)` | Present a route, using `modal` by default |
|
|
| `dismiss()` | Return from a presented route |
|
|
| `preload(to)` | Resolve and load a lazy route without mounting or committing it |
|
|
| `unload(to)` | Unmount inactive instances of one location and return the count |
|
|
| `trimCache(options?)` | Unmount inactive cached views while retaining lightweight descriptors |
|
|
|
|
Navigation options can override `presentation`, `replace`, `direction`, and
|
|
`sourceRect`. The navigation methods return `true` when Vue Router accepts the
|
|
commit and `false` for a no-op, cancellation, or rejected navigation.
|
|
|
|
Use normal `router.push()` for redirects or flows that intentionally do not need
|
|
a native preview. The runtime reconciles external Vue Router navigations, but
|
|
they do not receive the same preview-driven transition.
|
|
|
|
## Links and gesture components
|
|
|
|
### NativeLink
|
|
|
|
`NativeLink` renders a real anchor, resolves its `href`, preserves modified-click
|
|
behavior, and routes an ordinary primary click through the native runtime.
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import { NativeLink } from "@native-vue-router/core";
|
|
</script>
|
|
|
|
<template>
|
|
<NativeLink
|
|
:to="{ name: 'product', params: { id: '42' } }"
|
|
presentation="push"
|
|
class="product-link"
|
|
>
|
|
Product 42
|
|
</NativeLink>
|
|
</template>
|
|
```
|
|
|
|
Its navigation props are `to`, `replace`, and `presentation`; other attributes
|
|
are passed to the anchor.
|
|
|
|
### NativeGestureLink
|
|
|
|
`NativeGestureLink` lets a horizontal drag on a component reveal its destination.
|
|
It accepts `to`, `presentation` (default `reveal`), `replace`, `direction`
|
|
(`left`, `right`, or `any`), and `as` (default `div`).
|
|
|
|
```vue
|
|
<NativeGestureLink
|
|
as="article"
|
|
:to="{ name: 'product', params: { id: product.id } }"
|
|
presentation="reveal"
|
|
direction="left"
|
|
>
|
|
<ProductCard :product="product" />
|
|
</NativeGestureLink>
|
|
```
|
|
|
|
Choose a semantic `as` element and provide keyboard behavior when the result is
|
|
interactive. A normal click also invokes native `push()`.
|
|
|
|
### NativeNavigator
|
|
|
|
Pass ordered peer locations to enable full-surface horizontal sibling paging.
|
|
The leading edge remains reserved for Back when `canGoBack` is true.
|
|
|
|
```vue
|
|
<NativeNavigator
|
|
:siblings="['/inbox', '/stories', '/profile']"
|
|
:edge-width="28"
|
|
>
|
|
<NativeRouterView />
|
|
</NativeNavigator>
|
|
```
|
|
|
|
Inputs, editable content, links, buttons, and elements carrying
|
|
`data-native-gesture="ignore"` are excluded from automatic gesture recognition.
|
|
Use the explicit attribute for carousels, maps, editors, canvases, or other
|
|
regions that own horizontal input.
|
|
|
|
### NativeDismissGesture
|
|
|
|
Wrap a custom full-height modal surface to make a downward drag dismiss it:
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import { NativeDismissGesture, useNativeRouter } from "@native-vue-router/core";
|
|
|
|
const native = useNativeRouter();
|
|
</script>
|
|
|
|
<template>
|
|
<NativeDismissGesture as="main" class="sheet">
|
|
<button type="button" @click="native.dismiss()">Close</button>
|
|
<!-- sheet content -->
|
|
</NativeDismissGesture>
|
|
</template>
|
|
```
|
|
|
|
### NativeSheet
|
|
|
|
Use `NativeSheet` inside a route whose presentation is `sheet`. It keeps the
|
|
surface below the device's safe top inset, leaves the previous route visible but
|
|
inert beneath a backdrop, and includes a drag handle.
|
|
|
|
With no breakpoints, the surface grows to its content and is capped at the
|
|
available device height:
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import { NativeSheet } from "@native-vue-router/core";
|
|
</script>
|
|
|
|
<template>
|
|
<NativeSheet aria-label="Filters">
|
|
<FilterForm />
|
|
</NativeSheet>
|
|
</template>
|
|
```
|
|
|
|
Supply fractional breakpoints to create snap points. Fractions are measured
|
|
against the route height after the safe top inset has been reserved:
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import { ref } from "vue";
|
|
import { NativeSheet } from "@native-vue-router/core";
|
|
|
|
const breakpoint = ref(0.55);
|
|
</script>
|
|
|
|
<template>
|
|
<NativeSheet
|
|
v-model="breakpoint"
|
|
:breakpoints="[0.3, 0.55, 1]"
|
|
:initial-breakpoint="0.55"
|
|
aria-label="Choose a location"
|
|
@breakpoint-change="savePreferredSheetSize"
|
|
>
|
|
<LocationPicker />
|
|
</NativeSheet>
|
|
</template>
|
|
```
|
|
|
|
Users drag the handle or use Arrow Up/Down, Home, and End while it is focused.
|
|
Dragging below the smallest point dismisses the route. The backdrop and Escape
|
|
also dismiss by default.
|
|
|
|
The scrollable sheet body participates in the same gesture automatically. At an
|
|
interior scroll position, the content scrolls normally. When the content is at
|
|
the top, pulling down hands the gesture to the sheet so it can move to a lower
|
|
point or dismiss. When the content is at the bottom, pushing upward grows the
|
|
sheet to its next point. Mouse/pen dragging, touch input, and thresholded
|
|
trackpad/wheel overscroll follow the same boundary rules.
|
|
|
|
Gesture ownership is chosen from the initial directional intent and remains
|
|
locked until release. A gesture that starts while content exists in that
|
|
direction stays a content gesture even if it reaches an edge or reverses. Lift
|
|
and begin a new gesture at the edge to resize the sheet. This prevents content
|
|
and sheet movement from overlapping and prevents previously scrolled distance
|
|
from becoming a sheet-height jump.
|
|
|
|
| Prop | Meaning |
|
|
| ------------------- | --------------------------------------------------------------------------------------- |
|
|
| `breakpoints` | Unique fractions greater than `0` and at most `1`; an empty list enables content height |
|
|
| `initialBreakpoint` | Initial fraction, snapped to the nearest declared point |
|
|
| `modelValue` | Current fractional point for `v-model` |
|
|
| `dismissible` | Enables sheet-triggered drag and keyboard dismissal; default `true` |
|
|
| `backdropDismiss` | Lets a backdrop click dismiss; default `true` |
|
|
| `showHandle` | Renders the built-in drag/keyboard handle; default `true` |
|
|
| `ariaLabel` | Accessible dialog label; default `Sheet` |
|
|
|
|
Use the `handle` slot to replace the visual handle without replacing its input
|
|
behavior. Theme the surface with `--nvr-sheet-background`,
|
|
`--nvr-sheet-backdrop`, `--nvr-sheet-radius`, and `--nvr-sheet-top-gap`.
|
|
|
|
For a route opened with `native.present(to, "sheet")`, the runtime remembers the
|
|
sheet presentation after commit. Defining `meta.native.presentation: "sheet"`
|
|
as well makes direct URL entry and raw Vue Router navigation use the same
|
|
contained layout.
|
|
|
|
During presentation and dismissal, `NativeSheet` moves its actual surface by
|
|
that surface's height rather than translating a transparent viewport-sized
|
|
route wrapper. The source route remains continuously scaled as the sheet's
|
|
underlay, avoiding a geometry jump when the route transaction commits.
|
|
|
|
## Tabs and back controls
|
|
|
|
The optional native preset supplies a Back button and tab bar:
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import {
|
|
NativeBackButton,
|
|
NativeTabBar,
|
|
type NativeTabItem,
|
|
} from "@native-vue-router/preset-native";
|
|
|
|
const tabs: NativeTabItem[] = [
|
|
{ label: "Inbox", to: "/inbox", icon: "◉" },
|
|
{ label: "Stories", to: "/stories", icon: "◎" },
|
|
{
|
|
label: "Profile",
|
|
to: "/profile",
|
|
icon: "◇",
|
|
activeWhen: (route) => route.path.startsWith("/profile"),
|
|
},
|
|
];
|
|
</script>
|
|
|
|
<template>
|
|
<header><NativeBackButton label="Back" /></header>
|
|
<NativeTabBar :items="tabs" />
|
|
</template>
|
|
```
|
|
|
|
The preset tab bar uses replace-style sibling navigation. Build a product-specific
|
|
control with `native.sibling()` when tabs need different history semantics.
|
|
|
|
## Route params, query, hash, and injected route state
|
|
|
|
Pass any normal `RouteLocationRaw` to native navigation methods and components.
|
|
`useRoute()`, `useRouter()`, and Options API `this.$route` work within active and
|
|
preview route trees. During a held gesture, the destination subtree sees its own
|
|
params, query, hash, matched records, and metadata even though
|
|
`router.currentRoute` still points at the committed source route.
|
|
|
|
This distinction is intentional:
|
|
|
|
- Read `useRoute()` or `$route` inside a route component for that surface's
|
|
scoped route.
|
|
- Read `router.currentRoute` only when the application needs the globally
|
|
committed route.
|
|
- Expect the two values to differ while a preview is visible.
|
|
|
|
Normal Vue `provide()` and `inject()` work across the route surface. App-level
|
|
provides, plugin provides, and values provided by route components remain
|
|
available to descendants.
|
|
|
|
## Lifecycle and cached views
|
|
|
|
Native Vue Router can keep inactive route component trees mounted. Vue's normal
|
|
mount, update, and unmount hooks continue to describe component lifetime, but
|
|
being mounted does not mean the route is the current screen.
|
|
|
|
Use the native lifecycle for route visibility and activity:
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import {
|
|
onNativeViewActivate,
|
|
onNativeViewDeactivate,
|
|
onNativeViewEvict,
|
|
useNativeViewActiveEffect,
|
|
useNativeViewLifecycle,
|
|
useNativeViewVisibleEffect,
|
|
} from "@native-vue-router/core";
|
|
|
|
const view = useNativeViewLifecycle();
|
|
|
|
useNativeViewActiveEffect(() => {
|
|
const controller = new AbortController();
|
|
startPolling({ signal: controller.signal });
|
|
return () => controller.abort();
|
|
});
|
|
|
|
useNativeViewVisibleEffect(() => {
|
|
const stop = startAnimationNeededDuringTransitions();
|
|
return stop;
|
|
});
|
|
|
|
onNativeViewActivate(() => resumeMedia());
|
|
onNativeViewDeactivate(() => pauseMedia());
|
|
onNativeViewEvict((reason) => saveDraft(view.route.value, reason));
|
|
</script>
|
|
```
|
|
|
|
`isActive` means Vue Router has made the route authoritative. `isVisible` is
|
|
also true for either side of an interactive transition and for a sheet's visual
|
|
underlay. An underlay remains inert and is not active. `isCached` identifies a
|
|
mounted inactive view; `isPreview` identifies an uncommitted destination.
|
|
|
|
Use active effects for polling, subscriptions, media, analytics, and work that
|
|
should run only on the current route. Use visible effects for rendering work
|
|
needed while the route is on screen during a transition. Put durable state in a
|
|
store or persistence layer because cache eviction unmounts component-local
|
|
state.
|
|
|
|
The default cache limit is four inactive, non-pinned views. History descriptors
|
|
remain after a component tree is evicted and are remounted if navigation reaches
|
|
them later.
|
|
|
|
## Vue built-in components
|
|
|
|
`NativeRouterView` exposes `Component`, `route`, and `entry` through its default
|
|
slot, so normal Vue wrappers can be used inside each native route layer:
|
|
|
|
```vue
|
|
<NativeRouterView v-slot="{ Component, route }">
|
|
<Suspense>
|
|
<Transition name="route-content" mode="out-in">
|
|
<KeepAlive :max="3">
|
|
<component :is="Component" :key="route.fullPath" />
|
|
</KeepAlive>
|
|
</Transition>
|
|
|
|
<template #fallback>
|
|
<RouteSkeleton />
|
|
</template>
|
|
</Suspense>
|
|
</NativeRouterView>
|
|
```
|
|
|
|
These components retain their normal Vue meaning:
|
|
|
|
- `<KeepAlive>` caches components selected within that route layer. It is not a
|
|
replacement for the native multi-route cache, and native route changes alone
|
|
do not imply Vue `onActivated()` or `onDeactivated()`.
|
|
- `<Transition>` animates changes inside a layer. Native presentations animate
|
|
the route layers themselves.
|
|
- `<Suspense>` may show a fallback while an async preview component resolves.
|
|
- `<Teleport>` can move DOM outside the layer. Because teleported DOM is outside
|
|
the layer's `inert` and `aria-hidden` boundary, close or hide overlays whenever
|
|
the owning native view is not visible.
|
|
|
|
A visibility-safe teleported overlay looks like this:
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import { ref } from "vue";
|
|
import { useNativeViewLifecycle } from "@native-vue-router/core";
|
|
|
|
const open = ref(false);
|
|
const view = useNativeViewLifecycle();
|
|
</script>
|
|
|
|
<template>
|
|
<button type="button" @click="open = true">Open overlay</button>
|
|
<Teleport to="body">
|
|
<MyOverlay v-if="open && view.isVisible.value" @close="open = false" />
|
|
</Teleport>
|
|
</template>
|
|
```
|
|
|
|
Options API lifecycle hooks (`beforeCreate`, `created`, `beforeMount`,
|
|
`mounted`, `beforeUpdate`, `updated`, `beforeUnmount`, and `unmounted`) and the
|
|
corresponding Composition API hooks keep their standard Vue behavior.
|
|
|
|
## Guards, redirects, and preview side effects
|
|
|
|
Forward navigation resolves and loads the route component before commit so the
|
|
user can drag a live destination. Vue Router guards run when the runtime commits
|
|
the real `push()`, `replace()`, or Back operation. A rejected guard removes the
|
|
preview and restores the source route; a redirect is reconciled to the route Vue
|
|
Router accepts.
|
|
|
|
Consequently, a preview component may execute `setup()` and mount before a guard
|
|
allows entry, then unmount without ever becoming active. Avoid irreversible work
|
|
such as analytics events, mutations, purchases, or permanent subscriptions in
|
|
unconditional setup/mount code. Tie committed-screen behavior to
|
|
`useNativeViewActiveEffect()` or an explicit committed application action.
|
|
|
|
## Custom presentations
|
|
|
|
Register a presentation at runtime or pass it in `createNativeRouter()`:
|
|
|
|
```ts
|
|
import {
|
|
createNativeRouter,
|
|
definePresentation,
|
|
} from "@native-vue-router/core";
|
|
|
|
const scaleFade = definePresentation({
|
|
name: "scale-fade",
|
|
axis: "x",
|
|
layerStyle({ role, progress }) {
|
|
return role === "to"
|
|
? {
|
|
opacity: progress,
|
|
transform: `scale(${0.94 + progress * 0.06})`,
|
|
}
|
|
: { opacity: 1 - progress * 0.25 };
|
|
},
|
|
});
|
|
|
|
const nativeRouter = createNativeRouter({
|
|
router,
|
|
presentations: [scaleFade],
|
|
});
|
|
|
|
// This is also valid later:
|
|
nativeRouter.registerPresentation(scaleFade);
|
|
```
|
|
|
|
Reference the registered name from route metadata or a navigation option.
|
|
Presentation functions should derive compositor-friendly styles from progress;
|
|
they must not mutate history or application state.
|
|
|
|
Advanced interactions can call `beginInteractive()`, `updateInteractive()`,
|
|
`finishInteractive()`, and `cancelInteractive()` directly. Progress is normalized
|
|
from `0` to `1`, and velocity is normalized route progress per second. Cancel the
|
|
transaction when the owning component unmounts and ignore stale async results by
|
|
checking the returned transaction ID.
|
|
|
|
## Platform adapters
|
|
|
|
### Capacitor
|
|
|
|
```ts
|
|
import { createCapacitorAdapter } from "@native-vue-router/capacitor";
|
|
|
|
const nativeRouter = createNativeRouter({
|
|
router,
|
|
platform: createCapacitorAdapter({
|
|
haptics: true,
|
|
exitAtRoot: true,
|
|
trimCacheOnPause: true,
|
|
deepLinkPath: (url) => `${url.pathname}${url.search}${url.hash}`,
|
|
}),
|
|
});
|
|
```
|
|
|
|
The adapter handles hardware Back, launch/app URLs, pause cancellation, cache
|
|
trimming, optional haptics, and optional exit at the root. Configure Universal
|
|
Links/App Links in the native project separately.
|
|
|
|
### Electron
|
|
|
|
Disable Chromium's competing overscroll navigation in the main process before
|
|
`app.whenReady()`:
|
|
|
|
```ts
|
|
import { app } from "electron";
|
|
import { disableElectronHistoryGestures } from "@native-vue-router/electron";
|
|
|
|
disableElectronHistoryGestures(app.commandLine);
|
|
```
|
|
|
|
Install the renderer adapter after exposing the documented `window.nativeVueHost`
|
|
back/forward/memory-pressure bridge from a secure preload:
|
|
|
|
```ts
|
|
import { createElectronRendererAdapter } from "@native-vue-router/electron";
|
|
|
|
const nativeRouter = createNativeRouter({
|
|
router,
|
|
platform: createElectronRendererAdapter(),
|
|
});
|
|
```
|
|
|
|
Use hash history for packaged `file:` applications unless the Electron host
|
|
serves navigation URLs through an application protocol.
|
|
|
|
### Browser and PWA
|
|
|
|
The core works in normal browser tabs, but a browser may reserve an edge gesture
|
|
before page JavaScript can claim it. An installed iOS PWA can improve gesture
|
|
ownership with an early non-passive edge guard, but web content cannot change
|
|
`WKWebView.allowsBackForwardNavigationGestures`. Use Capacitor when deterministic
|
|
native-level ownership is required.
|
|
|
|
## Add to an existing Vue Router application
|
|
|
|
Adopt the library incrementally:
|
|
|
|
1. Confirm Vue 3.5+ and Vue Router 5, then install core and its CSS.
|
|
2. Create the native runtime from the existing router and install it after
|
|
`app.use(router)`.
|
|
3. Replace only the root visual outlet with `NativeRouterView`; leave nested
|
|
router views in route components intact.
|
|
4. Change navigation that needs native motion from `router.push()`/`RouterLink`
|
|
to runtime methods or `NativeLink`. Keep ordinary Vue Router calls where no
|
|
native transition is wanted.
|
|
5. Add `parent` metadata to deep-linkable child routes.
|
|
6. Identify peer routes such as tabs, assign `siblingOrder`, choose
|
|
`siblingHistory`, and pass their locations to `NativeNavigator`.
|
|
7. Move active-screen side effects from unconditional mount hooks into native
|
|
active or visible effects.
|
|
8. Mark nested horizontal controls with `data-native-gesture="ignore"` and add
|
|
component gesture links only where the product intends them.
|
|
9. Choose cache policy per route and move durable state out of component-local
|
|
memory.
|
|
10. Exercise URLs, redirects, rejected guards, direct deep links, browser Back
|
|
and Forward, held/cancelled gestures, reduced motion, and keyboard/focus
|
|
behavior before broad rollout.
|
|
|
|
For a lower-risk migration, start with button-driven `push()`, `pop()`, and
|
|
`present()`. Add predictive Back, siblings, and component-originated gestures
|
|
after the route topology and lifecycle behavior are verified.
|
|
|
|
## Diagnostics and profiling
|
|
|
|
Inspect the runtime's reactive state while integrating:
|
|
|
|
```ts
|
|
const native = useNativeRouter();
|
|
|
|
watchEffect(() => {
|
|
console.table(native.cacheStats.value);
|
|
console.log(native.transaction.value);
|
|
});
|
|
|
|
const stop = native.onDiagnostic((event) => {
|
|
console.debug("native-navigation", event);
|
|
});
|
|
```
|
|
|
|
The opt-in profiler records frame cadence and timing-safe navigation events:
|
|
|
|
```ts
|
|
import { createNativeNavigationProfiler } from "@native-vue-router/core";
|
|
|
|
const profiler = createNativeNavigationProfiler(nativeRouter, {
|
|
metadata: { build: import.meta.env.VITE_BUILD_ID },
|
|
});
|
|
|
|
profiler.start();
|
|
// Reproduce the navigation.
|
|
const report = profiler.stop();
|
|
const json = profiler.toJSON(report);
|
|
profiler.dispose();
|
|
```
|
|
|
|
Route params, query values, and application state are omitted from profiler
|
|
route labels.
|
|
|
|
## Troubleshooting
|
|
|
|
**The route surface is blank or has zero height.** Give `html`, `body`, `#app`,
|
|
and the application shell a definite height, and import the core stylesheet.
|
|
|
|
**`useNativeRouter()` says the plugin is not installed.** Create one runtime and
|
|
call `app.use(nativeRouter)` before mounting the app.
|
|
|
|
**`useNativeViewLifecycle()` throws.** Call it only from a component rendered
|
|
inside `NativeRouterView`.
|
|
|
|
**A route mounts even though a guard rejects it.** This is preview behavior, not
|
|
a committed navigation. Move irreversible work to an active effect.
|
|
|
|
**Back has no visual destination after a direct deep link.** Add a `parent`
|
|
location or parent function to that route's native metadata.
|
|
|
|
**A tab appears in browser Back history.** Set `siblingHistory: "replace"` and
|
|
use `native.sibling()` or the preset tab bar.
|
|
|
|
**A carousel or editor fights the route gesture.** Put
|
|
`data-native-gesture="ignore"` on the region that owns the input.
|
|
|
|
**A teleported dialog remains interactive from an inactive cached route.** Gate
|
|
the teleport content on `useNativeViewLifecycle().isVisible` and close it on
|
|
hide/deactivate when appropriate.
|
|
|
|
**Local state disappears.** The cache is bounded and can be trimmed by a host.
|
|
Use `cache: "pin"` sparingly or store durable state outside the route component.
|
|
|
|
## Further reading
|
|
|
|
- [How the preview and commit model works](docs/how-it-works.md)
|
|
- [Architecture reference](docs/architecture.md)
|
|
- [Engineering constraints and trade-offs](docs/challenges-and-tradeoffs.md)
|
|
- [Scalability and extension points](docs/principles-and-scalability.md)
|
|
- [Platform integration](docs/platforms.md)
|
|
- [Interactive demo](apps/demo)
|
|
|
|
## AI agent skill
|
|
|
|
This repository includes a portable integration skill at
|
|
[`skills/integrate-native-vue-router`](skills/integrate-native-vue-router).
|
|
Copy that complete directory into the skills location recognized by the agent
|
|
(for Codex, normally `~/.codex/skills/`) and invoke it as
|
|
`$integrate-native-vue-router`. Keep `SKILL.md`, `agents/openai.yaml`, and the
|
|
`references` directory together so the integration workflow retains its API and
|
|
verification reference.
|