vitarx-router 4.0.0-beta.22 → 4.0.0-beta.24
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 +34 -12
- package/dist/core/router/router.js +87 -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);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { App, type ReadonlyObject } from 'vitarx';
|
|
2
|
-
import type { AfterCallback, NavigateResult, NavigationGuard, NavOptions, NavTarget, ResolvedRouterConfig, Route, RouteIndex, RouteLocation, RouteRecord, RouterOptions, ScrollPosition, ScrollTarget, URLHash, URLQuery } from '../types/index.js';
|
|
2
|
+
import type { AfterCallback, NavErrorListener, NavigateResult, NavigationGuard, NavOptions, NavTarget, NotFoundHandler, ResolvedRouterConfig, Route, RouteIndex, RouteLocation, RouteRecord, RouterOptions, ScrollPosition, ScrollTarget, URLHash, URLQuery } from '../types/index.js';
|
|
3
3
|
import { RouteManager } from './manager.js';
|
|
4
4
|
/**
|
|
5
5
|
* 导航上下文
|
|
@@ -177,6 +177,16 @@ export declare abstract class Router {
|
|
|
177
177
|
* @param guard
|
|
178
178
|
*/
|
|
179
179
|
beforeEach(guard: NavigationGuard): void;
|
|
180
|
+
/**
|
|
181
|
+
* 添加错误处理回调函数
|
|
182
|
+
* @param handler
|
|
183
|
+
*/
|
|
184
|
+
onError(handler: NavErrorListener): void;
|
|
185
|
+
/**
|
|
186
|
+
* 添加未匹配路由处理回调函数
|
|
187
|
+
* @param handler
|
|
188
|
+
*/
|
|
189
|
+
onNotFound(handler: NotFoundHandler): void;
|
|
180
190
|
/**
|
|
181
191
|
* 跳转指定的历史记录位置
|
|
182
192
|
*
|
|
@@ -319,13 +329,24 @@ export declare abstract class Router {
|
|
|
319
329
|
* @returns 导航上下文对象
|
|
320
330
|
*/
|
|
321
331
|
private createNavigationContext;
|
|
332
|
+
/**
|
|
333
|
+
* 执行导航流程
|
|
334
|
+
*
|
|
335
|
+
* 统一处理重复路由检测、hash 变化、重定向、守卫和导航完成。
|
|
336
|
+
* 被 navigate 和 handleNotFound(onNotFound 返回 RouteLocation 时)共用。
|
|
337
|
+
*
|
|
338
|
+
* @param context - 导航上下文
|
|
339
|
+
* @returns 导航结果
|
|
340
|
+
*/
|
|
341
|
+
private processNavigation;
|
|
322
342
|
/**
|
|
323
343
|
* 处理路由未匹配(404)场景
|
|
324
344
|
*
|
|
325
345
|
* 当目标路由无法匹配时执行:
|
|
326
346
|
* 1. 触发全局 onNotFound 钩子
|
|
327
|
-
* 2.
|
|
328
|
-
* 3.
|
|
347
|
+
* 2. 如果钩子返回了 RouteLocation,将其设为导航目标继续正常导航流程
|
|
348
|
+
* 3. 如果钩子返回了新的导航目标(NavTarget),进行重定向
|
|
349
|
+
* 4. 否则返回 notfound 状态
|
|
329
350
|
*
|
|
330
351
|
* @param context - 导航上下文
|
|
331
352
|
* @param target - 原始导航目标
|
|
@@ -437,6 +458,16 @@ export declare abstract class Router {
|
|
|
437
458
|
private runScrollBehavior;
|
|
438
459
|
/**
|
|
439
460
|
* 处理 404 错误
|
|
461
|
+
*
|
|
462
|
+
* 执行 onNotFound 钩子链,按注册顺序依次调用,
|
|
463
|
+
* 第一个返回有效值的钩子会终止遍历。
|
|
464
|
+
*
|
|
465
|
+
* 返回值类型:
|
|
466
|
+
* - RouteLocation: 作为未匹配路由的位置对象(优先判断,因为结构更具体)
|
|
467
|
+
* - NavTarget: 重定向到新目标
|
|
468
|
+
* - string | symbol: 包装为 NavTarget 后重定向
|
|
469
|
+
* - void: 继续执行下一个钩子
|
|
470
|
+
*
|
|
440
471
|
* @param target - 导航目标对象
|
|
441
472
|
* @returns - 返回处理结果
|
|
442
473
|
*/
|
|
@@ -487,15 +518,6 @@ export declare abstract class Router {
|
|
|
487
518
|
* @private
|
|
488
519
|
*/
|
|
489
520
|
private reportError;
|
|
490
|
-
/**
|
|
491
|
-
* 创建缺失的路由
|
|
492
|
-
* @param component - 路由组件
|
|
493
|
-
* @param path - 路径
|
|
494
|
-
* @param query - 查询参数
|
|
495
|
-
* @param hash - 哈希值
|
|
496
|
-
* @returns {RouteLocation} - 返回创建的路由位置对象
|
|
497
|
-
*/
|
|
498
|
-
private createMissingRoute;
|
|
499
521
|
/**
|
|
500
522
|
* 构建完整URL路径
|
|
501
523
|
*
|
|
@@ -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';
|
|
@@ -247,6 +247,20 @@ export class Router {
|
|
|
247
247
|
beforeEach(guard) {
|
|
248
248
|
registerHookTool(this._hooks, 'beforeEach', guard);
|
|
249
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* 添加错误处理回调函数
|
|
252
|
+
* @param handler
|
|
253
|
+
*/
|
|
254
|
+
onError(handler) {
|
|
255
|
+
registerHookTool(this._hooks, 'onError', handler);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* 添加未匹配路由处理回调函数
|
|
259
|
+
* @param handler
|
|
260
|
+
*/
|
|
261
|
+
onNotFound(handler) {
|
|
262
|
+
registerHookTool(this._hooks, 'onNotFound', handler);
|
|
263
|
+
}
|
|
250
264
|
/**
|
|
251
265
|
* 后退到上一个历史记录
|
|
252
266
|
*/
|
|
@@ -430,6 +444,16 @@ export class Router {
|
|
|
430
444
|
}
|
|
431
445
|
// 解析目标路由
|
|
432
446
|
const to = this.matchRoute(target, redirectFrom);
|
|
447
|
+
// name-based 导航匹配失败视为编程错误,直接抛出异常
|
|
448
|
+
// 因为名称导航是编程式调用,name 不存在或参数校验失败属于代码 bug
|
|
449
|
+
if (!to && !isValidPath(target.index)) {
|
|
450
|
+
const name = target.index;
|
|
451
|
+
const route = this.manager.findByName(name);
|
|
452
|
+
if (route) {
|
|
453
|
+
throw new Error(`[Router] Route "${String(name)}" matched but params validation failed. Check required params and their formats.`);
|
|
454
|
+
}
|
|
455
|
+
throw new Error(`[Router] Route not found: "${String(name)}". Name-based navigation must target a registered route.`);
|
|
456
|
+
}
|
|
433
457
|
// 确定来源路由
|
|
434
458
|
const from = fromRoute ?? cloneRouteLocation(this._routeLocation);
|
|
435
459
|
// 构建导航结果基础对象
|
|
@@ -470,22 +494,61 @@ export class Router {
|
|
|
470
494
|
}
|
|
471
495
|
};
|
|
472
496
|
}
|
|
497
|
+
/**
|
|
498
|
+
* 执行导航流程
|
|
499
|
+
*
|
|
500
|
+
* 统一处理重复路由检测、hash 变化、重定向、守卫和导航完成。
|
|
501
|
+
* 被 navigate 和 handleNotFound(onNotFound 返回 RouteLocation 时)共用。
|
|
502
|
+
*
|
|
503
|
+
* @param context - 导航上下文
|
|
504
|
+
* @returns 导航结果
|
|
505
|
+
*/
|
|
506
|
+
async processNavigation(context) {
|
|
507
|
+
// 首次导航后才需检测重复和 hash 变化(首次导航无 from,不存在重复场景)
|
|
508
|
+
if (context.from.matched.length) {
|
|
509
|
+
// 重复路由:目标与当前路由完全一致,直接返回
|
|
510
|
+
const duplicatedResult = this.handleDuplicatedRoute(context);
|
|
511
|
+
if (duplicatedResult)
|
|
512
|
+
return duplicatedResult;
|
|
513
|
+
// 仅 hash 变化:路径和参数不变,仅 hash 不同,走轻量处理
|
|
514
|
+
const hashOnlyResult = this.handleHashOnlyChange(context);
|
|
515
|
+
if (hashOnlyResult)
|
|
516
|
+
return hashOnlyResult;
|
|
517
|
+
}
|
|
518
|
+
// 重定向:路由配置了 redirect,递归导航到新目标
|
|
519
|
+
const redirectResult = await this.handleRedirect(context);
|
|
520
|
+
if (redirectResult)
|
|
521
|
+
return redirectResult;
|
|
522
|
+
// 守卫流程:执行 beforeEach 和 beforeEnter,可能中止导航
|
|
523
|
+
const guardResult = await this.executeGuards(context);
|
|
524
|
+
if (guardResult)
|
|
525
|
+
return guardResult;
|
|
526
|
+
// 守卫通过,完成导航:更新状态、触发 afterEach、渲染组件
|
|
527
|
+
return this.finalizeNavigation(context);
|
|
528
|
+
}
|
|
473
529
|
/**
|
|
474
530
|
* 处理路由未匹配(404)场景
|
|
475
531
|
*
|
|
476
532
|
* 当目标路由无法匹配时执行:
|
|
477
533
|
* 1. 触发全局 onNotFound 钩子
|
|
478
|
-
* 2.
|
|
479
|
-
* 3.
|
|
534
|
+
* 2. 如果钩子返回了 RouteLocation,将其设为导航目标继续正常导航流程
|
|
535
|
+
* 3. 如果钩子返回了新的导航目标(NavTarget),进行重定向
|
|
536
|
+
* 4. 否则返回 notfound 状态
|
|
480
537
|
*
|
|
481
538
|
* @param context - 导航上下文
|
|
482
539
|
* @param target - 原始导航目标
|
|
483
540
|
* @returns 导航结果
|
|
484
541
|
*/
|
|
485
|
-
handleNotFound(context, target) {
|
|
542
|
+
async handleNotFound(context, target) {
|
|
486
543
|
const notFoundResult = this.runNotFoundHook(target);
|
|
487
|
-
// 钩子返回了新的导航目标,进行重定向
|
|
488
544
|
if (notFoundResult) {
|
|
545
|
+
// 钩子返回了 RouteLocation,将其设为导航目标,继续正常导航流程
|
|
546
|
+
if (isRouteLocation(notFoundResult)) {
|
|
547
|
+
context.to = notFoundResult;
|
|
548
|
+
context.result.to = notFoundResult;
|
|
549
|
+
return this.processNavigation(context);
|
|
550
|
+
}
|
|
551
|
+
// 钩子返回了 NavTarget,进行重定向
|
|
489
552
|
context.checkRedirectLoop(String(notFoundResult.index));
|
|
490
553
|
return this.navigate(notFoundResult, context.from);
|
|
491
554
|
}
|
|
@@ -677,31 +740,12 @@ export class Router {
|
|
|
677
740
|
async navigate(target, fromRoute, redirectFrom) {
|
|
678
741
|
// 创建导航上下文
|
|
679
742
|
const context = this.createNavigationContext(target, fromRoute, redirectFrom);
|
|
680
|
-
//
|
|
743
|
+
// 路由未匹配 (404)
|
|
681
744
|
if (!context.to) {
|
|
682
745
|
return this.handleNotFound(context, target);
|
|
683
746
|
}
|
|
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);
|
|
747
|
+
// 处理导航
|
|
748
|
+
return this.processNavigation(context);
|
|
705
749
|
}
|
|
706
750
|
// ============================================================
|
|
707
751
|
// 导航完成与滚动行为
|
|
@@ -769,6 +813,16 @@ export class Router {
|
|
|
769
813
|
// ============================================================
|
|
770
814
|
/**
|
|
771
815
|
* 处理 404 错误
|
|
816
|
+
*
|
|
817
|
+
* 执行 onNotFound 钩子链,按注册顺序依次调用,
|
|
818
|
+
* 第一个返回有效值的钩子会终止遍历。
|
|
819
|
+
*
|
|
820
|
+
* 返回值类型:
|
|
821
|
+
* - RouteLocation: 作为未匹配路由的位置对象(优先判断,因为结构更具体)
|
|
822
|
+
* - NavTarget: 重定向到新目标
|
|
823
|
+
* - string | symbol: 包装为 NavTarget 后重定向
|
|
824
|
+
* - void: 继续执行下一个钩子
|
|
825
|
+
*
|
|
772
826
|
* @param target - 导航目标对象
|
|
773
827
|
* @returns - 返回处理结果
|
|
774
828
|
*/
|
|
@@ -778,8 +832,13 @@ export class Router {
|
|
|
778
832
|
for (const hook of this._hooks.onNotFound) {
|
|
779
833
|
try {
|
|
780
834
|
const result = hook.call(this, target);
|
|
835
|
+
// 优先判断 RouteLocation(有 matched 和 path 属性)
|
|
836
|
+
if (isRouteLocation(result))
|
|
837
|
+
return result;
|
|
838
|
+
// 判断 NavTarget(有 index 属性)
|
|
781
839
|
if (hasValidNavTarget(result))
|
|
782
840
|
return result;
|
|
841
|
+
// 字符串或 symbol 包装为 NavTarget
|
|
783
842
|
if (isString(result) || typeof result === 'symbol') {
|
|
784
843
|
return {
|
|
785
844
|
index: result
|
|
@@ -928,25 +987,6 @@ export class Router {
|
|
|
928
987
|
// ============================================================
|
|
929
988
|
// 路由匹配与URL构建
|
|
930
989
|
// ============================================================
|
|
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
990
|
/**
|
|
951
991
|
* 构建完整URL路径
|
|
952
992
|
*
|
|
@@ -977,7 +1017,7 @@ export class Router {
|
|
|
977
1017
|
*/
|
|
978
1018
|
matchRoute(target, redirectFrom) {
|
|
979
1019
|
let matchTarget = target.index;
|
|
980
|
-
const isPath =
|
|
1020
|
+
const isPath = isValidPath(matchTarget);
|
|
981
1021
|
// 如果配置了后缀且目标是路径,则去除后缀
|
|
982
1022
|
if (this.config.suffix && isPath) {
|
|
983
1023
|
// 去除路径后缀
|
|
@@ -987,9 +1027,6 @@ export class Router {
|
|
|
987
1027
|
let match;
|
|
988
1028
|
if (isPath) {
|
|
989
1029
|
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
1030
|
}
|
|
994
1031
|
else {
|
|
995
1032
|
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.24",
|
|
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",
|