@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 CHANGED
@@ -6,7 +6,7 @@
6
6
  "LICENSE"
7
7
  ],
8
8
  "name": "@vobs/router",
9
- "version": "1.0.0",
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.0.0",
18
- "@vobs/runtime": "1.0.0",
19
- "@vobs/vobs": "1.0.0"
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(path: string): void
118
- replace(path: string): void
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
- push(path: string): void {
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
- push(path: string): void {
322
- window.history.pushState(null, '', withBase(normalizeHistoryPath(path), normalizedBase))
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 {