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 +78 -12
- package/dist/core/common/utils.d.ts +11 -1
- package/dist/core/common/utils.js +13 -3
- package/dist/core/router/checkOptions.js +1 -7
- package/dist/core/router/manager.js +2 -2
- package/dist/core/router/router.d.ts +23 -11
- package/dist/core/router/router.js +73 -50
- package/dist/core/shared/link.js +2 -2
- package/dist/core/shared/utils.d.ts +36 -1
- package/dist/core/shared/utils.js +55 -2
- package/dist/core/types/hooks.d.ts +13 -4
- package/dist/core/types/options.d.ts +1 -10
- package/dist/file-router/macros/astValueExtractor.js +5 -1
- package/package.json +1 -1
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)`
|
|
479
|
-
| `createMemoryRouter(options)`
|
|
480
|
-
| `createRouteManager(routes, options?)`
|
|
481
|
-
| `defineRoutes(...routes)`
|
|
482
|
-
| `
|
|
483
|
-
| `
|
|
484
|
-
| `
|
|
485
|
-
| `
|
|
486
|
-
| `
|
|
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
|
|
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
|
|
52
|
+
export function isValidPath(index) {
|
|
43
53
|
return isString(index) && index.startsWith('/');
|
|
44
54
|
}
|
|
45
55
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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,
|
|
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 (
|
|
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.
|
|
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,
|
|
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.
|
|
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
|
-
//
|
|
729
|
+
// 路由未匹配 (404)
|
|
681
730
|
if (!context.to) {
|
|
682
731
|
return this.handleNotFound(context, target);
|
|
683
732
|
}
|
|
684
|
-
//
|
|
685
|
-
|
|
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 =
|
|
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);
|
package/dist/core/shared/link.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { computed, isPlainObject, isString, logger } from 'vitarx';
|
|
2
|
-
import { hasValidNavTarget,
|
|
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) &&
|
|
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
|
|
48
|
-
normalizedPath = normalizedPath
|
|
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:
|
|
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
|
|
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'
|
|
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.
|
|
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",
|