@native-router/core 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/index.cjs +242 -4
- package/dist/index.mjs +242 -6
- package/dist/types/router.d.ts +54 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,18 @@ const unlisten = listen(router, (view) => {
|
|
|
27
27
|
});
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
`viewStack` is the SPA-navigation counterpart of the browser's [bfcache](https://web.dev/articles/bfcache). The browser snapshots whole documents so cross-document back/forward restores instantly; the router snapshots resolved views so same-document back/forward (`pushState`/POP) does too. The two layers are complementary and never overlap: a same-document navigation never enters the bfcache, and a bfcache restore does not fire `popstate`. Together with your data layer they stack as **bfcache > viewStack > queryCache**, outermost first — any restore short-circuits every inner layer with zero requests, so freshness is compensated at the edges (e.g. refetch-on-focus in the query layer).
|
|
31
|
+
|
|
32
|
+
Snapshots can outlive their validity — after a logout or an account switch, the previous account's resolved views are exactly what a back POP must not restore. `invalidate(router)` drops every snapshot at once: the currently rendered view is untouched (no re-resolve, no re-render), and the next back/forward re-runs the guards and loaders of the landed entry through the same lazy path as out-of-window entries.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import {invalidate} from '@native-router/core';
|
|
36
|
+
|
|
37
|
+
// After the session identity changed: keep rendering the current view,
|
|
38
|
+
// but never restore a snapshot of the previous account on back/forward.
|
|
39
|
+
invalidate(router);
|
|
40
|
+
```
|
|
41
|
+
|
|
30
42
|
### Survives a refresh
|
|
31
43
|
|
|
32
44
|
The session stack is serialized into `history.state` as a bounded tail window (`maxStackDepth`, default 100) and restored on `create`. Warm the window once after a refresh with `initHistoryStack`, and every in-window back/forward renders from cache with zero requests. Entries outside the window fall back to a single lazy re-resolve.
|
|
@@ -58,7 +70,9 @@ commit(router, entry.task, entry.location); // commit like a click
|
|
|
58
70
|
- Route matching via path-to-regexp: declaration order, layout routes without `path`, index/fallback children with `path: ''`, strict trailing slashes, case-sensitive, nested params merged deep over shallow
|
|
59
71
|
- Route guards: static `redirect` and async `beforeLoad` on every route level, run shallow → deep; more than 10 chained redirects reject with `RedirectLoopError`
|
|
60
72
|
- Cancelable async navigation: a new resolve supersedes the in-flight one (`currentGuard`); `cancel()` aborts it; a history POP cancels it too. A superseded or cancelled `navigate()` promise **never settles** — don't `await` a navigation that might be superseded. Superseding or cancelling also aborts the chain's `AbortSignal`: guards (`beforeLoad` ctx) and view loaders (`ResolveViewContext`) receive it as `ctx.signal`, so their in-flight requests stop instead of only having results dropped; `preload` resolutions are shared and therefore never aborted
|
|
73
|
+
- Navigation blockers: `setBlocker(router, fn)` registers a synchronous `(to, from) => boolean` veto over path strings, asked at the head of every `navigate`/`commit`/`commitReplace` and before a history POP lands; a vetoed navigation never starts and its promise resolves immediately (a veto is not an error — unlike a cancelled navigation, whose promise never settles), a vetoed POP is rewound with a counter-`go()` that leaves any in-flight navigation running — the classic unsaved-changes guard. `refresh` and guard redirects are never blocked
|
|
61
74
|
- Navigation API: `navigate`, `refresh`, `go`/`forward`/`back`, `commit`/`commitReplace`, `createHref`, `getParams`, `match`, `toLocation`, `resolve`, `resolveTo`
|
|
75
|
+
- `invalidate(router)`: drop the session view snapshots in one call — the current view stays rendered (no re-resolve, no re-render) and the next back/forward re-resolves through the guards; the typical call site is right after a logout/account switch, so a POP cannot render the previous account's data or bypass guards that already ran
|
|
62
76
|
- Search validation via [Standard Schema](https://standardschema.dev): a `search` schema on any route level (zod/valibot/arktype, no hard dependency), parsed with `parseSearch`/`parseSearchSync`; failures throw `SearchError`
|
|
63
77
|
- `preload(router, to, {ttl})`: resolve a target through the guards ahead of time, sharing one task across concurrent callers (in-flight dedup) with a TTL, default 30s; consumed entries are dropped on commit
|
|
64
78
|
- `errorHandler` hook turns resolve failures into fallback views
|
|
@@ -98,6 +112,14 @@ const router = create(
|
|
|
98
112
|
- `parseSearch(schema, search)` resolves the schema output (async validators are awaited); `parseSearchSync` is the render/guard-time flavor and rejects async validators with a clear error
|
|
99
113
|
- A rejected validation throws `SearchError` (a `NativeRouterError`) carrying the raw `search` and the reported `issues` — route it through your `errorHandler` like any other resolve failure
|
|
100
114
|
|
|
115
|
+
## Design principles
|
|
116
|
+
|
|
117
|
+
**Navigation semantics follow the browser** — native-router aligns with browser-native navigation semantics, not with what other SPA routers happen to do. Every navigation API decision is measured against that yardstick; "a popular router has it" is not, by itself, a reason to follow. These are deliberate choices, not bugs to fix.
|
|
118
|
+
|
|
119
|
+
- **An in-flight navigation keeps the old view.** The chain — guards, loaders — settles as a whole, and only then commits and pushes (`history.push`). The browser does the same: the old document stays displayed until the new one commits. A superseded or cancelled navigation is the browser's stop button / ESC — you stay on the old page and the URL never moved.
|
|
120
|
+
- **Failure means an error view.** A failed resolve renders the error semantics (`errorHandler` in core, `errorComponent` in the react bindings) — the counterpart of the browser's error page. There is no "waited too long → switch to a loading view" path: the browser has no UI-layer load timeout; a timeout surfaces as a network-layer failure, i.e. an error page.
|
|
121
|
+
- **Corollary: no pending-timeout escalation.** No TanStack-style `pendingMs` / in-app `pendingComponent` timeout upgrade. A pending view renders only on cold start / refresh, when there is no old view to keep.
|
|
122
|
+
|
|
101
123
|
## Install
|
|
102
124
|
|
|
103
125
|
```bash
|
package/dist/index.cjs
CHANGED
|
@@ -112,8 +112,15 @@ function create(routes, history, resolveView, options) {
|
|
|
112
112
|
resolveView,
|
|
113
113
|
history: instanceHistory,
|
|
114
114
|
locationStack,
|
|
115
|
-
// The view stack is
|
|
116
|
-
//
|
|
115
|
+
// The view stack is the SPA-navigation counterpart of the browser's
|
|
116
|
+
// bfcache — a resolved-view snapshot per history entry, restored with
|
|
117
|
+
// zero requests on POP. It is window-relative, so it is exactly as
|
|
118
|
+
// long as the location window and stays bounded by maxStackDepth
|
|
119
|
+
// with it; invalidate() drops these snapshots.
|
|
120
|
+
//
|
|
121
|
+
// viewStack 是 SPA 内导航对应的 bfcache——每个 history 条目一份已解析
|
|
122
|
+
// 视图快照,POP 时零请求还原。它按窗口相对位置存放,与 location 窗口
|
|
123
|
+
// 等长、随 maxStackDepth 一同封顶;invalidate() 丢弃这些快照。
|
|
117
124
|
viewStack: new Array(locationStack.length).fill(null),
|
|
118
125
|
baseIndex,
|
|
119
126
|
preloadCache: new Map(),
|
|
@@ -477,6 +484,15 @@ function preloadCacheOf(router) {
|
|
|
477
484
|
* @param location the location to resolved
|
|
478
485
|
*/
|
|
479
486
|
function commit(router, resolvePromise, location) {
|
|
487
|
+
// Blockers sit at the chain head: a vetoed external commit is dropped
|
|
488
|
+
// before the given task is ever awaited. The dropped task still gets
|
|
489
|
+
// a rejection handler — an orphaned failure(preload tasks re-throw
|
|
490
|
+
// NotFoundError through the default errorHandler) would otherwise
|
|
491
|
+
// surface as an unhandled rejection.
|
|
492
|
+
if (blockedBy(router, history.createPath(location))) {
|
|
493
|
+
resolvePromise.catch(util.noop);
|
|
494
|
+
return Promise.resolve();
|
|
495
|
+
}
|
|
480
496
|
// Wrap the raw task so external callers share the guarded entry
|
|
481
497
|
// pipeline; the entry location is the given one, as-is.
|
|
482
498
|
return pushEntry(router, Promise.resolve({
|
|
@@ -531,6 +547,12 @@ function pushEntry(router, entryPromise, fromLocation, ac) {
|
|
|
531
547
|
* @param location the location to resolved
|
|
532
548
|
*/
|
|
533
549
|
function commitReplace(router, resolvePromise, location) {
|
|
550
|
+
// Same chain-head veto as commit: a blocked replace never starts, and
|
|
551
|
+
// the dropped task's failure is swallowed the same way.
|
|
552
|
+
if (blockedBy(router, history.createPath(location))) {
|
|
553
|
+
resolvePromise.catch(util.noop);
|
|
554
|
+
return Promise.resolve();
|
|
555
|
+
}
|
|
534
556
|
return replaceEntry(router, Promise.resolve({
|
|
535
557
|
location,
|
|
536
558
|
task: resolvePromise
|
|
@@ -626,7 +648,9 @@ function commitBase(router, entryPromise, location, ac, onResolved) {
|
|
|
626
648
|
/**
|
|
627
649
|
* Navigate to a new path. Route guards(`redirect`/`beforeLoad`) run before
|
|
628
650
|
* the view resolves; the history entry is committed on the terminal
|
|
629
|
-
* location when guards redirected.
|
|
651
|
+
* location when guards redirected. A registered blocker(see {@link
|
|
652
|
+
* setBlocker}) may veto the navigation before anything starts. The guard
|
|
653
|
+
* phase is part of the
|
|
630
654
|
* cancelable navigation: a superseding navigate or a `cancel()` while
|
|
631
655
|
* guards are still running discards this navigation — and aborts the
|
|
632
656
|
* chain's `signal`, so guards and loaders observing it({@link
|
|
@@ -640,6 +664,14 @@ function commitBase(router, entryPromise, location, ac, onResolved) {
|
|
|
640
664
|
*/
|
|
641
665
|
function navigate(router, to, state) {
|
|
642
666
|
const location = toLocation(router, to, state);
|
|
667
|
+
// Blockers sit at the chain head, before the controller exists: a
|
|
668
|
+
// vetoed navigation never resolves a single guard, and its promise
|
|
669
|
+
// resolves immediately — a veto is not an error, and unlike a
|
|
670
|
+
// cancelled navigation(whose promise never settles) it does settle —
|
|
671
|
+
// so the ubiquitous `void navigate(...)` call sites stay untouched.
|
|
672
|
+
// The target is asked in its committed path form(`createPath`), the
|
|
673
|
+
// same string a POP blocker sees, baseUrl included.
|
|
674
|
+
if (blockedBy(router, history.createPath(location))) return Promise.resolve();
|
|
643
675
|
// One controller per navigation round: guards and view loaders of the
|
|
644
676
|
// whole chain(including redirect hops) share its signal.
|
|
645
677
|
const ac = new AbortController();
|
|
@@ -753,6 +785,174 @@ function initHistoryStack(router) {
|
|
|
753
785
|
});
|
|
754
786
|
}
|
|
755
787
|
|
|
788
|
+
/**
|
|
789
|
+
* Drop every view snapshot of the session window. The already rendered
|
|
790
|
+
* view is untouched — no re-resolve, no re-render; only future POPs
|
|
791
|
+
* change: with no snapshot to hit, {@link listen} falls back to the same
|
|
792
|
+
* lazy re-resolve path as out-of-window entries, so the landed entry's
|
|
793
|
+
* guards(`redirect`/`beforeLoad`) and loaders run again. Call it when
|
|
794
|
+
* the snapshots stop being valid — e.g. right after a logout or an
|
|
795
|
+
* account switch, so a back POP cannot render the previous account's
|
|
796
|
+
* view or bypass guards that already ran in the session.
|
|
797
|
+
*
|
|
798
|
+
* 丢弃会话窗口内的全部视图快照。已渲染的当前视图不受影响——不重解析、
|
|
799
|
+
* 不重渲染;变化的只有后续 POP:无快照可命中时,listen 落入与窗口外条目
|
|
800
|
+
* 相同的惰性重解析路径,落点条目的守卫与加载器重新执行。快照失效时调用
|
|
801
|
+
* ——例如登出/切换账号后,后退 POP 不再渲染上一账号的视图、也不再绕过
|
|
802
|
+
* 会话内已执行过的守卫。
|
|
803
|
+
* @group Methods
|
|
804
|
+
* @category Router
|
|
805
|
+
* @param router router instance
|
|
806
|
+
*/
|
|
807
|
+
function invalidate(router) {
|
|
808
|
+
// Keep the window shape: locationStack stays untouched, so getParams
|
|
809
|
+
// and the serialized window keep working — only the snapshots go.
|
|
810
|
+
router.viewStack = new Array(router.locationStack.length).fill(null);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Navigation blocker predicate: `to` and `from` are path strings
|
|
815
|
+
* (pathname, search and hash included, built with `createPath`). Return
|
|
816
|
+
* `false` to veto the navigation. A blocker that throws counts as a
|
|
817
|
+
* veto too — a crashed gate must not open, and the exception must not
|
|
818
|
+
* escape into a history listener.
|
|
819
|
+
* @group Methods
|
|
820
|
+
* @category Router
|
|
821
|
+
*/
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Registered blockers per router, in registration order. Module-level
|
|
825
|
+
* so the public {@link RouterInstance} type stays untouched; the router
|
|
826
|
+
* key is weakly held, an empty leftover set after the last release
|
|
827
|
+
* leaks nothing.
|
|
828
|
+
*/
|
|
829
|
+
const blockerRegistry = new WeakMap();
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Last settled history position per router, kept in sync by {@link listen}.
|
|
833
|
+
* POP blockers read it as the `from` path and the rewind base: by the
|
|
834
|
+
* time a POP listener runs, `history.location` is already the landed
|
|
835
|
+
* location, so the pre-POP position must be tracked separately.
|
|
836
|
+
*/
|
|
837
|
+
const lastSettled = new WeakMap();
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Pending blocker rewind per router: a rewind `go()` is in flight. The
|
|
841
|
+
* rewind's own POP must not query the blockers again — they would veto
|
|
842
|
+
* it too and ping-pong the history forever.
|
|
843
|
+
*/
|
|
844
|
+
const pendingRewind = new WeakMap();
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Register a navigation blocker. Every {@link navigate}, {@link commit},
|
|
848
|
+
* {@link commitReplace} and every history POP(see {@link listen}) asks
|
|
849
|
+
* the registered blockers(in registration order, first veto wins)
|
|
850
|
+
* before anything else; a vetoed navigation never starts — no guards,
|
|
851
|
+
* no loaders, no history change — and its promise resolves immediately
|
|
852
|
+
* (a veto is not an error; unlike a cancelled navigation, whose promise
|
|
853
|
+
* never settles, a vetoed one does). `refresh` and guard redirects
|
|
854
|
+
* are never blocked: a refresh re-resolves the current location, and a
|
|
855
|
+
* redirect is the guard chain's own target correction, already asked
|
|
856
|
+
* once at the chain head. A vetoed POP is rewound with a
|
|
857
|
+
* counter-`go()`; its landing re-announces the current view without
|
|
858
|
+
* cancelling an in-flight chain.
|
|
859
|
+
* @group Methods
|
|
860
|
+
* @category Router
|
|
861
|
+
* @param router router instance
|
|
862
|
+
* @param fn blocker predicate; `to` is the target path, `from` the
|
|
863
|
+
* current path, both path strings
|
|
864
|
+
* @returns unblock - remove the blocker(idempotent)
|
|
865
|
+
*/
|
|
866
|
+
function setBlocker(router, fn) {
|
|
867
|
+
let set = blockerRegistry.get(router);
|
|
868
|
+
if (!set) {
|
|
869
|
+
set = new Set();
|
|
870
|
+
blockerRegistry.set(router, set);
|
|
871
|
+
}
|
|
872
|
+
set.add(fn);
|
|
873
|
+
let released = false;
|
|
874
|
+
return () => {
|
|
875
|
+
if (released) return;
|
|
876
|
+
released = true;
|
|
877
|
+
set.delete(fn);
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Ask every registered blocker, in registration order. The first veto
|
|
883
|
+
* wins — `some` stops asking at it — and a blocker that throws counts
|
|
884
|
+
* as a veto: a crashed gate must not open, and the exception must not
|
|
885
|
+
* escape into a history listener.
|
|
886
|
+
*/
|
|
887
|
+
function vetoedBy(set, to, from) {
|
|
888
|
+
return Array.from(set).some(block => {
|
|
889
|
+
try {
|
|
890
|
+
return !block(to, from);
|
|
891
|
+
} catch {
|
|
892
|
+
return true;
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Ask the blockers about a router-driven navigation. Runs before
|
|
899
|
+
* anything else, so `history.location` is still the pre-navigation
|
|
900
|
+
* `from`. Returns `true` when any blocker vetoed.
|
|
901
|
+
*/
|
|
902
|
+
function blockedBy(router, to) {
|
|
903
|
+
const set = blockerRegistry.get(router);
|
|
904
|
+
if (!set) return false;
|
|
905
|
+
return vetoedBy(set, to, history.createPath(router.history.location));
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Ask the blockers about a history POP; rewind it when vetoed.
|
|
910
|
+
* Returns the POP's disposition for {@link listen}:
|
|
911
|
+
* - `'vetoed'`: a blocker vetoed. The caller drops the event wholesale
|
|
912
|
+
* — no `onViewChange`, no window sync, and no `cancel()` either, so
|
|
913
|
+
* an in-flight chain keeps running as if the POP never happened.
|
|
914
|
+
* - `'rewind'`: this POP is the landing of an earlier veto's rewind,
|
|
915
|
+
* back on the entry the router never left. The router state did not
|
|
916
|
+
* change, so the caller re-announces the current view without
|
|
917
|
+
* cancelling the in-flight chain or re-syncing the window state.
|
|
918
|
+
* - `false`: not blocked; the caller handles the POP normally.
|
|
919
|
+
*/
|
|
920
|
+
function blockedPop(router, location, index) {
|
|
921
|
+
const {
|
|
922
|
+
history: history$1
|
|
923
|
+
} = router;
|
|
924
|
+
// A pending rewind's own landing: swallow it without a second query
|
|
925
|
+
// (a blocker that vetoes leaving a page would veto the rewind too)
|
|
926
|
+
// and report it for the no-cancel re-announce branch in the caller.
|
|
927
|
+
// Deliberately not index-matched: a user POP racing the pending
|
|
928
|
+
// rewind must never re-enter the blockers either.
|
|
929
|
+
if (pendingRewind.delete(router)) return 'rewind';
|
|
930
|
+
const set = blockerRegistry.get(router);
|
|
931
|
+
if (!set) return false;
|
|
932
|
+
// Without a settled baseline(unreachable while this listener exists:
|
|
933
|
+
// listen() seeds the tracker before registering) there is neither a
|
|
934
|
+
// `from` nor a rewind delta to work with — let the POP land rather
|
|
935
|
+
// than veto blind.
|
|
936
|
+
const settled = lastSettled.get(router);
|
|
937
|
+
if (!settled) return false;
|
|
938
|
+
const to = history.createPath(location);
|
|
939
|
+
const from = history.createPath(settled.location);
|
|
940
|
+
if (!vetoedBy(set, to, from)) return false;
|
|
941
|
+
// Rewind by the distance the POP travelled. Router-driven pushes keep
|
|
942
|
+
// the state index and the history index in lockstep, so the delta
|
|
943
|
+
// between the landed and settled state indexes doubles as the history
|
|
944
|
+
// delta. A zero delta(same-index POP between stateless external
|
|
945
|
+
// entries) cannot be rewound — `go(0)` goes nowhere — so the URL
|
|
946
|
+
// stays on the vetoed target while the router stacks and the
|
|
947
|
+
// rendered view keep the current entry.
|
|
948
|
+
const delta = index - settled.index;
|
|
949
|
+
if (delta) {
|
|
950
|
+
pendingRewind.set(router, true);
|
|
951
|
+
history$1.go(-delta);
|
|
952
|
+
}
|
|
953
|
+
return 'vetoed';
|
|
954
|
+
}
|
|
955
|
+
|
|
756
956
|
/**
|
|
757
957
|
* Listen the history change.
|
|
758
958
|
* @group Methods
|
|
@@ -765,13 +965,45 @@ function listen(router, onViewChange) {
|
|
|
765
965
|
const {
|
|
766
966
|
history: history$1
|
|
767
967
|
} = router;
|
|
968
|
+
|
|
969
|
+
// Seed the settled-position tracker so a POP arriving before any other
|
|
970
|
+
// history change still reads a correct `from` and rewind delta.
|
|
971
|
+
lastSettled.set(router, {
|
|
972
|
+
index: getHistoryState(router).index,
|
|
973
|
+
location: history$1.location
|
|
974
|
+
});
|
|
768
975
|
const rmListener = history$1.listen(({
|
|
769
976
|
action,
|
|
770
977
|
location
|
|
771
978
|
}) => {
|
|
772
|
-
cancel(router);
|
|
773
979
|
const state = location.state;
|
|
774
980
|
const index = state?.index || 0;
|
|
981
|
+
if (action === 'POP') {
|
|
982
|
+
const blocked = blockedPop(router, location, index);
|
|
983
|
+
// A blocker veto runs before anything else: the POP is rewound
|
|
984
|
+
// with a counter-`go()`, so neither the view nor the in-flight
|
|
985
|
+
// chain may observe it.
|
|
986
|
+
if (blocked === 'vetoed') return;
|
|
987
|
+
if (blocked === 'rewind') {
|
|
988
|
+
// The rewind's landing: an entry the router never left. The
|
|
989
|
+
// stacks and the landed entry's window state are already
|
|
990
|
+
// correct — no sync replace — and no `cancel()` either: an
|
|
991
|
+
// in-flight chain must survive the bounced POP. Re-announce
|
|
992
|
+
// the current view only when a snapshot exists; an
|
|
993
|
+
// invalidate()d slot emits nothing and the host keeps its
|
|
994
|
+
// retained view, exactly like invalidate() itself — a lazy
|
|
995
|
+
// refresh here would supersede the very chain this branch
|
|
996
|
+
// protects.
|
|
997
|
+
const view = viewAt(router, index);
|
|
998
|
+
if (view) onViewChange(view);
|
|
999
|
+
lastSettled.set(router, {
|
|
1000
|
+
index,
|
|
1001
|
+
location
|
|
1002
|
+
});
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
cancel(router);
|
|
775
1007
|
const view = viewAt(router, index);
|
|
776
1008
|
onViewChange(view);
|
|
777
1009
|
if (!view) {
|
|
@@ -790,6 +1022,10 @@ function listen(router, onViewChange) {
|
|
|
790
1022
|
...serializeStack(router)
|
|
791
1023
|
});
|
|
792
1024
|
}
|
|
1025
|
+
lastSettled.set(router, {
|
|
1026
|
+
index,
|
|
1027
|
+
location
|
|
1028
|
+
});
|
|
793
1029
|
});
|
|
794
1030
|
history$1.replace(history.createPath(history$1.location), history$1.location.state);
|
|
795
1031
|
return () => {
|
|
@@ -934,6 +1170,7 @@ exports.getLocation = getLocation;
|
|
|
934
1170
|
exports.getParams = getParams;
|
|
935
1171
|
exports.go = go;
|
|
936
1172
|
exports.initHistoryStack = initHistoryStack;
|
|
1173
|
+
exports.invalidate = invalidate;
|
|
937
1174
|
exports.listen = listen;
|
|
938
1175
|
exports.match = match;
|
|
939
1176
|
exports.mergeMatchedParams = mergeMatchedParams;
|
|
@@ -946,5 +1183,6 @@ exports.refresh = refresh;
|
|
|
946
1183
|
exports.resolve = resolve;
|
|
947
1184
|
exports.resolveEntry = resolveEntry;
|
|
948
1185
|
exports.resolveTo = resolveTo;
|
|
1186
|
+
exports.setBlocker = setBlocker;
|
|
949
1187
|
exports.setOptions = setOptions;
|
|
950
1188
|
exports.toLocation = toLocation;
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createPath, parsePath } from 'history';
|
|
2
2
|
import { match as match$1 } from 'path-to-regexp';
|
|
3
|
-
import { createCurrentGuard, reject
|
|
3
|
+
import { noop, createCurrentGuard, reject } from './util.mjs';
|
|
4
4
|
|
|
5
5
|
/* eslint-disable max-classes-per-file */
|
|
6
6
|
|
|
@@ -110,8 +110,15 @@ function create(routes, history, resolveView, options) {
|
|
|
110
110
|
resolveView,
|
|
111
111
|
history: instanceHistory,
|
|
112
112
|
locationStack,
|
|
113
|
-
// The view stack is
|
|
114
|
-
//
|
|
113
|
+
// The view stack is the SPA-navigation counterpart of the browser's
|
|
114
|
+
// bfcache — a resolved-view snapshot per history entry, restored with
|
|
115
|
+
// zero requests on POP. It is window-relative, so it is exactly as
|
|
116
|
+
// long as the location window and stays bounded by maxStackDepth
|
|
117
|
+
// with it; invalidate() drops these snapshots.
|
|
118
|
+
//
|
|
119
|
+
// viewStack 是 SPA 内导航对应的 bfcache——每个 history 条目一份已解析
|
|
120
|
+
// 视图快照,POP 时零请求还原。它按窗口相对位置存放,与 location 窗口
|
|
121
|
+
// 等长、随 maxStackDepth 一同封顶;invalidate() 丢弃这些快照。
|
|
115
122
|
viewStack: new Array(locationStack.length).fill(null),
|
|
116
123
|
baseIndex,
|
|
117
124
|
preloadCache: new Map(),
|
|
@@ -475,6 +482,15 @@ function preloadCacheOf(router) {
|
|
|
475
482
|
* @param location the location to resolved
|
|
476
483
|
*/
|
|
477
484
|
function commit(router, resolvePromise, location) {
|
|
485
|
+
// Blockers sit at the chain head: a vetoed external commit is dropped
|
|
486
|
+
// before the given task is ever awaited. The dropped task still gets
|
|
487
|
+
// a rejection handler — an orphaned failure(preload tasks re-throw
|
|
488
|
+
// NotFoundError through the default errorHandler) would otherwise
|
|
489
|
+
// surface as an unhandled rejection.
|
|
490
|
+
if (blockedBy(router, createPath(location))) {
|
|
491
|
+
resolvePromise.catch(noop);
|
|
492
|
+
return Promise.resolve();
|
|
493
|
+
}
|
|
478
494
|
// Wrap the raw task so external callers share the guarded entry
|
|
479
495
|
// pipeline; the entry location is the given one, as-is.
|
|
480
496
|
return pushEntry(router, Promise.resolve({
|
|
@@ -529,6 +545,12 @@ function pushEntry(router, entryPromise, fromLocation, ac) {
|
|
|
529
545
|
* @param location the location to resolved
|
|
530
546
|
*/
|
|
531
547
|
function commitReplace(router, resolvePromise, location) {
|
|
548
|
+
// Same chain-head veto as commit: a blocked replace never starts, and
|
|
549
|
+
// the dropped task's failure is swallowed the same way.
|
|
550
|
+
if (blockedBy(router, createPath(location))) {
|
|
551
|
+
resolvePromise.catch(noop);
|
|
552
|
+
return Promise.resolve();
|
|
553
|
+
}
|
|
532
554
|
return replaceEntry(router, Promise.resolve({
|
|
533
555
|
location,
|
|
534
556
|
task: resolvePromise
|
|
@@ -624,7 +646,9 @@ function commitBase(router, entryPromise, location, ac, onResolved) {
|
|
|
624
646
|
/**
|
|
625
647
|
* Navigate to a new path. Route guards(`redirect`/`beforeLoad`) run before
|
|
626
648
|
* the view resolves; the history entry is committed on the terminal
|
|
627
|
-
* location when guards redirected.
|
|
649
|
+
* location when guards redirected. A registered blocker(see {@link
|
|
650
|
+
* setBlocker}) may veto the navigation before anything starts. The guard
|
|
651
|
+
* phase is part of the
|
|
628
652
|
* cancelable navigation: a superseding navigate or a `cancel()` while
|
|
629
653
|
* guards are still running discards this navigation — and aborts the
|
|
630
654
|
* chain's `signal`, so guards and loaders observing it({@link
|
|
@@ -638,6 +662,14 @@ function commitBase(router, entryPromise, location, ac, onResolved) {
|
|
|
638
662
|
*/
|
|
639
663
|
function navigate(router, to, state) {
|
|
640
664
|
const location = toLocation(router, to, state);
|
|
665
|
+
// Blockers sit at the chain head, before the controller exists: a
|
|
666
|
+
// vetoed navigation never resolves a single guard, and its promise
|
|
667
|
+
// resolves immediately — a veto is not an error, and unlike a
|
|
668
|
+
// cancelled navigation(whose promise never settles) it does settle —
|
|
669
|
+
// so the ubiquitous `void navigate(...)` call sites stay untouched.
|
|
670
|
+
// The target is asked in its committed path form(`createPath`), the
|
|
671
|
+
// same string a POP blocker sees, baseUrl included.
|
|
672
|
+
if (blockedBy(router, createPath(location))) return Promise.resolve();
|
|
641
673
|
// One controller per navigation round: guards and view loaders of the
|
|
642
674
|
// whole chain(including redirect hops) share its signal.
|
|
643
675
|
const ac = new AbortController();
|
|
@@ -751,6 +783,174 @@ function initHistoryStack(router) {
|
|
|
751
783
|
});
|
|
752
784
|
}
|
|
753
785
|
|
|
786
|
+
/**
|
|
787
|
+
* Drop every view snapshot of the session window. The already rendered
|
|
788
|
+
* view is untouched — no re-resolve, no re-render; only future POPs
|
|
789
|
+
* change: with no snapshot to hit, {@link listen} falls back to the same
|
|
790
|
+
* lazy re-resolve path as out-of-window entries, so the landed entry's
|
|
791
|
+
* guards(`redirect`/`beforeLoad`) and loaders run again. Call it when
|
|
792
|
+
* the snapshots stop being valid — e.g. right after a logout or an
|
|
793
|
+
* account switch, so a back POP cannot render the previous account's
|
|
794
|
+
* view or bypass guards that already ran in the session.
|
|
795
|
+
*
|
|
796
|
+
* 丢弃会话窗口内的全部视图快照。已渲染的当前视图不受影响——不重解析、
|
|
797
|
+
* 不重渲染;变化的只有后续 POP:无快照可命中时,listen 落入与窗口外条目
|
|
798
|
+
* 相同的惰性重解析路径,落点条目的守卫与加载器重新执行。快照失效时调用
|
|
799
|
+
* ——例如登出/切换账号后,后退 POP 不再渲染上一账号的视图、也不再绕过
|
|
800
|
+
* 会话内已执行过的守卫。
|
|
801
|
+
* @group Methods
|
|
802
|
+
* @category Router
|
|
803
|
+
* @param router router instance
|
|
804
|
+
*/
|
|
805
|
+
function invalidate(router) {
|
|
806
|
+
// Keep the window shape: locationStack stays untouched, so getParams
|
|
807
|
+
// and the serialized window keep working — only the snapshots go.
|
|
808
|
+
router.viewStack = new Array(router.locationStack.length).fill(null);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Navigation blocker predicate: `to` and `from` are path strings
|
|
813
|
+
* (pathname, search and hash included, built with `createPath`). Return
|
|
814
|
+
* `false` to veto the navigation. A blocker that throws counts as a
|
|
815
|
+
* veto too — a crashed gate must not open, and the exception must not
|
|
816
|
+
* escape into a history listener.
|
|
817
|
+
* @group Methods
|
|
818
|
+
* @category Router
|
|
819
|
+
*/
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Registered blockers per router, in registration order. Module-level
|
|
823
|
+
* so the public {@link RouterInstance} type stays untouched; the router
|
|
824
|
+
* key is weakly held, an empty leftover set after the last release
|
|
825
|
+
* leaks nothing.
|
|
826
|
+
*/
|
|
827
|
+
const blockerRegistry = new WeakMap();
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Last settled history position per router, kept in sync by {@link listen}.
|
|
831
|
+
* POP blockers read it as the `from` path and the rewind base: by the
|
|
832
|
+
* time a POP listener runs, `history.location` is already the landed
|
|
833
|
+
* location, so the pre-POP position must be tracked separately.
|
|
834
|
+
*/
|
|
835
|
+
const lastSettled = new WeakMap();
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Pending blocker rewind per router: a rewind `go()` is in flight. The
|
|
839
|
+
* rewind's own POP must not query the blockers again — they would veto
|
|
840
|
+
* it too and ping-pong the history forever.
|
|
841
|
+
*/
|
|
842
|
+
const pendingRewind = new WeakMap();
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Register a navigation blocker. Every {@link navigate}, {@link commit},
|
|
846
|
+
* {@link commitReplace} and every history POP(see {@link listen}) asks
|
|
847
|
+
* the registered blockers(in registration order, first veto wins)
|
|
848
|
+
* before anything else; a vetoed navigation never starts — no guards,
|
|
849
|
+
* no loaders, no history change — and its promise resolves immediately
|
|
850
|
+
* (a veto is not an error; unlike a cancelled navigation, whose promise
|
|
851
|
+
* never settles, a vetoed one does). `refresh` and guard redirects
|
|
852
|
+
* are never blocked: a refresh re-resolves the current location, and a
|
|
853
|
+
* redirect is the guard chain's own target correction, already asked
|
|
854
|
+
* once at the chain head. A vetoed POP is rewound with a
|
|
855
|
+
* counter-`go()`; its landing re-announces the current view without
|
|
856
|
+
* cancelling an in-flight chain.
|
|
857
|
+
* @group Methods
|
|
858
|
+
* @category Router
|
|
859
|
+
* @param router router instance
|
|
860
|
+
* @param fn blocker predicate; `to` is the target path, `from` the
|
|
861
|
+
* current path, both path strings
|
|
862
|
+
* @returns unblock - remove the blocker(idempotent)
|
|
863
|
+
*/
|
|
864
|
+
function setBlocker(router, fn) {
|
|
865
|
+
let set = blockerRegistry.get(router);
|
|
866
|
+
if (!set) {
|
|
867
|
+
set = new Set();
|
|
868
|
+
blockerRegistry.set(router, set);
|
|
869
|
+
}
|
|
870
|
+
set.add(fn);
|
|
871
|
+
let released = false;
|
|
872
|
+
return () => {
|
|
873
|
+
if (released) return;
|
|
874
|
+
released = true;
|
|
875
|
+
set.delete(fn);
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Ask every registered blocker, in registration order. The first veto
|
|
881
|
+
* wins — `some` stops asking at it — and a blocker that throws counts
|
|
882
|
+
* as a veto: a crashed gate must not open, and the exception must not
|
|
883
|
+
* escape into a history listener.
|
|
884
|
+
*/
|
|
885
|
+
function vetoedBy(set, to, from) {
|
|
886
|
+
return Array.from(set).some(block => {
|
|
887
|
+
try {
|
|
888
|
+
return !block(to, from);
|
|
889
|
+
} catch {
|
|
890
|
+
return true;
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Ask the blockers about a router-driven navigation. Runs before
|
|
897
|
+
* anything else, so `history.location` is still the pre-navigation
|
|
898
|
+
* `from`. Returns `true` when any blocker vetoed.
|
|
899
|
+
*/
|
|
900
|
+
function blockedBy(router, to) {
|
|
901
|
+
const set = blockerRegistry.get(router);
|
|
902
|
+
if (!set) return false;
|
|
903
|
+
return vetoedBy(set, to, createPath(router.history.location));
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Ask the blockers about a history POP; rewind it when vetoed.
|
|
908
|
+
* Returns the POP's disposition for {@link listen}:
|
|
909
|
+
* - `'vetoed'`: a blocker vetoed. The caller drops the event wholesale
|
|
910
|
+
* — no `onViewChange`, no window sync, and no `cancel()` either, so
|
|
911
|
+
* an in-flight chain keeps running as if the POP never happened.
|
|
912
|
+
* - `'rewind'`: this POP is the landing of an earlier veto's rewind,
|
|
913
|
+
* back on the entry the router never left. The router state did not
|
|
914
|
+
* change, so the caller re-announces the current view without
|
|
915
|
+
* cancelling the in-flight chain or re-syncing the window state.
|
|
916
|
+
* - `false`: not blocked; the caller handles the POP normally.
|
|
917
|
+
*/
|
|
918
|
+
function blockedPop(router, location, index) {
|
|
919
|
+
const {
|
|
920
|
+
history
|
|
921
|
+
} = router;
|
|
922
|
+
// A pending rewind's own landing: swallow it without a second query
|
|
923
|
+
// (a blocker that vetoes leaving a page would veto the rewind too)
|
|
924
|
+
// and report it for the no-cancel re-announce branch in the caller.
|
|
925
|
+
// Deliberately not index-matched: a user POP racing the pending
|
|
926
|
+
// rewind must never re-enter the blockers either.
|
|
927
|
+
if (pendingRewind.delete(router)) return 'rewind';
|
|
928
|
+
const set = blockerRegistry.get(router);
|
|
929
|
+
if (!set) return false;
|
|
930
|
+
// Without a settled baseline(unreachable while this listener exists:
|
|
931
|
+
// listen() seeds the tracker before registering) there is neither a
|
|
932
|
+
// `from` nor a rewind delta to work with — let the POP land rather
|
|
933
|
+
// than veto blind.
|
|
934
|
+
const settled = lastSettled.get(router);
|
|
935
|
+
if (!settled) return false;
|
|
936
|
+
const to = createPath(location);
|
|
937
|
+
const from = createPath(settled.location);
|
|
938
|
+
if (!vetoedBy(set, to, from)) return false;
|
|
939
|
+
// Rewind by the distance the POP travelled. Router-driven pushes keep
|
|
940
|
+
// the state index and the history index in lockstep, so the delta
|
|
941
|
+
// between the landed and settled state indexes doubles as the history
|
|
942
|
+
// delta. A zero delta(same-index POP between stateless external
|
|
943
|
+
// entries) cannot be rewound — `go(0)` goes nowhere — so the URL
|
|
944
|
+
// stays on the vetoed target while the router stacks and the
|
|
945
|
+
// rendered view keep the current entry.
|
|
946
|
+
const delta = index - settled.index;
|
|
947
|
+
if (delta) {
|
|
948
|
+
pendingRewind.set(router, true);
|
|
949
|
+
history.go(-delta);
|
|
950
|
+
}
|
|
951
|
+
return 'vetoed';
|
|
952
|
+
}
|
|
953
|
+
|
|
754
954
|
/**
|
|
755
955
|
* Listen the history change.
|
|
756
956
|
* @group Methods
|
|
@@ -763,13 +963,45 @@ function listen(router, onViewChange) {
|
|
|
763
963
|
const {
|
|
764
964
|
history
|
|
765
965
|
} = router;
|
|
966
|
+
|
|
967
|
+
// Seed the settled-position tracker so a POP arriving before any other
|
|
968
|
+
// history change still reads a correct `from` and rewind delta.
|
|
969
|
+
lastSettled.set(router, {
|
|
970
|
+
index: getHistoryState(router).index,
|
|
971
|
+
location: history.location
|
|
972
|
+
});
|
|
766
973
|
const rmListener = history.listen(({
|
|
767
974
|
action,
|
|
768
975
|
location
|
|
769
976
|
}) => {
|
|
770
|
-
cancel(router);
|
|
771
977
|
const state = location.state;
|
|
772
978
|
const index = state?.index || 0;
|
|
979
|
+
if (action === 'POP') {
|
|
980
|
+
const blocked = blockedPop(router, location, index);
|
|
981
|
+
// A blocker veto runs before anything else: the POP is rewound
|
|
982
|
+
// with a counter-`go()`, so neither the view nor the in-flight
|
|
983
|
+
// chain may observe it.
|
|
984
|
+
if (blocked === 'vetoed') return;
|
|
985
|
+
if (blocked === 'rewind') {
|
|
986
|
+
// The rewind's landing: an entry the router never left. The
|
|
987
|
+
// stacks and the landed entry's window state are already
|
|
988
|
+
// correct — no sync replace — and no `cancel()` either: an
|
|
989
|
+
// in-flight chain must survive the bounced POP. Re-announce
|
|
990
|
+
// the current view only when a snapshot exists; an
|
|
991
|
+
// invalidate()d slot emits nothing and the host keeps its
|
|
992
|
+
// retained view, exactly like invalidate() itself — a lazy
|
|
993
|
+
// refresh here would supersede the very chain this branch
|
|
994
|
+
// protects.
|
|
995
|
+
const view = viewAt(router, index);
|
|
996
|
+
if (view) onViewChange(view);
|
|
997
|
+
lastSettled.set(router, {
|
|
998
|
+
index,
|
|
999
|
+
location
|
|
1000
|
+
});
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
cancel(router);
|
|
773
1005
|
const view = viewAt(router, index);
|
|
774
1006
|
onViewChange(view);
|
|
775
1007
|
if (!view) {
|
|
@@ -788,6 +1020,10 @@ function listen(router, onViewChange) {
|
|
|
788
1020
|
...serializeStack(router)
|
|
789
1021
|
});
|
|
790
1022
|
}
|
|
1023
|
+
lastSettled.set(router, {
|
|
1024
|
+
index,
|
|
1025
|
+
location
|
|
1026
|
+
});
|
|
791
1027
|
});
|
|
792
1028
|
history.replace(createPath(history.location), history.location.state);
|
|
793
1029
|
return () => {
|
|
@@ -916,4 +1152,4 @@ function isThenable(value) {
|
|
|
916
1152
|
return typeof value?.then === 'function';
|
|
917
1153
|
}
|
|
918
1154
|
|
|
919
|
-
export { NativeRouterError, NotFoundError, RedirectLoopError, SearchError, back, cancel, commit, commitReplace, create, createHref, forward, getCurrentView, getLocation, getParams, go, initHistoryStack, listen, match, mergeMatchedParams, navigate, parseSearch, parseSearchInput, parseSearchSync, preload, refresh, resolve, resolveEntry, resolveTo, setOptions, toLocation };
|
|
1155
|
+
export { NativeRouterError, NotFoundError, RedirectLoopError, SearchError, back, cancel, commit, commitReplace, create, createHref, forward, getCurrentView, getLocation, getParams, go, initHistoryStack, invalidate, listen, match, mergeMatchedParams, navigate, parseSearch, parseSearchInput, parseSearchSync, preload, refresh, resolve, resolveEntry, resolveTo, setBlocker, setOptions, toLocation };
|
package/dist/types/router.d.ts
CHANGED
|
@@ -156,7 +156,9 @@ export declare function commitReplace<R extends BaseRoute = BaseRoute, V = any>(
|
|
|
156
156
|
/**
|
|
157
157
|
* Navigate to a new path. Route guards(`redirect`/`beforeLoad`) run before
|
|
158
158
|
* the view resolves; the history entry is committed on the terminal
|
|
159
|
-
* location when guards redirected.
|
|
159
|
+
* location when guards redirected. A registered blocker(see {@link
|
|
160
|
+
* setBlocker}) may veto the navigation before anything starts. The guard
|
|
161
|
+
* phase is part of the
|
|
160
162
|
* cancelable navigation: a superseding navigate or a `cancel()` while
|
|
161
163
|
* guards are still running discards this navigation — and aborts the
|
|
162
164
|
* chain's `signal`, so guards and loaders observing it({@link
|
|
@@ -230,6 +232,57 @@ export declare function cancel<R extends BaseRoute = BaseRoute, V = any>(router:
|
|
|
230
232
|
* @param router router instance
|
|
231
233
|
*/
|
|
232
234
|
export declare function initHistoryStack<R extends BaseRoute = BaseRoute, V = any>(router: RouterInstance<R, V>): Promise<void>;
|
|
235
|
+
/**
|
|
236
|
+
* Drop every view snapshot of the session window. The already rendered
|
|
237
|
+
* view is untouched — no re-resolve, no re-render; only future POPs
|
|
238
|
+
* change: with no snapshot to hit, {@link listen} falls back to the same
|
|
239
|
+
* lazy re-resolve path as out-of-window entries, so the landed entry's
|
|
240
|
+
* guards(`redirect`/`beforeLoad`) and loaders run again. Call it when
|
|
241
|
+
* the snapshots stop being valid — e.g. right after a logout or an
|
|
242
|
+
* account switch, so a back POP cannot render the previous account's
|
|
243
|
+
* view or bypass guards that already ran in the session.
|
|
244
|
+
*
|
|
245
|
+
* 丢弃会话窗口内的全部视图快照。已渲染的当前视图不受影响——不重解析、
|
|
246
|
+
* 不重渲染;变化的只有后续 POP:无快照可命中时,listen 落入与窗口外条目
|
|
247
|
+
* 相同的惰性重解析路径,落点条目的守卫与加载器重新执行。快照失效时调用
|
|
248
|
+
* ——例如登出/切换账号后,后退 POP 不再渲染上一账号的视图、也不再绕过
|
|
249
|
+
* 会话内已执行过的守卫。
|
|
250
|
+
* @group Methods
|
|
251
|
+
* @category Router
|
|
252
|
+
* @param router router instance
|
|
253
|
+
*/
|
|
254
|
+
export declare function invalidate<R extends BaseRoute = BaseRoute, V = any>(router: RouterInstance<R, V>): void;
|
|
255
|
+
/**
|
|
256
|
+
* Navigation blocker predicate: `to` and `from` are path strings
|
|
257
|
+
* (pathname, search and hash included, built with `createPath`). Return
|
|
258
|
+
* `false` to veto the navigation. A blocker that throws counts as a
|
|
259
|
+
* veto too — a crashed gate must not open, and the exception must not
|
|
260
|
+
* escape into a history listener.
|
|
261
|
+
* @group Methods
|
|
262
|
+
* @category Router
|
|
263
|
+
*/
|
|
264
|
+
export type BlockerFn = (to: string, from: string) => boolean;
|
|
265
|
+
/**
|
|
266
|
+
* Register a navigation blocker. Every {@link navigate}, {@link commit},
|
|
267
|
+
* {@link commitReplace} and every history POP(see {@link listen}) asks
|
|
268
|
+
* the registered blockers(in registration order, first veto wins)
|
|
269
|
+
* before anything else; a vetoed navigation never starts — no guards,
|
|
270
|
+
* no loaders, no history change — and its promise resolves immediately
|
|
271
|
+
* (a veto is not an error; unlike a cancelled navigation, whose promise
|
|
272
|
+
* never settles, a vetoed one does). `refresh` and guard redirects
|
|
273
|
+
* are never blocked: a refresh re-resolves the current location, and a
|
|
274
|
+
* redirect is the guard chain's own target correction, already asked
|
|
275
|
+
* once at the chain head. A vetoed POP is rewound with a
|
|
276
|
+
* counter-`go()`; its landing re-announces the current view without
|
|
277
|
+
* cancelling an in-flight chain.
|
|
278
|
+
* @group Methods
|
|
279
|
+
* @category Router
|
|
280
|
+
* @param router router instance
|
|
281
|
+
* @param fn blocker predicate; `to` is the target path, `from` the
|
|
282
|
+
* current path, both path strings
|
|
283
|
+
* @returns unblock - remove the blocker(idempotent)
|
|
284
|
+
*/
|
|
285
|
+
export declare function setBlocker<R extends BaseRoute = BaseRoute, V = any>(router: RouterInstance<R, V>, fn: BlockerFn): () => void;
|
|
233
286
|
/**
|
|
234
287
|
* Listen the history change.
|
|
235
288
|
* @group Methods
|