@vobs/router 1.0.0 → 1.1.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/package.json +4 -4
- package/src/index.test.ts +56 -0
- package/src/index.ts +51 -34
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"LICENSE"
|
|
7
7
|
],
|
|
8
8
|
"name": "@vobs/router",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.1.0",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"main": "src/index.ts",
|
|
12
12
|
"types": "src/index.ts",
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
".": "./src/index.ts"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@vobs/reactivity": "1.
|
|
18
|
-
"@vobs/
|
|
19
|
-
"@vobs/
|
|
17
|
+
"@vobs/reactivity": "1.1.0",
|
|
18
|
+
"@vobs/vobs": "1.1.0",
|
|
19
|
+
"@vobs/runtime": "1.1.0"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -40,6 +40,62 @@ describe('@vobs/router', () => {
|
|
|
40
40
|
router.destroy()
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
+
it('导航 state 随 history 条目存取,back 时恢复', async () => {
|
|
44
|
+
const router = createRouter({
|
|
45
|
+
history: createMemoryHistory('/'),
|
|
46
|
+
routes: [
|
|
47
|
+
{ path: '/', name: 'home', component: () => createText('home') },
|
|
48
|
+
{ path: '/result', name: 'result', component: () => createText('result') }
|
|
49
|
+
]
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// push 携带 state(对象目标与字符串目标的透传路径都要覆盖)
|
|
53
|
+
await router.push({ path: '/result', state: { orderId: 'A-1' } })
|
|
54
|
+
expect(router.currentRoute.value.path).toBe('/result')
|
|
55
|
+
expect(router.currentRoute.value.state).toEqual({ orderId: 'A-1' })
|
|
56
|
+
expect(router.history.state).toEqual({ orderId: 'A-1' })
|
|
57
|
+
|
|
58
|
+
await router.push({ path: '/', state: { orderId: 'A-2' } })
|
|
59
|
+
expect(router.currentRoute.value.state).toEqual({ orderId: 'A-2' })
|
|
60
|
+
|
|
61
|
+
// back 回到上一条目,state 一并恢复
|
|
62
|
+
router.history.back()
|
|
63
|
+
await vi.waitFor(() => expect(router.currentRoute.value.path).toBe('/result'))
|
|
64
|
+
expect(router.currentRoute.value.state).toEqual({ orderId: 'A-1' })
|
|
65
|
+
router.destroy()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('replace 更新当前条目的 state,同路径 state-only 变更被去重忽略', async () => {
|
|
69
|
+
const history = createMemoryHistory('/')
|
|
70
|
+
const router = createRouter({
|
|
71
|
+
history,
|
|
72
|
+
routes: [
|
|
73
|
+
{ path: '/', name: 'home', component: () => createText('home') },
|
|
74
|
+
{ path: '/a', name: 'a', component: () => createText('a') },
|
|
75
|
+
{ path: '/b', name: 'b', component: () => createText('b') }
|
|
76
|
+
]
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
await router.push('/a')
|
|
80
|
+
expect(router.currentRoute.value.state).toBeUndefined()
|
|
81
|
+
|
|
82
|
+
// replace 到不同路径:state 写入新条目
|
|
83
|
+
await router.replace({ path: '/b', state: { retried: true } })
|
|
84
|
+
expect(router.currentRoute.value.path).toBe('/b')
|
|
85
|
+
expect(router.currentRoute.value.state).toEqual({ retried: true })
|
|
86
|
+
expect(router.history.state).toEqual({ retried: true })
|
|
87
|
+
|
|
88
|
+
// 与既有“相同目标忽略”语义一致:同路径仅 state 变化不会触发导航
|
|
89
|
+
await router.replace({ path: '/b', state: { retried: false } })
|
|
90
|
+
expect(router.currentRoute.value.state).toEqual({ retried: true })
|
|
91
|
+
|
|
92
|
+
// replace 覆盖了 /a 的条目而非新增:back 直接回到初始 /
|
|
93
|
+
// (若 replace 误作 push,这里会回到 /a)
|
|
94
|
+
router.history.back()
|
|
95
|
+
await vi.waitFor(() => expect(router.currentRoute.value.path).toBe('/'))
|
|
96
|
+
router.destroy()
|
|
97
|
+
})
|
|
98
|
+
|
|
43
99
|
it('支持无路径父路由、嵌套 children、父子 meta 合并和命名导航', () => {
|
|
44
100
|
const Layout = () => createText('layout')
|
|
45
101
|
const router = createRouter({
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface RouteLocation {
|
|
|
37
37
|
readonly meta: RouteMeta
|
|
38
38
|
readonly record: RouteRecord | null
|
|
39
39
|
readonly matched: readonly RouteRecord[]
|
|
40
|
+
readonly state?: unknown
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export interface RouteComponentProps {
|
|
@@ -84,6 +85,7 @@ export interface RouteLocationRaw {
|
|
|
84
85
|
readonly params?: Record<string, unknown>
|
|
85
86
|
readonly query?: RouteQueryInput
|
|
86
87
|
readonly hash?: string
|
|
88
|
+
readonly state?: unknown
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
export type RouteTarget = string | RouteLocationRaw
|
|
@@ -114,10 +116,12 @@ export class NavigationRedirectError extends Error {
|
|
|
114
116
|
|
|
115
117
|
export interface RouterHistory {
|
|
116
118
|
readonly location: string
|
|
117
|
-
push
|
|
118
|
-
|
|
119
|
+
/** 当前 history 条目携带的导航 state(push/replace 时写入,popstate/初始启动时回读)。 */
|
|
120
|
+
readonly state?: unknown
|
|
121
|
+
push(path: string, state?: unknown): void
|
|
122
|
+
replace(path: string, state?: unknown): void
|
|
119
123
|
back(): void
|
|
120
|
-
listen(listener: (path: string) => void): () => void
|
|
124
|
+
listen(listener: (path: string, state: unknown) => void): () => void
|
|
121
125
|
}
|
|
122
126
|
|
|
123
127
|
export interface RouterOptions {
|
|
@@ -269,33 +273,37 @@ export function lazy(loader: RouteComponentLoader): LazyRouteComponent {
|
|
|
269
273
|
}
|
|
270
274
|
|
|
271
275
|
export function createMemoryHistory(initial = '/'): RouterHistory {
|
|
272
|
-
let entries = [normalizeHistoryPath(initial)]
|
|
276
|
+
let entries = [{ path: normalizeHistoryPath(initial), state: undefined as unknown }]
|
|
273
277
|
let index = 0
|
|
274
|
-
const listeners = new Set<(path: string) => void>()
|
|
278
|
+
const listeners = new Set<(path: string, state: unknown) => void>()
|
|
275
279
|
|
|
276
280
|
return {
|
|
277
281
|
get location(): string {
|
|
278
|
-
return entries[index]
|
|
282
|
+
return entries[index]!.path
|
|
279
283
|
},
|
|
280
284
|
|
|
281
|
-
|
|
285
|
+
get state(): unknown {
|
|
286
|
+
return entries[index]!.state
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
push(path: string, state?: unknown): void {
|
|
282
290
|
const next = normalizeHistoryPath(path)
|
|
283
291
|
entries = entries.slice(0, index + 1)
|
|
284
|
-
entries.push(next)
|
|
292
|
+
entries.push({ path: next, state })
|
|
285
293
|
index++
|
|
286
294
|
},
|
|
287
295
|
|
|
288
|
-
replace(path: string): void {
|
|
289
|
-
entries[index] = normalizeHistoryPath(path)
|
|
296
|
+
replace(path: string, state?: unknown): void {
|
|
297
|
+
entries[index] = { path: normalizeHistoryPath(path), state }
|
|
290
298
|
},
|
|
291
299
|
|
|
292
300
|
back(): void {
|
|
293
301
|
if (index === 0) return
|
|
294
302
|
index--
|
|
295
|
-
notifyListeners(listeners, entries[index])
|
|
303
|
+
notifyListeners(listeners, entries[index]!.path, entries[index]!.state)
|
|
296
304
|
},
|
|
297
305
|
|
|
298
|
-
listen(listener: (path: string) => void): () => void {
|
|
306
|
+
listen(listener: (path: string, state: unknown) => void): () => void {
|
|
299
307
|
listeners.add(listener)
|
|
300
308
|
return () => listeners.delete(listener)
|
|
301
309
|
}
|
|
@@ -308,9 +316,9 @@ export function createBrowserHistory(base = ''): RouterHistory {
|
|
|
308
316
|
}
|
|
309
317
|
|
|
310
318
|
const normalizedBase = normalizeBase(base)
|
|
311
|
-
const listeners = new Set<(path: string) => void>()
|
|
312
|
-
const onPopState = (): void => {
|
|
313
|
-
notifyListeners(listeners, readBrowserLocation(normalizedBase))
|
|
319
|
+
const listeners = new Set<(path: string, state: unknown) => void>()
|
|
320
|
+
const onPopState = (event: PopStateEvent): void => {
|
|
321
|
+
notifyListeners(listeners, readBrowserLocation(normalizedBase), event.state)
|
|
314
322
|
}
|
|
315
323
|
|
|
316
324
|
return {
|
|
@@ -318,19 +326,23 @@ export function createBrowserHistory(base = ''): RouterHistory {
|
|
|
318
326
|
return readBrowserLocation(normalizedBase)
|
|
319
327
|
},
|
|
320
328
|
|
|
321
|
-
|
|
322
|
-
window.history.
|
|
329
|
+
get state(): unknown {
|
|
330
|
+
return window.history.state
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
push(path: string, state?: unknown): void {
|
|
334
|
+
window.history.pushState(state ?? null, '', withBase(normalizeHistoryPath(path), normalizedBase))
|
|
323
335
|
},
|
|
324
336
|
|
|
325
|
-
replace(path: string): void {
|
|
326
|
-
window.history.replaceState(null, '', withBase(normalizeHistoryPath(path), normalizedBase))
|
|
337
|
+
replace(path: string, state?: unknown): void {
|
|
338
|
+
window.history.replaceState(state ?? null, '', withBase(normalizeHistoryPath(path), normalizedBase))
|
|
327
339
|
},
|
|
328
340
|
|
|
329
341
|
back(): void {
|
|
330
342
|
window.history.back()
|
|
331
343
|
},
|
|
332
344
|
|
|
333
|
-
listen(listener: (path: string) => void): () => void {
|
|
345
|
+
listen(listener: (path: string, state: unknown) => void): () => void {
|
|
334
346
|
if (listeners.size === 0) window.addEventListener('popstate', onPopState)
|
|
335
347
|
listeners.add(listener)
|
|
336
348
|
return () => {
|
|
@@ -368,10 +380,10 @@ export function createRouter(options: RouterOptions): Router {
|
|
|
368
380
|
function resolve(to: RouteTarget): RouteLocation {
|
|
369
381
|
ensureActive()
|
|
370
382
|
const target = typeof to === 'string' ? parseTargetString(to) : normalizeTarget(to, matchers)
|
|
371
|
-
return resolvePath(buildTargetPath(target.path, target.query, target.hash))
|
|
383
|
+
return resolvePath(buildTargetPath(target.path, target.query, target.hash), target.state)
|
|
372
384
|
}
|
|
373
385
|
|
|
374
|
-
function resolvePath(rawPath: string): RouteLocation {
|
|
386
|
+
function resolvePath(rawPath: string, state?: unknown): RouteLocation {
|
|
375
387
|
const parsed = parseTargetString(rawPath)
|
|
376
388
|
const matched = matchers.find(matcher => matcher.regex.exec(parsed.path))
|
|
377
389
|
const params = matched ? extractParams(matched, parsed.path) : {}
|
|
@@ -387,19 +399,23 @@ export function createRouter(options: RouterOptions): Router {
|
|
|
387
399
|
name: record?.name,
|
|
388
400
|
meta: matched?.meta ?? {},
|
|
389
401
|
record,
|
|
390
|
-
matched: matched?.chain ?? EMPTY_MATCHED
|
|
402
|
+
matched: matched?.chain ?? EMPTY_MATCHED,
|
|
403
|
+
state
|
|
391
404
|
}
|
|
392
405
|
}
|
|
393
406
|
|
|
394
407
|
async function navigate(
|
|
395
408
|
to: RouteTarget,
|
|
396
409
|
replaceHistory: boolean,
|
|
397
|
-
fromHistory: boolean
|
|
410
|
+
fromHistory: boolean,
|
|
411
|
+
historyState?: unknown
|
|
398
412
|
): Promise<RouteLocation | false> {
|
|
399
413
|
ensureActive()
|
|
400
414
|
const id = ++navigationId
|
|
401
415
|
const from = currentRoute.value
|
|
402
416
|
let target = resolve(to)
|
|
417
|
+
// popstate 回读的 state 保存在 history 条目上,不在目标描述里,这里回填。
|
|
418
|
+
if (fromHistory && historyState !== undefined) target = { ...target, state: historyState }
|
|
403
419
|
const source: NavigationTrace['source'] = fromHistory ? 'history' : replaceHistory ? 'replace' : 'push'
|
|
404
420
|
const startedAt = now()
|
|
405
421
|
const initialTarget = target.fullPath
|
|
@@ -470,8 +486,8 @@ export function createRouter(options: RouterOptions): Router {
|
|
|
470
486
|
|
|
471
487
|
if (target.fullPath === from.fullPath) return from
|
|
472
488
|
if (!fromHistory) {
|
|
473
|
-
if (replaceHistory) history.replace(target.fullPath)
|
|
474
|
-
else history.push(target.fullPath)
|
|
489
|
+
if (replaceHistory) history.replace(target.fullPath, target.state)
|
|
490
|
+
else history.push(target.fullPath, target.state)
|
|
475
491
|
}
|
|
476
492
|
currentRoute.value = target
|
|
477
493
|
recordNavigation({ id, from: from.fullPath, to: target.fullPath, status: 'success', source, startedAt, endedAt: now(), duration: now() - startedAt, redirect: target.fullPath !== initialTarget ? target.fullPath : undefined })
|
|
@@ -615,13 +631,13 @@ export function createRouter(options: RouterOptions): Router {
|
|
|
615
631
|
return buildRouteDebugTree(options.routes, statuses)
|
|
616
632
|
}
|
|
617
633
|
|
|
618
|
-
function handleHistoryNavigation(path: string): void {
|
|
619
|
-
void navigate(path, false, true).then(result => {
|
|
634
|
+
function handleHistoryNavigation(path: string, state: unknown): void {
|
|
635
|
+
void navigate(path, false, true, state).then(result => {
|
|
620
636
|
if (destroyed) return
|
|
621
637
|
if (result === false) {
|
|
622
|
-
history.replace(currentRoute.value.fullPath)
|
|
638
|
+
history.replace(currentRoute.value.fullPath, currentRoute.value.state)
|
|
623
639
|
} else if (result.fullPath !== normalizeHistoryPath(path)) {
|
|
624
|
-
history.replace(result.fullPath)
|
|
640
|
+
history.replace(result.fullPath, result.state)
|
|
625
641
|
}
|
|
626
642
|
}).catch(error => {
|
|
627
643
|
if (!(error instanceof NavigationCancelledError) && !destroyed) {
|
|
@@ -630,7 +646,7 @@ export function createRouter(options: RouterOptions): Router {
|
|
|
630
646
|
reportError('navigation', failed, normalizeHistoryPath(path))
|
|
631
647
|
navigationState = { status: 'error', from: current, to: normalizeHistoryPath(path), error: failed.message }
|
|
632
648
|
emitRouter('navigation:end', { status: 'error', from: current, to: normalizeHistoryPath(path), error: failed.message })
|
|
633
|
-
history.replace(currentRoute.value.fullPath)
|
|
649
|
+
history.replace(currentRoute.value.fullPath, currentRoute.value.state)
|
|
634
650
|
}
|
|
635
651
|
})
|
|
636
652
|
}
|
|
@@ -872,6 +888,7 @@ interface ParsedTarget {
|
|
|
872
888
|
readonly path: string
|
|
873
889
|
readonly query: RouteQuery
|
|
874
890
|
readonly hash: string
|
|
891
|
+
readonly state?: unknown
|
|
875
892
|
}
|
|
876
893
|
|
|
877
894
|
function defaultHistory(): RouterHistory {
|
|
@@ -1024,7 +1041,7 @@ function normalizeTarget(target: RouteLocationRaw, matchers: readonly RouteMatch
|
|
|
1024
1041
|
const filledPath = fillRouteParams(parsed.path, target.params ?? {})
|
|
1025
1042
|
const query = target.query === undefined ? parsed.query : normalizeQuery(target.query)
|
|
1026
1043
|
const hash = target.hash === undefined ? parsed.hash : normalizeHash(target.hash)
|
|
1027
|
-
return { path: filledPath, query, hash }
|
|
1044
|
+
return { path: filledPath, query, hash, state: target.state }
|
|
1028
1045
|
}
|
|
1029
1046
|
|
|
1030
1047
|
function fillRouteParams(path: string, params: Record<string, unknown>): string {
|
|
@@ -1110,8 +1127,8 @@ function withBase(path: string, base: string): string {
|
|
|
1110
1127
|
return `${base}${path === '/' ? '/' : path}` || '/'
|
|
1111
1128
|
}
|
|
1112
1129
|
|
|
1113
|
-
function notifyListeners(listeners: Set<(path: string) => void>, path: string): void {
|
|
1114
|
-
for (const listener of [...listeners]) listener(path)
|
|
1130
|
+
function notifyListeners(listeners: Set<(path: string, state: unknown) => void>, path: string, state: unknown): void {
|
|
1131
|
+
for (const listener of [...listeners]) listener(path, state)
|
|
1115
1132
|
}
|
|
1116
1133
|
|
|
1117
1134
|
function escapeRegExp(value: string): string {
|