vitarx-router 4.0.0-beta.22 → 4.0.0-beta.23

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 CHANGED
@@ -292,6 +292,70 @@ if (hasSuccess(result)) {
292
292
  | `duplicated` | 8 | 重复导航 |
293
293
  | `notfound` | 16 | 路由未匹配 |
294
294
 
295
+ ## 路由未匹配处理
296
+
297
+ 当路由匹配失败(404)时,可以通过 `onNotFound` 钩子进行自定义处理。
298
+
299
+ ### onNotFound 钩子
300
+
301
+ ```typescript
302
+ import { createRouter } from 'vitarx-router'
303
+
304
+ const router = createRouter({
305
+ routes: [],
306
+ onNotFound(target) {
307
+ // target.index 为用户尝试访问的目标
308
+ console.log('路由未匹配:', target.index)
309
+ }
310
+ })
311
+ ```
312
+
313
+ **返回值说明:**
314
+
315
+ | 返回值 | 说明 |
316
+ |----------------------------|----------------------|
317
+ | `NavTarget` / `RouteIndex` | 重定向到新目标 |
318
+ | `RouteLocation` | 作为未匹配路由的位置对象(渲染指定组件) |
319
+ | `void` | 不处理,返回 `notfound` 状态 |
320
+
321
+ ### 重定向到 404 页面
322
+
323
+ ```typescript
324
+ import { createRouter } from 'vitarx-router'
325
+
326
+ const router = createRouter({
327
+ routes: [],
328
+ onNotFound(target) {
329
+ return { index: '/404' }
330
+ }
331
+ })
332
+ ```
333
+
334
+ ### 渲染 404 组件(使用 createMissingRoute)
335
+
336
+ ```typescript
337
+ import { createRouter, createMissingRoute } from 'vitarx-router'
338
+ import NotFoundPage from './pages/NotFound.jsx'
339
+
340
+ const router = createRouter({
341
+ routes: [],
342
+ onNotFound(target) {
343
+ return createMissingRoute(NotFoundPage, target, {
344
+ title: '页面未找到'
345
+ })
346
+ }
347
+ })
348
+ ```
349
+
350
+ ### 名称导航不匹配
351
+
352
+ 名称导航(name-based)匹配失败时,路由器会直接抛出错误,因为名称导航是编程式调用,name 不存在属于代码 bug:
353
+
354
+ ```typescript
355
+ // 如果 'userDetail' 路由不存在,将抛出错误
356
+ router.push({ index: 'userDetail', params: { id: '123' } })
357
+ ```
358
+
295
359
  ## 导航守卫
296
360
 
297
361
  ### 全局前置守卫
@@ -472,18 +536,20 @@ declare module 'vitarx-router' {
472
536
 
473
537
  ### 助手函数
474
538
 
475
- | 函数 | 说明 |
476
- |----------------------------------------|-----------------|
477
- | `createRouter(options)` | 创建路由器实例 |
478
- | `createWebRouter(options)` | 创建 Web 模式路由器 |
479
- | `createMemoryRouter(options)` | 创建 Memory 模式路由器 |
480
- | `createRouteManager(routes, options?)` | 创建路由管理器 |
481
- | `defineRoutes(...routes)` | 定义路由表 |
482
- | `useRouter()` | 获取路由器实例 |
483
- | `useRoute(global?)` | 获取当前路由信息 |
484
- | `useLink(options)` | 创建链接助手 |
485
- | `onBeforeRouteLeave(guard)` | 注册离开守卫 |
486
- | `onBeforeRouteUpdate(callback)` | 注册更新钩子 |
539
+ | 函数 | 说明 |
540
+ |------------------------------------------------|------------------------|
541
+ | `createRouter(options)` | 创建路由器实例 |
542
+ | `createWebRouter(options)` | 创建 Web 模式路由器 |
543
+ | `createMemoryRouter(options)` | 创建 Memory 模式路由器 |
544
+ | `createRouteManager(routes, options?)` | 创建路由管理器 |
545
+ | `defineRoutes(...routes)` | 定义路由表 |
546
+ | `createMissingRoute(component, target, meta?)` | 创建未匹配路由的 RouteLocation |
547
+ | `useRouter()` | 获取路由器实例 |
548
+ | `useRoute(global?)` | 获取当前路由信息 |
549
+ | `useLink(options)` | 创建链接助手 |
550
+ | `onBeforeRouteLeave(guard)` | 注册离开守卫 |
551
+ | `onBeforeRouteUpdate(callback)` | 注册更新钩子 |
552
+ | `removePathEndSlash(path)` | 删除路径末尾的斜杠 |
487
553
 
488
554
  ### Router 实例方法
489
555
 
@@ -7,6 +7,16 @@ import type { GuardResult, NavTarget, RouteIndex, RouteLocation, RoutePath, URLH
7
7
  * @returns {boolean} 如果值是一个导航目标对象则返回true,否则返回false
8
8
  */
9
9
  export declare function hasValidNavTarget(val: unknown): val is NavTarget;
10
+ /**
11
+ * 检查给定的值是否为 RouteLocation 对象
12
+ *
13
+ * 通过检测 `matched` 和 `path` 属性来区分 RouteLocation 与 NavTarget,
14
+ * 因为 NavTarget 不包含这两个字段,而 RouteLocation 必定包含。
15
+ *
16
+ * @param val - 需要检查的值
17
+ * @returns {val is RouteLocation} 如果是 RouteLocation 对象则返回 true
18
+ */
19
+ export declare function isRouteLocation(val: unknown): val is RouteLocation;
10
20
  /**
11
21
  * 检查给定的值是否为有效的路由索引
12
22
  *
@@ -28,7 +38,7 @@ export declare function hasOnlyChangeHash(route1: RouteLocation, route2: RouteLo
28
38
  * @param index - 要判断的索引
29
39
  * @returns {boolean} - 如果索引为路径索引则返回true,否则返回false
30
40
  */
31
- export declare function hasValidPath(index: any): index is RoutePath;
41
+ export declare function isValidPath(index: unknown): index is RoutePath;
32
42
  /**
33
43
  * 移除路径字符串中的指定后缀
34
44
  * @param path - 原始路径字符串
@@ -7,10 +7,20 @@ import { normalizePath, parseQuery } from '../shared/utils.js';
7
7
  * @returns {boolean} 如果值是一个导航目标对象则返回true,否则返回false
8
8
  */
9
9
  export function hasValidNavTarget(val) {
10
- // 首先检查值是否是一个普通对象
11
- // 然后检查该对象是否包含 'index' 属性
12
10
  return isPlainObject(val) && 'index' in val && hasValidRouteIndex(val.index);
13
11
  }
12
+ /**
13
+ * 检查给定的值是否为 RouteLocation 对象
14
+ *
15
+ * 通过检测 `matched` 和 `path` 属性来区分 RouteLocation 与 NavTarget,
16
+ * 因为 NavTarget 不包含这两个字段,而 RouteLocation 必定包含。
17
+ *
18
+ * @param val - 需要检查的值
19
+ * @returns {val is RouteLocation} 如果是 RouteLocation 对象则返回 true
20
+ */
21
+ export function isRouteLocation(val) {
22
+ return isPlainObject(val) && 'matched' in val && 'path' in val;
23
+ }
14
24
  /**
15
25
  * 检查给定的值是否为有效的路由索引
16
26
  *
@@ -39,7 +49,7 @@ export function hasOnlyChangeHash(route1, route2) {
39
49
  * @param index - 要判断的索引
40
50
  * @returns {boolean} - 如果索引为路径索引则返回true,否则返回false
41
51
  */
42
- export function hasValidPath(index) {
52
+ export function isValidPath(index) {
43
53
  return isString(index) && index.startsWith('/');
44
54
  }
45
55
  /**
@@ -1,4 +1,4 @@
1
- import { isComponent, isFunction } from 'vitarx';
1
+ import { isFunction } from 'vitarx';
2
2
  import { RouteManager } from './manager.js';
3
3
  /**
4
4
  * 检查路由器配置选项的合法性
@@ -110,10 +110,4 @@ export const checkRouterOptions = (options) => {
110
110
  });
111
111
  }
112
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
113
  };
@@ -1,6 +1,6 @@
1
1
  import { isArray, isFunction, isString, logger, markRaw } from 'vitarx';
2
2
  import { resolveComponent, resolvePattern, resolveProps } from '../common/resolve.js';
3
- import { hasValidNavTarget, hasValidPath, hasValidRouteIndex } from '../common/utils.js';
3
+ import { hasValidNavTarget, hasValidRouteIndex, isValidPath } from '../common/utils.js';
4
4
  import { isVariablePath, mergePathVariable, mergePattern, optionalVariableCount, validateAliasVariables } from '../common/variable.js';
5
5
  import { normalizePath } from '../shared/utils.js';
6
6
  /**
@@ -288,7 +288,7 @@ export class RouteManager {
288
288
  * @returns 匹配结果对象,包含路由记录和解析后的参数;未匹配返回 null
289
289
  */
290
290
  match(index, params) {
291
- if (hasValidPath(index)) {
291
+ if (isValidPath(index)) {
292
292
  return this.matchByPath(index);
293
293
  }
294
294
  return this.matchByName(index, params);
@@ -319,13 +319,24 @@ export declare abstract class Router {
319
319
  * @returns 导航上下文对象
320
320
  */
321
321
  private createNavigationContext;
322
+ /**
323
+ * 执行导航流程
324
+ *
325
+ * 统一处理重复路由检测、hash 变化、重定向、守卫和导航完成。
326
+ * 被 navigate 和 handleNotFound(onNotFound 返回 RouteLocation 时)共用。
327
+ *
328
+ * @param context - 导航上下文
329
+ * @returns 导航结果
330
+ */
331
+ private processNavigation;
322
332
  /**
323
333
  * 处理路由未匹配(404)场景
324
334
  *
325
335
  * 当目标路由无法匹配时执行:
326
336
  * 1. 触发全局 onNotFound 钩子
327
- * 2. 如果钩子返回了新的导航目标,进行重定向
328
- * 3. 否则返回 notfound 状态
337
+ * 2. 如果钩子返回了 RouteLocation,将其设为导航目标继续正常导航流程
338
+ * 3. 如果钩子返回了新的导航目标(NavTarget),进行重定向
339
+ * 4. 否则返回 notfound 状态
329
340
  *
330
341
  * @param context - 导航上下文
331
342
  * @param target - 原始导航目标
@@ -437,6 +448,16 @@ export declare abstract class Router {
437
448
  private runScrollBehavior;
438
449
  /**
439
450
  * 处理 404 错误
451
+ *
452
+ * 执行 onNotFound 钩子链,按注册顺序依次调用,
453
+ * 第一个返回有效值的钩子会终止遍历。
454
+ *
455
+ * 返回值类型:
456
+ * - RouteLocation: 作为未匹配路由的位置对象(优先判断,因为结构更具体)
457
+ * - NavTarget: 重定向到新目标
458
+ * - string | symbol: 包装为 NavTarget 后重定向
459
+ * - void: 继续执行下一个钩子
460
+ *
440
461
  * @param target - 导航目标对象
441
462
  * @returns - 返回处理结果
442
463
  */
@@ -487,15 +508,6 @@ export declare abstract class Router {
487
508
  * @private
488
509
  */
489
510
  private reportError;
490
- /**
491
- * 创建缺失的路由
492
- * @param component - 路由组件
493
- * @param path - 路径
494
- * @param query - 查询参数
495
- * @param hash - 哈希值
496
- * @returns {RouteLocation} - 返回创建的路由位置对象
497
- */
498
- private createMissingRoute;
499
511
  /**
500
512
  * 构建完整URL路径
501
513
  *
@@ -1,7 +1,7 @@
1
1
  import { getLazyLoader, isArray, isFunction, isPlainObject, isPromise, isString, logger, nextTick, preloadComponent, readonly, shallowReactive } from 'vitarx';
2
2
  import { __ROUTER_KEY__, NavState } from '../common/constant.js';
3
3
  import { updateRouteLocation } from '../common/update.js';
4
- import { hasOnlyChangeHash, hasValidNavTarget, hasValidPath, hasValidRouteIndex, processGuardResult, registerHookTool, removePathSuffix, resolveNavTarget } from '../common/utils.js';
4
+ import { hasOnlyChangeHash, hasValidNavTarget, hasValidRouteIndex, isRouteLocation, isValidPath, processGuardResult, registerHookTool, removePathSuffix, resolveNavTarget } from '../common/utils.js';
5
5
  import { cloneRouteLocation, normalizePath, stringifyQuery } from '../shared/utils.js';
6
6
  import { checkRouterOptions } from './checkOptions.js';
7
7
  import { RouteManager } from './manager.js';
@@ -430,6 +430,16 @@ export class Router {
430
430
  }
431
431
  // 解析目标路由
432
432
  const to = this.matchRoute(target, redirectFrom);
433
+ // name-based 导航匹配失败视为编程错误,直接抛出异常
434
+ // 因为名称导航是编程式调用,name 不存在或参数校验失败属于代码 bug
435
+ if (!to && !isValidPath(target.index)) {
436
+ const name = target.index;
437
+ const route = this.manager.findByName(name);
438
+ if (route) {
439
+ throw new Error(`[Router] Route "${String(name)}" matched but params validation failed. Check required params and their formats.`);
440
+ }
441
+ throw new Error(`[Router] Route not found: "${String(name)}". Name-based navigation must target a registered route.`);
442
+ }
433
443
  // 确定来源路由
434
444
  const from = fromRoute ?? cloneRouteLocation(this._routeLocation);
435
445
  // 构建导航结果基础对象
@@ -470,22 +480,61 @@ export class Router {
470
480
  }
471
481
  };
472
482
  }
483
+ /**
484
+ * 执行导航流程
485
+ *
486
+ * 统一处理重复路由检测、hash 变化、重定向、守卫和导航完成。
487
+ * 被 navigate 和 handleNotFound(onNotFound 返回 RouteLocation 时)共用。
488
+ *
489
+ * @param context - 导航上下文
490
+ * @returns 导航结果
491
+ */
492
+ async processNavigation(context) {
493
+ // 首次导航后才需检测重复和 hash 变化(首次导航无 from,不存在重复场景)
494
+ if (context.from.matched.length) {
495
+ // 重复路由:目标与当前路由完全一致,直接返回
496
+ const duplicatedResult = this.handleDuplicatedRoute(context);
497
+ if (duplicatedResult)
498
+ return duplicatedResult;
499
+ // 仅 hash 变化:路径和参数不变,仅 hash 不同,走轻量处理
500
+ const hashOnlyResult = this.handleHashOnlyChange(context);
501
+ if (hashOnlyResult)
502
+ return hashOnlyResult;
503
+ }
504
+ // 重定向:路由配置了 redirect,递归导航到新目标
505
+ const redirectResult = await this.handleRedirect(context);
506
+ if (redirectResult)
507
+ return redirectResult;
508
+ // 守卫流程:执行 beforeEach 和 beforeEnter,可能中止导航
509
+ const guardResult = await this.executeGuards(context);
510
+ if (guardResult)
511
+ return guardResult;
512
+ // 守卫通过,完成导航:更新状态、触发 afterEach、渲染组件
513
+ return this.finalizeNavigation(context);
514
+ }
473
515
  /**
474
516
  * 处理路由未匹配(404)场景
475
517
  *
476
518
  * 当目标路由无法匹配时执行:
477
519
  * 1. 触发全局 onNotFound 钩子
478
- * 2. 如果钩子返回了新的导航目标,进行重定向
479
- * 3. 否则返回 notfound 状态
520
+ * 2. 如果钩子返回了 RouteLocation,将其设为导航目标继续正常导航流程
521
+ * 3. 如果钩子返回了新的导航目标(NavTarget),进行重定向
522
+ * 4. 否则返回 notfound 状态
480
523
  *
481
524
  * @param context - 导航上下文
482
525
  * @param target - 原始导航目标
483
526
  * @returns 导航结果
484
527
  */
485
- handleNotFound(context, target) {
528
+ async handleNotFound(context, target) {
486
529
  const notFoundResult = this.runNotFoundHook(target);
487
- // 钩子返回了新的导航目标,进行重定向
488
530
  if (notFoundResult) {
531
+ // 钩子返回了 RouteLocation,将其设为导航目标,继续正常导航流程
532
+ if (isRouteLocation(notFoundResult)) {
533
+ context.to = notFoundResult;
534
+ context.result.to = notFoundResult;
535
+ return this.processNavigation(context);
536
+ }
537
+ // 钩子返回了 NavTarget,进行重定向
489
538
  context.checkRedirectLoop(String(notFoundResult.index));
490
539
  return this.navigate(notFoundResult, context.from);
491
540
  }
@@ -677,31 +726,12 @@ export class Router {
677
726
  async navigate(target, fromRoute, redirectFrom) {
678
727
  // 创建导航上下文
679
728
  const context = this.createNavigationContext(target, fromRoute, redirectFrom);
680
- // 场景1: 路由未匹配 (404)
729
+ // 路由未匹配 (404)
681
730
  if (!context.to) {
682
731
  return this.handleNotFound(context, target);
683
732
  }
684
- // 仅在路由第一次路由后检测重复路由
685
- if (context.from.matched.length) {
686
- // 场景2: 重复路由
687
- const duplicatedResult = this.handleDuplicatedRoute(context);
688
- if (duplicatedResult)
689
- return duplicatedResult;
690
- // 场景3: 仅hash变化
691
- const hashOnlyResult = this.handleHashOnlyChange(context);
692
- if (hashOnlyResult)
693
- return hashOnlyResult;
694
- }
695
- // 场景4: 路由重定向
696
- const redirectResult = await this.handleRedirect(context);
697
- if (redirectResult)
698
- return redirectResult;
699
- // 场景5: 执行守卫流程
700
- const guardResult = await this.executeGuards(context);
701
- if (guardResult)
702
- return guardResult;
703
- // 场景6: 完成导航
704
- return this.finalizeNavigation(context);
733
+ // 处理导航
734
+ return this.processNavigation(context);
705
735
  }
706
736
  // ============================================================
707
737
  // 导航完成与滚动行为
@@ -769,6 +799,16 @@ export class Router {
769
799
  // ============================================================
770
800
  /**
771
801
  * 处理 404 错误
802
+ *
803
+ * 执行 onNotFound 钩子链,按注册顺序依次调用,
804
+ * 第一个返回有效值的钩子会终止遍历。
805
+ *
806
+ * 返回值类型:
807
+ * - RouteLocation: 作为未匹配路由的位置对象(优先判断,因为结构更具体)
808
+ * - NavTarget: 重定向到新目标
809
+ * - string | symbol: 包装为 NavTarget 后重定向
810
+ * - void: 继续执行下一个钩子
811
+ *
772
812
  * @param target - 导航目标对象
773
813
  * @returns - 返回处理结果
774
814
  */
@@ -778,8 +818,13 @@ export class Router {
778
818
  for (const hook of this._hooks.onNotFound) {
779
819
  try {
780
820
  const result = hook.call(this, target);
821
+ // 优先判断 RouteLocation(有 matched 和 path 属性)
822
+ if (isRouteLocation(result))
823
+ return result;
824
+ // 判断 NavTarget(有 index 属性)
781
825
  if (hasValidNavTarget(result))
782
826
  return result;
827
+ // 字符串或 symbol 包装为 NavTarget
783
828
  if (isString(result) || typeof result === 'symbol') {
784
829
  return {
785
830
  index: result
@@ -928,25 +973,6 @@ export class Router {
928
973
  // ============================================================
929
974
  // 路由匹配与URL构建
930
975
  // ============================================================
931
- /**
932
- * 创建缺失的路由
933
- * @param component - 路由组件
934
- * @param path - 路径
935
- * @param query - 查询参数
936
- * @param hash - 哈希值
937
- * @returns {RouteLocation} - 返回创建的路由位置对象
938
- */
939
- createMissingRoute(component, path, query = {}, hash = '') {
940
- return {
941
- href: this.buildUrl(path, query, hash),
942
- path,
943
- hash,
944
- params: {},
945
- query,
946
- meta: {},
947
- matched: [{ path, isGroup: false, component: { default: component } }]
948
- };
949
- }
950
976
  /**
951
977
  * 构建完整URL路径
952
978
  *
@@ -977,7 +1003,7 @@ export class Router {
977
1003
  */
978
1004
  matchRoute(target, redirectFrom) {
979
1005
  let matchTarget = target.index;
980
- const isPath = hasValidPath(matchTarget);
1006
+ const isPath = isValidPath(matchTarget);
981
1007
  // 如果配置了后缀且目标是路径,则去除后缀
982
1008
  if (this.config.suffix && isPath) {
983
1009
  // 去除路径后缀
@@ -987,9 +1013,6 @@ export class Router {
987
1013
  let match;
988
1014
  if (isPath) {
989
1015
  match = this.manager.matchByPath(matchTarget);
990
- if (!match && this.config.missing) {
991
- return this.createMissingRoute(this.config.missing, matchTarget, target.query, target.hash);
992
- }
993
1016
  }
994
1017
  else {
995
1018
  match = this.manager.matchByName(matchTarget, target.params);
@@ -1,5 +1,5 @@
1
1
  import { computed, isPlainObject, isString, logger } from 'vitarx';
2
- import { hasValidNavTarget, hasValidPath } from '../common/utils.js';
2
+ import { hasValidNavTarget, isValidPath } from '../common/utils.js';
3
3
  import { useRouter } from './inject.js';
4
4
  import { cloneRouteLocation, parseQuery } from './utils.js';
5
5
  /**
@@ -104,7 +104,7 @@ export function useLink(props) {
104
104
  if (route.value?.href) {
105
105
  return route.value.href;
106
106
  }
107
- if (isPlainObject(props.to) && hasValidPath(props.to.index)) {
107
+ if (isPlainObject(props.to) && isValidPath(props.to.index)) {
108
108
  return props.to.index;
109
109
  }
110
110
  if (isString(props.to)) {
@@ -1,5 +1,5 @@
1
1
  import { type DeepReadonly } from 'vitarx';
2
- import type { RouteLocation } from '../types/index.js';
2
+ import type { NotFoundTarget, RouteLocation, RouteMetaData, RouteViewComponent } from '../types/index.js';
3
3
  /**
4
4
  * 将 query 字符串转为对象
5
5
  *
@@ -14,6 +14,17 @@ export declare function parseQuery(queryString: string): Record<string, string>;
14
14
  * @return {string} 转换后的查询字符串(如 ?key1=value1&key2=value2)
15
15
  */
16
16
  export declare function stringifyQuery(obj: Record<string, string>): `?${string}` | '';
17
+ /**
18
+ * 去除路径末尾的斜杠
19
+ *
20
+ * @example
21
+ * removePathEndSlash('/foo/') // '/foo'
22
+ * removePathEndSlash('/foo') // '/foo'
23
+ *
24
+ * @param {string} str - 路径字符串
25
+ * @return {string} - 去除末尾斜杠后的路径字符串
26
+ */
27
+ export declare function removePathEndSlash<T extends string>(str: T): T;
17
28
  /**
18
29
  * 归一化path
19
30
  *
@@ -38,3 +49,27 @@ export declare function normalizePath(path: string, removeEndSlash?: boolean): `
38
49
  * @return {RouteLocation} - 克隆过后的对象
39
50
  */
40
51
  export declare function cloneRouteLocation(route: RouteLocation | DeepReadonly<RouteLocation>): RouteLocation;
52
+ /**
53
+ * 创建未匹配路由的 RouteLocation 对象
54
+ *
55
+ * 用于在 onNotFound 钩子中快速创建一个可渲染的 RouteLocation,
56
+ * 使路由匹配失败时仍能渲染指定组件(如 404 页面)。
57
+ *
58
+ * 该函数要求 target.index 必须为路径(以 `/` 开头),
59
+ * 因为名称导航(name-based)匹配失败属于编程错误,应直接抛出异常,
60
+ * 而非创建伪路由位置。
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * // 在 onNotFound 钩子中使用
65
+ * onNotFound(target) {
66
+ * return createMissingRoute(NotFoundPage, target, { title: '页面未找到' })
67
+ * }
68
+ * ```
69
+ *
70
+ * @param component - 未匹配时要渲染的组件
71
+ * @param target - 用户的原始导航意图(index 必须为路径)
72
+ * @param meta - 可选的自定义 meta 信息,默认为空对象
73
+ * @returns {RouteLocation} 可在 onNotFound 钩子中直接返回的 RouteLocation
74
+ */
75
+ export declare function createMissingRoute(component: RouteViewComponent, target: NotFoundTarget, meta?: RouteMetaData): RouteLocation;
@@ -25,6 +25,21 @@ export function stringifyQuery(obj) {
25
25
  // 如果对象为空或没有任何有效查询参数,返回空字符串
26
26
  return queryString ? `?${queryString}` : '';
27
27
  }
28
+ /**
29
+ * 去除路径末尾的斜杠
30
+ *
31
+ * @example
32
+ * removePathEndSlash('/foo/') // '/foo'
33
+ * removePathEndSlash('/foo') // '/foo'
34
+ *
35
+ * @param {string} str - 路径字符串
36
+ * @return {string} - 去除末尾斜杠后的路径字符串
37
+ */
38
+ export function removePathEndSlash(str) {
39
+ if (str === '/')
40
+ return str;
41
+ return str.endsWith('/') ? str.slice(0, -1) : str;
42
+ }
28
43
  /**
29
44
  * 归一化path
30
45
  *
@@ -44,8 +59,8 @@ export function stringifyQuery(obj) {
44
59
  export function normalizePath(path, removeEndSlash = false) {
45
60
  // 去除所有空格 处理重复//
46
61
  let normalizedPath = `/${path.trim()}`.replace(/\s+/g, '').replace(/\/+/g, '/');
47
- if (removeEndSlash && normalizedPath.endsWith('/') && normalizedPath !== '/') {
48
- normalizedPath = normalizedPath.replace(/\/$/, '');
62
+ if (removeEndSlash) {
63
+ normalizedPath = removePathEndSlash(normalizedPath);
49
64
  }
50
65
  // 去除尾随斜杠
51
66
  return normalizedPath;
@@ -60,3 +75,41 @@ export function cloneRouteLocation(route) {
60
75
  const { matched, ...rest } = toRaw(route);
61
76
  return Object.assign(deepClone(rest), { matched: Array.from(matched) });
62
77
  }
78
+ /**
79
+ * 创建未匹配路由的 RouteLocation 对象
80
+ *
81
+ * 用于在 onNotFound 钩子中快速创建一个可渲染的 RouteLocation,
82
+ * 使路由匹配失败时仍能渲染指定组件(如 404 页面)。
83
+ *
84
+ * 该函数要求 target.index 必须为路径(以 `/` 开头),
85
+ * 因为名称导航(name-based)匹配失败属于编程错误,应直接抛出异常,
86
+ * 而非创建伪路由位置。
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * // 在 onNotFound 钩子中使用
91
+ * onNotFound(target) {
92
+ * return createMissingRoute(NotFoundPage, target, { title: '页面未找到' })
93
+ * }
94
+ * ```
95
+ *
96
+ * @param component - 未匹配时要渲染的组件
97
+ * @param target - 用户的原始导航意图(index 必须为路径)
98
+ * @param meta - 可选的自定义 meta 信息,默认为空对象
99
+ * @returns {RouteLocation} 可在 onNotFound 钩子中直接返回的 RouteLocation
100
+ */
101
+ export function createMissingRoute(component, target, meta) {
102
+ const path = target.index;
103
+ const query = target.query ?? {};
104
+ const hash = target.hash ?? '';
105
+ const params = target.params ?? {};
106
+ return {
107
+ href: path,
108
+ path,
109
+ hash,
110
+ params,
111
+ query,
112
+ meta: meta || {},
113
+ matched: [{ path, isGroup: false, component: { default: component } }]
114
+ };
115
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Router } from '../router/router.js';
2
2
  import type { NavTarget, RouteLocation } from './navigation.js';
3
- import type { RouteIndex } from './route.js';
3
+ import type { RouteIndex, RoutePath } from './route.js';
4
4
  /**
5
5
  * 守卫结果
6
6
  *
@@ -32,17 +32,26 @@ export type NavigationGuard = (this: Router, to: RouteLocation, from: RouteLocat
32
32
  * @param from - 上一个路由对象
33
33
  */
34
34
  export type AfterCallback = (this: Router, to: RouteLocation, from: RouteLocation) => void;
35
+ /**
36
+ * 未匹配路由目标类型
37
+ */
38
+ export type NotFoundTarget = NavTarget<RoutePath>;
35
39
  /**
36
40
  * 路由未匹配钩子
37
41
  *
38
42
  * 触发时机: 路由匹配失败 (404) 时。
39
- * 用途: 可用于统一跳转 404 页面或记录错误日志。
43
+ * 用途: 可用于统一跳转 404 页面、渲染 404 组件或记录错误日志。
44
+ *
45
+ * 返回值说明:
46
+ * - NavTarget | RouteIndex: 重定向到新目标
47
+ * - RouteLocation: 作为未匹配路由的位置对象(可配合 createMissingRoute 使用)
48
+ * - void: 不处理,返回 notfound 状态
40
49
  *
41
50
  * @param this - 路由器实例
42
51
  * @param target - 用户的原始导航意图
43
- * @returns {NavTarget | RouteIndex | void} 返回新目标表示重定向,无返回值则抛出错误
52
+ * @returns {NavTarget | RouteIndex | RouteLocation | void} 返回新目标表示重定向,返回 RouteLocation 表示渲染指定组件,无返回值则返回 notfound 状态
44
53
  */
45
- export type NotFoundHandler = (this: Router, target: NavTarget) => NavTarget | RouteIndex | void;
54
+ export type NotFoundHandler = (this: Router, target: NotFoundTarget) => NavTarget | RouteIndex | RouteLocation | void;
46
55
  /**
47
56
  * 路由错误处理钩子
48
57
  *
@@ -1,7 +1,7 @@
1
1
  import type { MakeRequired } from 'vitarx';
2
2
  import type { RouteManager } from '../router/manager.js';
3
3
  import type { AfterCallback, NavErrorListener, NavigationGuard, NotFoundHandler } from './hooks.js';
4
- import type { InjectPropsHandler, Route, RouteViewComponent } from './route.js';
4
+ import type { InjectPropsHandler, Route } from './route.js';
5
5
  import type { BeforeScrollCallback } from './scroll.js';
6
6
  /**
7
7
  * 定义路由器配置选项接口
@@ -87,14 +87,5 @@ export interface RouterOptions {
87
87
  * @param from - 源路由对象
88
88
  */
89
89
  onError?: NavErrorListener | NavErrorListener[];
90
- /**
91
- * 未匹配到路由时要渲染的组件
92
- *
93
- * 如果你需要在未匹配到路由时重定向到指定的页面,则不应该使用`missing`选项,
94
- * 而是应该使用 `onNotFound` 钩子指定重定向目标。
95
- *
96
- * > 注意:如果你设置了`missing`选项,`path` 导航不匹配时也会更新`URL`地址,然后渲染`missing`组件。
97
- */
98
- missing?: RouteViewComponent;
99
90
  }
100
91
  export type ResolvedRouterConfig = MakeRequired<Omit<RouterOptions, 'routes' | 'beforeEach' | 'afterEach' | 'onError' | 'onNotFound'>, 'mode' | 'base'>;
@@ -23,7 +23,11 @@ function extractAstLiteralValue(node) {
23
23
  for (const prop of node.properties) {
24
24
  if (prop.type !== 'ObjectProperty')
25
25
  continue;
26
- const key = prop.key.type === 'Identifier' ? prop.key.name : null;
26
+ const key = prop.key.type === 'Identifier'
27
+ ? prop.key.name
28
+ : prop.key.type === 'StringLiteral'
29
+ ? prop.key.value
30
+ : null;
27
31
  if (key) {
28
32
  obj[key] = extractAstLiteralValue(prop.value);
29
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitarx-router",
3
- "version": "4.0.0-beta.22",
3
+ "version": "4.0.0-beta.23",
4
4
  "description": "Official routing solution for Vitarx framework with declarative routing, navigation guards, dynamic routes, file-based routing with HMR, and full TypeScript support.",
5
5
  "author": "ZhuChonglin <8210856@qq.com>",
6
6
  "license": "MIT",