vitarx-router 4.0.0-beta.2 → 4.0.0-beta.21

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.
Files changed (93) hide show
  1. package/README.md +42 -17
  2. package/dist/{plugin-vite/auto-routes → auto-routes}/handleHotUpdate.d.ts +1 -1
  3. package/dist/components/RouterView.js +5 -4
  4. package/dist/core/common/constant.d.ts +5 -6
  5. package/dist/core/common/constant.js +5 -6
  6. package/dist/core/common/utils.js +2 -1
  7. package/dist/core/router/checkOptions.d.ts +11 -0
  8. package/dist/core/router/checkOptions.js +119 -0
  9. package/dist/core/router/manager.js +27 -23
  10. package/dist/core/router/router.d.ts +154 -1
  11. package/dist/core/router/router.js +303 -230
  12. package/dist/core/router/web.d.ts +13 -0
  13. package/dist/core/router/web.js +35 -4
  14. package/dist/core/shared/link.d.ts +7 -0
  15. package/dist/core/shared/link.js +11 -8
  16. package/dist/core/shared/route.js +1 -2
  17. package/dist/core/shared/router.d.ts +3 -3
  18. package/dist/core/shared/router.js +7 -4
  19. package/dist/core/types/options.d.ts +2 -0
  20. package/dist/file-router/config/index.d.ts +2 -1
  21. package/dist/file-router/config/index.js +2 -1
  22. package/dist/file-router/config/resolve.d.ts +43 -0
  23. package/dist/file-router/config/resolve.js +69 -0
  24. package/dist/file-router/{utils/validateOptions.d.ts → config/validate.d.ts} +11 -10
  25. package/dist/file-router/config/validate.js +280 -0
  26. package/dist/file-router/constants.d.ts +12 -2
  27. package/dist/file-router/constants.js +13 -3
  28. package/dist/file-router/generator/generateRoutes.d.ts +44 -13
  29. package/dist/file-router/generator/generateRoutes.js +159 -80
  30. package/dist/file-router/generator/generateTypes.d.ts +3 -29
  31. package/dist/file-router/generator/generateTypes.js +36 -41
  32. package/dist/file-router/global.d.ts +1 -1
  33. package/dist/file-router/index.d.ts +224 -90
  34. package/dist/file-router/index.js +571 -135
  35. package/dist/file-router/macros/astValueExtractor.d.ts +1 -1
  36. package/dist/file-router/macros/astValueExtractor.js +27 -7
  37. package/dist/file-router/macros/definePage.d.ts +20 -3
  38. package/dist/file-router/macros/definePage.js +120 -40
  39. package/dist/file-router/parser/exportChecker.d.ts +4 -23
  40. package/dist/file-router/parser/exportChecker.js +38 -79
  41. package/dist/file-router/parser/filterUtils.d.ts +25 -0
  42. package/dist/file-router/parser/filterUtils.js +43 -0
  43. package/dist/file-router/parser/index.d.ts +2 -1
  44. package/dist/file-router/parser/index.js +2 -1
  45. package/dist/file-router/parser/parsePage.d.ts +56 -9
  46. package/dist/file-router/parser/parsePage.js +194 -172
  47. package/dist/file-router/parser/routePath.d.ts +22 -0
  48. package/dist/file-router/parser/routePath.js +74 -0
  49. package/dist/file-router/types/hooks.d.ts +52 -0
  50. package/dist/file-router/types/index.d.ts +3 -0
  51. package/dist/file-router/types/index.js +1 -0
  52. package/dist/file-router/types/options.d.ts +279 -0
  53. package/dist/file-router/types/options.js +1 -0
  54. package/dist/file-router/types/route.d.ts +114 -0
  55. package/dist/file-router/types/route.js +1 -0
  56. package/dist/file-router/utils/fileReader.d.ts +11 -0
  57. package/dist/file-router/utils/fileReader.js +22 -0
  58. package/dist/file-router/utils/findRoute.d.ts +8 -0
  59. package/dist/file-router/utils/findRoute.js +22 -0
  60. package/dist/file-router/utils/index.d.ts +4 -2
  61. package/dist/file-router/utils/index.js +4 -2
  62. package/dist/file-router/utils/logger.d.ts +6 -6
  63. package/dist/file-router/utils/logger.js +44 -4
  64. package/dist/file-router/utils/pathStrategy.d.ts +28 -0
  65. package/dist/file-router/utils/{namingStrategy.js → pathStrategy.js} +18 -28
  66. package/dist/file-router/utils/pathUtils.d.ts +31 -0
  67. package/dist/file-router/utils/pathUtils.js +53 -1
  68. package/dist/plugin-vite/constant.d.ts +9 -0
  69. package/dist/plugin-vite/constant.js +9 -0
  70. package/dist/plugin-vite/index.d.ts +4 -24
  71. package/dist/plugin-vite/index.js +4 -94
  72. package/dist/plugin-vite/plugin.d.ts +86 -0
  73. package/dist/plugin-vite/plugin.js +181 -0
  74. package/dist/plugin-vite/watcher.d.ts +15 -0
  75. package/dist/plugin-vite/watcher.js +65 -0
  76. package/package.json +9 -7
  77. package/dist/file-router/config/configUtils.d.ts +0 -54
  78. package/dist/file-router/config/configUtils.js +0 -88
  79. package/dist/file-router/scanner/filterUtils.d.ts +0 -35
  80. package/dist/file-router/scanner/filterUtils.js +0 -188
  81. package/dist/file-router/scanner/index.d.ts +0 -8
  82. package/dist/file-router/scanner/index.js +0 -8
  83. package/dist/file-router/scanner/routeTreeBuilder.d.ts +0 -21
  84. package/dist/file-router/scanner/routeTreeBuilder.js +0 -312
  85. package/dist/file-router/scanner/scanPages.d.ts +0 -48
  86. package/dist/file-router/scanner/scanPages.js +0 -174
  87. package/dist/file-router/types.d.ts +0 -344
  88. package/dist/file-router/utils/namingStrategy.d.ts +0 -57
  89. package/dist/file-router/utils/validateOptions.js +0 -233
  90. /package/dist/{plugin-vite/auto-routes → auto-routes}/handleHotUpdate.js +0 -0
  91. /package/dist/{plugin-vite/auto-routes → auto-routes}/index.d.ts +0 -0
  92. /package/dist/{plugin-vite/auto-routes → auto-routes}/index.js +0 -0
  93. /package/dist/file-router/{types.js → types/hooks.js} +0 -0
package/README.md CHANGED
@@ -10,7 +10,7 @@ Vitarx 前端框架官方路由解决方案,提供声明式路由配置、导
10
10
  ## 特性
11
11
 
12
12
  - 🚀 **多种路由模式** - 支持 Hash、History、Memory 三种路由模式
13
- - 📁 **文件路由** - 基于 Vite 插件的文件系统路由自动生成
13
+ - 📁 **文件路由** - 基于 Vite 插件的文件系统路由自动生成,HMR 热更新支持
14
14
  - 🔒 **导航守卫** - 完整的路由守卫机制,支持权限控制
15
15
  - 🔄 **动态路由** - 支持动态参数、正则约束、可选参数
16
16
  - 📦 **懒加载** - 内置组件懒加载支持
@@ -112,6 +112,25 @@ import { createMemoryRouter } from 'vitarx-router'
112
112
  createMemoryRouter({ routes })
113
113
  ```
114
114
 
115
+ ### 手动初始化
116
+
117
+ 默认情况下,`createRouter` / `createWebRouter` 会在创建实例后自动初始化路由器(执行初始导航并注册浏览器事件监听)。如果需要延迟初始化,可以使用 `createWebRouter` 并将第二个参数 `autoInit` 设为 `false`,然后手动调用 `init()` 方法。
118
+
119
+ ```typescript
120
+ const router = createWebRouter({ routes }, false)
121
+
122
+ // 在合适的时机手动初始化
123
+ router.init()
124
+ ```
125
+
126
+ **适用场景:**
127
+
128
+ - 需要在初始化前注册导航守卫
129
+ - 需要在初始化前完成异步配置加载
130
+ - 需要精确控制初始化时机
131
+
132
+ > **注意:** `autoInit` 仅对 Web 路由器有效。
133
+
115
134
  ## 路由配置
116
135
 
117
136
  ### 基本路由
@@ -403,7 +422,10 @@ import VitarxRouter from 'vitarx-router/vite'
403
422
  export default defineConfig({
404
423
  plugins: [
405
424
  VitarxRouter({
406
- pagesDir: 'src/pages'
425
+ pages: 'src/pages',
426
+ pathStrategy: 'kebab',
427
+ importMode: 'lazy',
428
+ dts: 'router.d.ts'
407
429
  })
408
430
  ]
409
431
  })
@@ -418,7 +440,9 @@ import { createRouter } from 'vitarx-router'
418
440
  export const router = createRouter({ routes })
419
441
  ```
420
442
 
421
- 详细配置请参考 [Vite 插件文档](src/plugin-vite/README.md)。
443
+ ### 文件路由配置选项
444
+
445
+ 请参考 [File Router 文档](src/file-router/README.md)。
422
446
 
423
447
  ## TypeScript 支持
424
448
 
@@ -463,20 +487,21 @@ declare module 'vitarx-router' {
463
487
 
464
488
  ### Router 实例方法
465
489
 
466
- | 方法 | 说明 |
467
- |----------------------------|-----------|
468
- | `push(target)` | 跳转到新路由 |
469
- | `replace(target)` | 替换当前路由 |
470
- | `go(delta)` | 前进/后退指定步数 |
471
- | `back()` | 后退一步 |
472
- | `forward()` | 前进一步 |
473
- | `addRoute(route, parent?)` | 添加路由 |
474
- | `removeRoute(index)` | 移除路由 |
475
- | `hasRoute(index)` | 检查路由是否存在 |
476
- | `matchRoute(target)` | 匹配路由 |
477
- | `beforeEach(guard)` | 添加前置守卫 |
478
- | `afterEach(callback)` | 添加后置回调 |
479
- | `destroy()` | 销毁路由器 |
490
+ | 方法 | 说明 |
491
+ |----------------------------|-----------------------|
492
+ | `init()` | 手动初始化路由器(仅 WebRouter) |
493
+ | `push(target)` | 跳转到新路由 |
494
+ | `replace(target)` | 替换当前路由 |
495
+ | `go(delta)` | 前进/后退指定步数 |
496
+ | `back()` | 后退一步 |
497
+ | `forward()` | 前进一步 |
498
+ | `addRoute(route, parent?)` | 添加路由 |
499
+ | `removeRoute(index)` | 移除路由 |
500
+ | `hasRoute(index)` | 检查路由是否存在 |
501
+ | `matchRoute(target)` | 匹配路由 |
502
+ | `beforeEach(guard)` | 添加前置守卫 |
503
+ | `afterEach(callback)` | 添加后置回调 |
504
+ | `destroy()` | 销毁路由器 |
480
505
 
481
506
  ### Router 实例属性
482
507
 
@@ -1,4 +1,4 @@
1
- import type { Route, Router } from '../../core/index.js';
1
+ import type { Route, Router } from '../core/index.js';
2
2
  /**
3
3
  * 处理路由热更新函数
4
4
  *
@@ -12,7 +12,7 @@ import { __ROUTER_VIEW_DEPTH_KEY__, useRouter } from '../core/index.js';
12
12
  * @returns {View} 返回渲染的视图
13
13
  */
14
14
  export function RouterView(props) {
15
- const { children, name = 'default' } = props;
15
+ const { children } = props;
16
16
  // 获取路由实例
17
17
  const router = useRouter();
18
18
  // 获取父级 index
@@ -20,6 +20,7 @@ export function RouterView(props) {
20
20
  const index = parentIndex + 1; // 计算当前视图的索引
21
21
  provide(__ROUTER_VIEW_DEPTH_KEY__, index); // 向子组件提供当前索引
22
22
  // 匹配的路由线路
23
+ const viewName = computed(() => props.name || 'default');
23
24
  const matchedRoute = computed(() => {
24
25
  return router.route.matched[index] ?? null;
25
26
  });
@@ -28,8 +29,7 @@ export function RouterView(props) {
28
29
  const currentRoute = matchedRoute.value;
29
30
  if (!currentRoute)
30
31
  return null;
31
- const name = props.name || 'default'; // 获取视图名称,默认为 'default'
32
- let injectProps = currentRoute.props?.[name] ?? router.config.props ?? false;
32
+ let injectProps = currentRoute.props?.[viewName.value] ?? router.config.props ?? true;
33
33
  if (injectProps === false)
34
34
  return null; // 如果属性为 false,返回null
35
35
  if (injectProps === true && currentRoute.pattern) {
@@ -52,7 +52,7 @@ export function RouterView(props) {
52
52
  const route = matchedRoute.value; // 获取匹配的路由记录
53
53
  if (!route)
54
54
  return null;
55
- return route.component?.[name] ?? null;
55
+ return route.component?.[viewName.value] ?? null;
56
56
  });
57
57
  // 如果传入了 children 函数,则调用并返回其结果
58
58
  if (isFunction(children)) {
@@ -61,6 +61,7 @@ export function RouterView(props) {
61
61
  }
62
62
  catch (e) {
63
63
  logger.error('[RouterView] Error occurred while executing children function', e);
64
+ return createCommentView(`router-view:error`);
64
65
  }
65
66
  }
66
67
  let lastView = null;
@@ -2,12 +2,11 @@
2
2
  * 导航状态
3
3
  *
4
4
  * 枚举值:
5
- * 0. success: 导航成功
6
- * 1. aborted: 导航被阻止
7
- * 2. cancelled: 导航被取消
8
- * 3. duplicated: 重复导航
9
- * 4. notfound: 路由未匹配
10
- * 5. exception: 捕获到异常
5
+ * 1. success: 导航成功
6
+ * 2. aborted: 导航被阻止
7
+ * 4. cancelled: 导航被取消
8
+ * 8. duplicated: 重复导航
9
+ * 16. notfound: 路由未匹配
11
10
  */
12
11
  export declare enum NavState {
13
12
  /**
@@ -2,12 +2,11 @@
2
2
  * 导航状态
3
3
  *
4
4
  * 枚举值:
5
- * 0. success: 导航成功
6
- * 1. aborted: 导航被阻止
7
- * 2. cancelled: 导航被取消
8
- * 3. duplicated: 重复导航
9
- * 4. notfound: 路由未匹配
10
- * 5. exception: 捕获到异常
5
+ * 1. success: 导航成功
6
+ * 2. aborted: 导航被阻止
7
+ * 4. cancelled: 导航被取消
8
+ * 8. duplicated: 重复导航
9
+ * 16. notfound: 路由未匹配
11
10
  */
12
11
  export var NavState;
13
12
  (function (NavState) {
@@ -108,7 +108,8 @@ export function resolveNavTarget(index) {
108
108
  return {
109
109
  index: index.matched.at(-1)?.name || index.path, // 使用最后一个匹配的路由名称,如果没有则使用路径
110
110
  params: index.params, // 路由参数
111
- query: index.query // 查询参数
111
+ query: index.query, // 查询参数
112
+ hash: index.hash // 哈希值
112
113
  };
113
114
  }
114
115
  throw new Error('Invalid navigation target');
@@ -0,0 +1,11 @@
1
+ import type { RouterOptions } from '../types/index.js';
2
+ /**
3
+ * 检查路由器配置选项的合法性
4
+ *
5
+ * 在开发环境下对用户传入的 RouterOptions 进行校验,
6
+ * 确保必填字段存在、类型正确、值合法。
7
+ *
8
+ * @param options - 用户传入的路由器配置对象
9
+ * @throws {Error} 当选项不合法时抛出错误
10
+ */
11
+ export declare const checkRouterOptions: (options: RouterOptions) => void;
@@ -0,0 +1,119 @@
1
+ import { isComponent, isFunction } from 'vitarx';
2
+ import { RouteManager } from './manager.js';
3
+ /**
4
+ * 检查路由器配置选项的合法性
5
+ *
6
+ * 在开发环境下对用户传入的 RouterOptions 进行校验,
7
+ * 确保必填字段存在、类型正确、值合法。
8
+ *
9
+ * @param options - 用户传入的路由器配置对象
10
+ * @throws {Error} 当选项不合法时抛出错误
11
+ */
12
+ export const checkRouterOptions = (options) => {
13
+ // 1. 检查 options 是否为对象
14
+ if (typeof options !== 'object' || options === null) {
15
+ throw new Error('[Router] Router options must be an object.');
16
+ }
17
+ // 2. 检查 routes 是否存在且为有效类型
18
+ if (!('routes' in options) || options.routes === undefined) {
19
+ throw new Error('[Router] "routes" is a required option.');
20
+ }
21
+ if (!Array.isArray(options.routes) && !(options.routes instanceof RouteManager)) {
22
+ throw new Error('[Router] "routes" must be an array or a RouteManager instance.');
23
+ }
24
+ // 3. 检查 mode 的值是否合法
25
+ if ('mode' in options && options.mode !== undefined) {
26
+ const validModes = ['hash', 'path'];
27
+ if (!validModes.includes(options.mode)) {
28
+ throw new Error(`[Router] "mode" must be one of: ${validModes.join(', ')}. Received "${options.mode}".`);
29
+ }
30
+ }
31
+ // 4. 检查 base 的格式
32
+ if ('base' in options && options.base !== undefined) {
33
+ if (typeof options.base !== 'string') {
34
+ throw new Error('[Router] "base" must be a string.');
35
+ }
36
+ if (!options.base.startsWith('/')) {
37
+ throw new Error('[Router] "base" must start with a slash (/).');
38
+ }
39
+ }
40
+ // 5. 检查 suffix 的格式
41
+ if ('suffix' in options && options.suffix !== undefined) {
42
+ if (typeof options.suffix !== 'string') {
43
+ throw new Error('[Router] "suffix" must be a string.');
44
+ }
45
+ if (!options.suffix.startsWith('.')) {
46
+ throw new Error('[Router] "suffix" must start with a dot (.).');
47
+ }
48
+ if (options.suffix === '.') {
49
+ throw new Error('[Router] "suffix" cannot be just a dot, please provide a valid extension like ".html".');
50
+ }
51
+ }
52
+ // 6. 检查 props 的类型
53
+ if ('props' in options && options.props !== undefined) {
54
+ if (typeof options.props !== 'boolean' && !isFunction(options.props)) {
55
+ throw new Error('[Router] "props" must be a boolean or function.');
56
+ }
57
+ }
58
+ // 7. 检查 scrollBehavior 的类型
59
+ if ('scrollBehavior' in options && options.scrollBehavior !== undefined) {
60
+ if (!isFunction(options.scrollBehavior)) {
61
+ throw new Error('[Router] "scrollBehavior" must be a function.');
62
+ }
63
+ }
64
+ // 8. 检查钩子函数的类型
65
+ if ('beforeEach' in options && options.beforeEach !== undefined) {
66
+ if (!isFunction(options.beforeEach) && !Array.isArray(options.beforeEach)) {
67
+ throw new Error('[Router] "beforeEach" must be a function or an array of functions.');
68
+ }
69
+ if (Array.isArray(options.beforeEach)) {
70
+ options.beforeEach.forEach((hook, index) => {
71
+ if (!isFunction(hook)) {
72
+ throw new Error(`[Router] "beforeEach" must be a function or an array of functions. Index: ${index}`);
73
+ }
74
+ });
75
+ }
76
+ }
77
+ if ('afterEach' in options && options.afterEach !== undefined) {
78
+ if (!isFunction(options.afterEach) && !Array.isArray(options.afterEach)) {
79
+ throw new Error('[Router] "afterEach" must be a function or an array of functions.');
80
+ }
81
+ if (Array.isArray(options.afterEach)) {
82
+ options.afterEach.forEach((hook, index) => {
83
+ if (!isFunction(hook)) {
84
+ throw new Error(`[Router] "afterEach" must be a function or an array of functions. Index: ${index}`);
85
+ }
86
+ });
87
+ }
88
+ }
89
+ if ('onNotFound' in options && options.onNotFound !== undefined) {
90
+ if (!isFunction(options.onNotFound) && !Array.isArray(options.onNotFound)) {
91
+ throw new Error('[Router] "onNotFound" must be a function or an array of functions.');
92
+ }
93
+ if (Array.isArray(options.onNotFound)) {
94
+ options.onNotFound.forEach((hook, index) => {
95
+ if (!isFunction(hook)) {
96
+ throw new Error(`[Router] "onNotFound" must be a function or an array of functions. Index: ${index}`);
97
+ }
98
+ });
99
+ }
100
+ }
101
+ if ('onError' in options && options.onError !== undefined) {
102
+ if (!isFunction(options.onError) && !Array.isArray(options.onError)) {
103
+ throw new Error('[Router] "onError" must be a function or an array of functions.');
104
+ }
105
+ if (Array.isArray(options.onError)) {
106
+ options.onError.forEach((hook, index) => {
107
+ if (!isFunction(hook)) {
108
+ throw new Error(`[Router] "onError" must be a function or an array of functions. Index: ${index}`);
109
+ }
110
+ });
111
+ }
112
+ }
113
+ // 9. 检查 missing 组件的类型
114
+ if ('missing' in options && options.missing !== undefined) {
115
+ if (!isComponent(options.missing)) {
116
+ throw new Error('[Router] "missing" must be a valid component.');
117
+ }
118
+ }
119
+ };
@@ -176,54 +176,57 @@ export class RouteManager {
176
176
  * @returns 匹配结果对象,包含路由记录和解析后的参数;未匹配返回 null
177
177
  */
178
178
  matchByPath(path) {
179
- if (this.config.strict && path.endsWith('/') && path !== '/') {
180
- return null;
179
+ let normalizedPath = path;
180
+ if (normalizedPath.endsWith('/') && normalizedPath !== '/') {
181
+ if (this.config.strict)
182
+ return null;
183
+ normalizedPath = normalizedPath.slice(0, -1);
181
184
  }
182
185
  // 1. 标准化路径
183
- const formattedPath = normalizePath(path);
184
186
  const lookupPath = this.config.ignoreCase
185
- ? formattedPath.toLowerCase()
186
- : formattedPath;
187
+ ? normalizedPath.toLowerCase()
188
+ : normalizedPath;
187
189
  // 2. 静态路由精确匹配
188
190
  const staticRoute = this.staticRoutes.get(lookupPath);
189
191
  if (staticRoute) {
190
- return { path: formattedPath, route: staticRoute, params: {} };
192
+ return { path, route: staticRoute, params: {} };
191
193
  }
192
194
  // 2.5 别名路由精确匹配
193
195
  const aliasRoute = this.aliasRoutes.get(lookupPath);
194
196
  if (aliasRoute) {
195
- return { path: formattedPath, route: aliasRoute, params: {} };
197
+ return { path, route: aliasRoute, params: {} };
196
198
  }
197
199
  // 3. 动态路由匹配
198
- const segments = formattedPath.split('/').filter(Boolean);
199
- const length = segments.length;
200
- const candidates = this.dynamicRoutes.get(length);
201
- if (candidates) {
202
- for (const { regex, route } of candidates) {
200
+ const pathSegments = normalizedPath.split('/').filter(Boolean);
201
+ const segmentCount = pathSegments.length;
202
+ const dynamicCandidates = this.dynamicRoutes.get(segmentCount);
203
+ if (dynamicCandidates) {
204
+ for (const { regex, route } of dynamicCandidates) {
205
+ regex.lastIndex = 0;
203
206
  // 执行正则匹配
204
- const match = regex.exec(formattedPath);
205
- if (match) {
207
+ const regexResult = regex.exec(normalizedPath);
208
+ if (regexResult) {
206
209
  const params = {};
207
210
  // 解析捕获组参数
208
211
  if (route.pattern) {
209
212
  for (let i = 0; i < route.pattern.length; i++) {
210
213
  const paramDef = route.pattern[i];
211
- const value = match[i + 1];
214
+ const capturedValue = regexResult[i + 1];
212
215
  // 仅当捕获到值时写入 params
213
- if (value !== undefined) {
214
- params[paramDef.name] = value;
216
+ if (capturedValue !== undefined) {
217
+ params[paramDef.name] = capturedValue;
215
218
  }
216
219
  }
217
220
  }
218
- return { path: formattedPath, route, params };
221
+ return { path, route, params };
219
222
  }
220
223
  }
221
224
  }
222
- if (formattedPath.endsWith('/index') && this.config.fallbackIndex) {
223
- const shortPath = formattedPath.slice(0, -6);
224
- const staticRoute = this.staticRoutes.get(shortPath || '/');
225
- if (staticRoute) {
226
- return { path: formattedPath, route: staticRoute, params: {} };
225
+ if (lookupPath.endsWith('/index') && this.config.fallbackIndex) {
226
+ const fallbackPath = lookupPath.slice(0, -6);
227
+ const fallbackRoute = this.staticRoutes.get(fallbackPath || '/');
228
+ if (fallbackRoute) {
229
+ return { path, route: fallbackRoute, params: {} };
227
230
  }
228
231
  }
229
232
  return null;
@@ -248,6 +251,7 @@ export class RouteManager {
248
251
  // 3. 动态路由:参数校验与序列化
249
252
  const resolvedParams = {};
250
253
  for (const paramDef of route.pattern) {
254
+ paramDef.regex.lastIndex = 0;
251
255
  const value = params[paramDef.name];
252
256
  const rawValue = String(value ?? ''); // 统一转字符串处理
253
257
  // 3.1 必填参数校验
@@ -1,6 +1,47 @@
1
1
  import { App, type ReadonlyObject } from 'vitarx';
2
2
  import type { AfterCallback, NavigateResult, NavigationGuard, NavOptions, NavTarget, ResolvedRouterConfig, Route, RouteIndex, RouteLocation, RouteRecord, RouterOptions, ScrollPosition, ScrollTarget, URLHash, URLQuery } from '../types/index.js';
3
3
  import { RouteManager } from './manager.js';
4
+ /**
5
+ * 导航上下文
6
+ *
7
+ * 在每次导航过程中创建,封装该次导航所需的所有状态和操作。
8
+ * 作为各导航阶段处理方法之间的共享数据载体,避免方法间传递大量独立参数。
9
+ *
10
+ * @internal
11
+ */
12
+ export interface NavigationContext {
13
+ /** 当前导航任务的唯一标识,用于并发导航竞争检测 */
14
+ taskId: number;
15
+ /** 导航结果对象,各阶段处理方法可修改其 state 和 message */
16
+ result: NavigateResult;
17
+ /** 目标路由位置,匹配失败时为 null */
18
+ to: RouteLocation | null;
19
+ /** 来源路由位置 */
20
+ from: RouteLocation;
21
+ /** 重定向来源路由位置,仅在重定向链中存在 */
22
+ redirectFrom: RouteLocation | undefined;
23
+ /** 是否替换当前历史记录(而非推入新记录) */
24
+ replace: boolean;
25
+ /**
26
+ * 检测重定向循环
27
+ *
28
+ * 每次重定向时调用,递增重定向计数器。
29
+ * 当重定向次数超过最大限制时抛出错误,防止无限循环。
30
+ *
31
+ * @param path - 重定向目标路径,用于错误信息
32
+ * @throws {Error} 当重定向次数超过 MAX_REDIRECTS 时
33
+ */
34
+ checkRedirectLoop: (path: string) => void;
35
+ /**
36
+ * 检测并发导航竞争
37
+ *
38
+ * 判断当前导航任务是否已被更新的导航任务取代。
39
+ * 如果已被取代,则将结果状态标记为 cancelled。
40
+ *
41
+ * @returns true 表示当前导航已被新导航取代,应中止执行
42
+ */
43
+ hasChanged: () => boolean;
44
+ }
4
45
  /**
5
46
  * 路由器抽象基类
6
47
  *
@@ -29,6 +70,11 @@ export declare abstract class Router {
29
70
  * @private
30
71
  */
31
72
  private readonly _routeLocation;
73
+ /**
74
+ * 只读路由位置对象
75
+ * @private
76
+ */
77
+ private readonly _readonlyLocation;
32
78
  /**
33
79
  * 存储就绪状态的 Promise(延迟创建)
34
80
  * @private
@@ -220,7 +266,7 @@ export declare abstract class Router {
220
266
  * await router.push('/foo')
221
267
  * await router.waitViewRender()
222
268
  *
223
- * // 此时 DOM 已确保更新
269
+ * // 此时 DOM 已更新
224
270
  * console.log(document.querySelector('#app').innerHTML)
225
271
  */
226
272
  waitViewRender(navResult?: Promise<NavigateResult>): Promise<void>;
@@ -257,8 +303,115 @@ export declare abstract class Router {
257
303
  * @returns 返回标准的导航结果 Promise
258
304
  */
259
305
  private initialNavigation;
306
+ /**
307
+ * 创建导航上下文
308
+ *
309
+ * 在每次导航开始时构建上下文对象,封装该次导航所需的所有状态:
310
+ * - 生成唯一任务ID,用于并发导航竞争检测
311
+ * - 重置重定向计数器(非重定向场景下)
312
+ * - 执行路由匹配,确定目标路由位置
313
+ * - 构建 NavigateResult 基础对象
314
+ * - 提供 checkRedirectLoop 和 hasChanged 闭包方法
315
+ *
316
+ * @param target - 导航目标
317
+ * @param fromRoute - 来源路由,默认使用当前路由位置
318
+ * @param redirectFrom - 重定向来源路由
319
+ * @returns 导航上下文对象
320
+ */
321
+ private createNavigationContext;
322
+ /**
323
+ * 处理路由未匹配(404)场景
324
+ *
325
+ * 当目标路由无法匹配时执行:
326
+ * 1. 触发全局 onNotFound 钩子
327
+ * 2. 如果钩子返回了新的导航目标,进行重定向
328
+ * 3. 否则返回 notfound 状态
329
+ *
330
+ * @param context - 导航上下文
331
+ * @param target - 原始导航目标
332
+ * @returns 导航结果
333
+ */
334
+ private handleNotFound;
335
+ /**
336
+ * 处理重复路由场景
337
+ *
338
+ * 当目标路由与当前路由的 href 和最终匹配记录完全相同时,
339
+ * 视为重复导航,返回 duplicated 状态,不执行后续流程。
340
+ *
341
+ * @param context - 导航上下文
342
+ * @returns 重复路由结果,或 null 表示非重复路由
343
+ */
344
+ private handleDuplicatedRoute;
345
+ /**
346
+ * 处理仅 hash 变化场景
347
+ *
348
+ * 当路由路径和查询参数相同,仅 hash 部分发生变化时,
349
+ * 直接更新 hash 值并触发 hashUpdate 回调,不执行完整的导航流程。
350
+ *
351
+ * @param context - 导航上下文
352
+ * @returns hash 变化结果,或 null 表示非仅 hash 变化
353
+ */
354
+ private handleHashOnlyChange;
355
+ /**
356
+ * 处理路由重定向场景
357
+ *
358
+ * 当目标路由配置了 redirect 字段时,解析重定向目标并递归执行导航:
359
+ * 1. 支持 redirect 为函数(动态重定向)或静态值
360
+ * 2. 重定向目标可以是路由索引(RouteIndex)或导航目标(NavTarget)
361
+ * 3. 如果重定向配置无效且无组件定义,抛出错误
362
+ *
363
+ * @param context - 导航上下文
364
+ * @returns 重定向导航结果,或 null 表示无需重定向
365
+ */
366
+ private handleRedirect;
367
+ /**
368
+ * 执行守卫流程
369
+ *
370
+ * 按顺序执行路由离开守卫和全局前置守卫,
371
+ * 在每个异步守卫执行后进行并发竞争检测。
372
+ * 如果守卫拦截导航或触发重定向,返回对应结果;
373
+ * 如果守卫全部通过,返回 null 表示继续导航。
374
+ *
375
+ * @param context - 导航上下文
376
+ * @returns 守卫拦截/重定向结果,或 null 表示守卫全部通过
377
+ */
378
+ private executeGuards;
379
+ /**
380
+ * 处理前置守卫执行结果
381
+ *
382
+ * 根据守卫返回值决定导航走向:
383
+ * - false: 拦截导航,返回 aborted 状态
384
+ * - NavTarget: 重定向到新目标
385
+ * - 其他(true/void): 放行,继续导航
386
+ *
387
+ * @param context - 导航上下文
388
+ * @param guardResult - 前置守卫的返回值
389
+ * @returns 拦截/重定向结果,或 null 表示守卫通过
390
+ */
391
+ private handleGuardResult;
392
+ /**
393
+ * 完成导航流程
394
+ *
395
+ * 守卫全部通过后执行最后的导航确认:
396
+ * 1. 根据替换标记更新历史记录(push 或 replace)
397
+ * 2. 调用 completeNavigation 更新路由状态、触发后置钩子和滚动行为
398
+ *
399
+ * @param context - 导航上下文
400
+ * @returns 导航成功结果
401
+ */
402
+ private finalizeNavigation;
260
403
  /**
261
404
  * 导航到指定位置
405
+ *
406
+ * 作为导航流程的编排器,按顺序协调各场景处理方法的执行:
407
+ * 1. 创建导航上下文(路由匹配、并发控制初始化)
408
+ * 2. 处理 404 场景(路由未匹配)
409
+ * 3. 处理重复路由场景
410
+ * 4. 处理仅 hash 变化场景
411
+ * 5. 处理路由重定向场景
412
+ * 6. 执行守卫流程(离开守卫 → 前置守卫)
413
+ * 7. 完成导航(更新历史记录、触发后置钩子)
414
+ *
262
415
  * @param target - 导航目标对象 | 路由位置对象
263
416
  * @param fromRoute - 来源路由对象
264
417
  * @param redirectFrom - 重定向来源对象