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.
- package/README.md +42 -17
- package/dist/{plugin-vite/auto-routes → auto-routes}/handleHotUpdate.d.ts +1 -1
- package/dist/components/RouterView.js +5 -4
- package/dist/core/common/constant.d.ts +5 -6
- package/dist/core/common/constant.js +5 -6
- package/dist/core/common/utils.js +2 -1
- package/dist/core/router/checkOptions.d.ts +11 -0
- package/dist/core/router/checkOptions.js +119 -0
- package/dist/core/router/manager.js +27 -23
- package/dist/core/router/router.d.ts +154 -1
- package/dist/core/router/router.js +303 -230
- package/dist/core/router/web.d.ts +13 -0
- package/dist/core/router/web.js +35 -4
- package/dist/core/shared/link.d.ts +7 -0
- package/dist/core/shared/link.js +11 -8
- package/dist/core/shared/route.js +1 -2
- package/dist/core/shared/router.d.ts +3 -3
- package/dist/core/shared/router.js +7 -4
- package/dist/core/types/options.d.ts +2 -0
- package/dist/file-router/config/index.d.ts +2 -1
- package/dist/file-router/config/index.js +2 -1
- package/dist/file-router/config/resolve.d.ts +43 -0
- package/dist/file-router/config/resolve.js +69 -0
- package/dist/file-router/{utils/validateOptions.d.ts → config/validate.d.ts} +11 -10
- package/dist/file-router/config/validate.js +280 -0
- package/dist/file-router/constants.d.ts +12 -2
- package/dist/file-router/constants.js +13 -3
- package/dist/file-router/generator/generateRoutes.d.ts +44 -13
- package/dist/file-router/generator/generateRoutes.js +159 -80
- package/dist/file-router/generator/generateTypes.d.ts +3 -29
- package/dist/file-router/generator/generateTypes.js +36 -41
- package/dist/file-router/global.d.ts +1 -1
- package/dist/file-router/index.d.ts +224 -90
- package/dist/file-router/index.js +571 -135
- package/dist/file-router/macros/astValueExtractor.d.ts +1 -1
- package/dist/file-router/macros/astValueExtractor.js +27 -7
- package/dist/file-router/macros/definePage.d.ts +20 -3
- package/dist/file-router/macros/definePage.js +120 -40
- package/dist/file-router/parser/exportChecker.d.ts +4 -23
- package/dist/file-router/parser/exportChecker.js +38 -79
- package/dist/file-router/parser/filterUtils.d.ts +25 -0
- package/dist/file-router/parser/filterUtils.js +43 -0
- package/dist/file-router/parser/index.d.ts +2 -1
- package/dist/file-router/parser/index.js +2 -1
- package/dist/file-router/parser/parsePage.d.ts +56 -9
- package/dist/file-router/parser/parsePage.js +194 -172
- package/dist/file-router/parser/routePath.d.ts +22 -0
- package/dist/file-router/parser/routePath.js +74 -0
- package/dist/file-router/types/hooks.d.ts +52 -0
- package/dist/file-router/types/index.d.ts +3 -0
- package/dist/file-router/types/index.js +1 -0
- package/dist/file-router/types/options.d.ts +279 -0
- package/dist/file-router/types/options.js +1 -0
- package/dist/file-router/types/route.d.ts +114 -0
- package/dist/file-router/types/route.js +1 -0
- package/dist/file-router/utils/fileReader.d.ts +11 -0
- package/dist/file-router/utils/fileReader.js +22 -0
- package/dist/file-router/utils/findRoute.d.ts +8 -0
- package/dist/file-router/utils/findRoute.js +22 -0
- package/dist/file-router/utils/index.d.ts +4 -2
- package/dist/file-router/utils/index.js +4 -2
- package/dist/file-router/utils/logger.d.ts +6 -6
- package/dist/file-router/utils/logger.js +44 -4
- package/dist/file-router/utils/pathStrategy.d.ts +28 -0
- package/dist/file-router/utils/{namingStrategy.js → pathStrategy.js} +18 -28
- package/dist/file-router/utils/pathUtils.d.ts +31 -0
- package/dist/file-router/utils/pathUtils.js +53 -1
- package/dist/plugin-vite/constant.d.ts +9 -0
- package/dist/plugin-vite/constant.js +9 -0
- package/dist/plugin-vite/index.d.ts +4 -24
- package/dist/plugin-vite/index.js +4 -94
- package/dist/plugin-vite/plugin.d.ts +86 -0
- package/dist/plugin-vite/plugin.js +181 -0
- package/dist/plugin-vite/watcher.d.ts +15 -0
- package/dist/plugin-vite/watcher.js +65 -0
- package/package.json +9 -7
- package/dist/file-router/config/configUtils.d.ts +0 -54
- package/dist/file-router/config/configUtils.js +0 -88
- package/dist/file-router/scanner/filterUtils.d.ts +0 -35
- package/dist/file-router/scanner/filterUtils.js +0 -188
- package/dist/file-router/scanner/index.d.ts +0 -8
- package/dist/file-router/scanner/index.js +0 -8
- package/dist/file-router/scanner/routeTreeBuilder.d.ts +0 -21
- package/dist/file-router/scanner/routeTreeBuilder.js +0 -312
- package/dist/file-router/scanner/scanPages.d.ts +0 -48
- package/dist/file-router/scanner/scanPages.js +0 -174
- package/dist/file-router/types.d.ts +0 -344
- package/dist/file-router/utils/namingStrategy.d.ts +0 -57
- package/dist/file-router/utils/validateOptions.js +0 -233
- /package/dist/{plugin-vite/auto-routes → auto-routes}/handleHotUpdate.js +0 -0
- /package/dist/{plugin-vite/auto-routes → auto-routes}/index.d.ts +0 -0
- /package/dist/{plugin-vite/auto-routes → auto-routes}/index.js +0 -0
- /package/dist/file-router/{types.js → types/hooks.js} +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { getLazyLoader, isArray,
|
|
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
4
|
import { hasOnlyChangeHash, hasValidNavTarget, hasValidPath, hasValidRouteIndex, processGuardResult, registerHookTool, removePathSuffix, resolveNavTarget } from '../common/utils.js';
|
|
5
5
|
import { cloneRouteLocation, normalizePath, stringifyQuery } from '../shared/utils.js';
|
|
6
|
+
import { checkRouterOptions } from './checkOptions.js';
|
|
6
7
|
import { RouteManager } from './manager.js';
|
|
7
8
|
/**
|
|
8
9
|
* 路由器抽象基类
|
|
@@ -33,6 +34,16 @@ export class Router {
|
|
|
33
34
|
writable: true,
|
|
34
35
|
value: void 0
|
|
35
36
|
});
|
|
37
|
+
/**
|
|
38
|
+
* 只读路由位置对象
|
|
39
|
+
* @private
|
|
40
|
+
*/
|
|
41
|
+
Object.defineProperty(this, "_readonlyLocation", {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
configurable: true,
|
|
44
|
+
writable: true,
|
|
45
|
+
value: void 0
|
|
46
|
+
});
|
|
36
47
|
/**
|
|
37
48
|
* 存储就绪状态的 Promise(延迟创建)
|
|
38
49
|
* @private
|
|
@@ -158,12 +169,13 @@ export class Router {
|
|
|
158
169
|
matched: shallowReactive([]),
|
|
159
170
|
meta: shallowReactive({})
|
|
160
171
|
});
|
|
172
|
+
this._readonlyLocation = readonly(this._routeLocation);
|
|
161
173
|
}
|
|
162
174
|
/**
|
|
163
175
|
* 获取当前路由位置对象
|
|
164
176
|
*/
|
|
165
177
|
get route() {
|
|
166
|
-
return
|
|
178
|
+
return this._readonlyLocation;
|
|
167
179
|
}
|
|
168
180
|
/**
|
|
169
181
|
* 获取解析后的路由记录数组
|
|
@@ -362,7 +374,7 @@ export class Router {
|
|
|
362
374
|
* await router.push('/foo')
|
|
363
375
|
* await router.waitViewRender()
|
|
364
376
|
*
|
|
365
|
-
* // 此时 DOM
|
|
377
|
+
* // 此时 DOM 已更新
|
|
366
378
|
* console.log(document.querySelector('#app').innerHTML)
|
|
367
379
|
*/
|
|
368
380
|
async waitViewRender(navResult) {
|
|
@@ -373,6 +385,9 @@ export class Router {
|
|
|
373
385
|
await this.resolveComponents();
|
|
374
386
|
await nextTick();
|
|
375
387
|
}
|
|
388
|
+
// ============================================================
|
|
389
|
+
// 导航流程
|
|
390
|
+
// ============================================================
|
|
376
391
|
/**
|
|
377
392
|
* 处理首次导航
|
|
378
393
|
* 它会拦截 navigate 的结果,仅在首次调用时生效,用于控制 isReady 状态
|
|
@@ -391,138 +406,306 @@ export class Router {
|
|
|
391
406
|
return result;
|
|
392
407
|
}
|
|
393
408
|
/**
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
409
|
+
* 创建导航上下文
|
|
410
|
+
*
|
|
411
|
+
* 在每次导航开始时构建上下文对象,封装该次导航所需的所有状态:
|
|
412
|
+
* - 生成唯一任务ID,用于并发导航竞争检测
|
|
413
|
+
* - 重置重定向计数器(非重定向场景下)
|
|
414
|
+
* - 执行路由匹配,确定目标路由位置
|
|
415
|
+
* - 构建 NavigateResult 基础对象
|
|
416
|
+
* - 提供 checkRedirectLoop 和 hasChanged 闭包方法
|
|
417
|
+
*
|
|
418
|
+
* @param target - 导航目标
|
|
419
|
+
* @param fromRoute - 来源路由,默认使用当前路由位置
|
|
420
|
+
* @param redirectFrom - 重定向来源路由
|
|
421
|
+
* @returns 导航上下文对象
|
|
399
422
|
*/
|
|
400
|
-
|
|
401
|
-
//
|
|
423
|
+
createNavigationContext(target, fromRoute, redirectFrom) {
|
|
424
|
+
// 生成任务ID并标记为当前任务
|
|
402
425
|
const taskId = ++this._taskCounter;
|
|
403
426
|
this._currentTaskId = taskId;
|
|
404
|
-
//
|
|
427
|
+
// 新的导航任务(非重定向)重置重定向计数器
|
|
405
428
|
if (!redirectFrom) {
|
|
406
429
|
this._redirectCount = 0;
|
|
407
430
|
}
|
|
408
|
-
|
|
409
|
-
if (this._currentTaskId === taskId)
|
|
410
|
-
return false;
|
|
411
|
-
result.state = NavState.cancelled;
|
|
412
|
-
result.message = 'Navigation superseded by a newer navigation';
|
|
413
|
-
return true;
|
|
414
|
-
};
|
|
415
|
-
const checkRedirectLoop = (targetPath) => {
|
|
416
|
-
this._redirectCount++;
|
|
417
|
-
if (this._redirectCount > Router.MAX_REDIRECTS) {
|
|
418
|
-
throw new Error(`[Router] Detected infinite redirect loop: exceeded maximum redirects (${Router.MAX_REDIRECTS}). Last redirect was to "${targetPath}". Check your route configuration or navigation guards.`);
|
|
419
|
-
}
|
|
420
|
-
};
|
|
421
|
-
// 1. 解析目标路由
|
|
431
|
+
// 解析目标路由
|
|
422
432
|
const to = this.matchRoute(target, redirectFrom);
|
|
423
|
-
//
|
|
433
|
+
// 确定来源路由
|
|
424
434
|
const from = fromRoute ?? cloneRouteLocation(this._routeLocation);
|
|
425
|
-
//
|
|
435
|
+
// 构建导航结果基础对象
|
|
426
436
|
const result = {
|
|
427
|
-
state: NavState.success,
|
|
428
|
-
to,
|
|
429
|
-
from,
|
|
430
|
-
redirectFrom,
|
|
437
|
+
state: NavState.success,
|
|
438
|
+
to,
|
|
439
|
+
from,
|
|
440
|
+
redirectFrom,
|
|
431
441
|
message: 'Navigation successful'
|
|
432
442
|
};
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
443
|
+
return {
|
|
444
|
+
taskId,
|
|
445
|
+
result,
|
|
446
|
+
to,
|
|
447
|
+
from,
|
|
448
|
+
redirectFrom,
|
|
449
|
+
replace: !!target.replace,
|
|
450
|
+
/**
|
|
451
|
+
* 重定向循环检测闭包
|
|
452
|
+
* 每次重定向时递增计数器,超过最大限制时抛出错误
|
|
453
|
+
*/
|
|
454
|
+
checkRedirectLoop: (path) => {
|
|
455
|
+
this._redirectCount++;
|
|
456
|
+
if (this._redirectCount > Router.MAX_REDIRECTS) {
|
|
457
|
+
throw new Error(`[Router] Detected infinite redirect loop: exceeded maximum redirects (${Router.MAX_REDIRECTS}). Last redirect was to "${path}". Check your route configuration or navigation guards.`);
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
/**
|
|
461
|
+
* 并发竞争检测闭包
|
|
462
|
+
* 比较当前任务ID与最新任务ID,判断当前导航是否已被新导航取代
|
|
463
|
+
*/
|
|
464
|
+
hasChanged: () => {
|
|
465
|
+
if (this._currentTaskId === taskId)
|
|
466
|
+
return false;
|
|
467
|
+
result.state = NavState.cancelled;
|
|
468
|
+
result.message = 'Navigation superseded by a newer navigation';
|
|
469
|
+
return true;
|
|
444
470
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
return
|
|
465
|
-
}
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* 处理路由未匹配(404)场景
|
|
475
|
+
*
|
|
476
|
+
* 当目标路由无法匹配时执行:
|
|
477
|
+
* 1. 触发全局 onNotFound 钩子
|
|
478
|
+
* 2. 如果钩子返回了新的导航目标,进行重定向
|
|
479
|
+
* 3. 否则返回 notfound 状态
|
|
480
|
+
*
|
|
481
|
+
* @param context - 导航上下文
|
|
482
|
+
* @param target - 原始导航目标
|
|
483
|
+
* @returns 导航结果
|
|
484
|
+
*/
|
|
485
|
+
handleNotFound(context, target) {
|
|
486
|
+
const notFoundResult = this.runNotFoundHook(target);
|
|
487
|
+
// 钩子返回了新的导航目标,进行重定向
|
|
488
|
+
if (notFoundResult) {
|
|
489
|
+
context.checkRedirectLoop(String(notFoundResult.index));
|
|
490
|
+
return this.navigate(notFoundResult, context.from);
|
|
491
|
+
}
|
|
492
|
+
// 无钩子处理,返回未匹配结果
|
|
493
|
+
context.result.message = `No match found for target: ${JSON.stringify(target.index)}`;
|
|
494
|
+
context.result.state = NavState.notfound;
|
|
495
|
+
return context.result;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* 处理重复路由场景
|
|
499
|
+
*
|
|
500
|
+
* 当目标路由与当前路由的 href 和最终匹配记录完全相同时,
|
|
501
|
+
* 视为重复导航,返回 duplicated 状态,不执行后续流程。
|
|
502
|
+
*
|
|
503
|
+
* @param context - 导航上下文
|
|
504
|
+
* @returns 重复路由结果,或 null 表示非重复路由
|
|
505
|
+
*/
|
|
506
|
+
handleDuplicatedRoute(context) {
|
|
507
|
+
if (!context.to)
|
|
508
|
+
return null;
|
|
509
|
+
if (context.to.href === context.from.href &&
|
|
510
|
+
context.to.matched.at(-1) === context.from.matched.at(-1)) {
|
|
511
|
+
context.result.state = NavState.duplicated;
|
|
512
|
+
context.result.message = 'Navigation aborted due to the same route';
|
|
513
|
+
return context.result;
|
|
514
|
+
}
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* 处理仅 hash 变化场景
|
|
519
|
+
*
|
|
520
|
+
* 当路由路径和查询参数相同,仅 hash 部分发生变化时,
|
|
521
|
+
* 直接更新 hash 值并触发 hashUpdate 回调,不执行完整的导航流程。
|
|
522
|
+
*
|
|
523
|
+
* @param context - 导航上下文
|
|
524
|
+
* @returns hash 变化结果,或 null 表示非仅 hash 变化
|
|
525
|
+
*/
|
|
526
|
+
handleHashOnlyChange(context) {
|
|
527
|
+
if (!context.to)
|
|
528
|
+
return null;
|
|
529
|
+
if (hasOnlyChangeHash(context.to, context.from)) {
|
|
530
|
+
this._routeLocation.href = context.to.href;
|
|
531
|
+
this.hashUpdate?.(context.to);
|
|
532
|
+
context.result.state = NavState.success;
|
|
533
|
+
context.result.message = 'Navigation succeeded: only hash changed';
|
|
534
|
+
return context.result;
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* 处理路由重定向场景
|
|
540
|
+
*
|
|
541
|
+
* 当目标路由配置了 redirect 字段时,解析重定向目标并递归执行导航:
|
|
542
|
+
* 1. 支持 redirect 为函数(动态重定向)或静态值
|
|
543
|
+
* 2. 重定向目标可以是路由索引(RouteIndex)或导航目标(NavTarget)
|
|
544
|
+
* 3. 如果重定向配置无效且无组件定义,抛出错误
|
|
545
|
+
*
|
|
546
|
+
* @param context - 导航上下文
|
|
547
|
+
* @returns 重定向导航结果,或 null 表示无需重定向
|
|
548
|
+
*/
|
|
549
|
+
handleRedirect(context) {
|
|
550
|
+
if (!context.to)
|
|
551
|
+
return null;
|
|
552
|
+
const to = context.to;
|
|
469
553
|
const matched = to.matched.at(-1);
|
|
554
|
+
// 解析重定向配置:支持函数和静态值
|
|
470
555
|
const redirect = isFunction(matched.redirect)
|
|
471
556
|
? matched.redirect.call(this, to)
|
|
472
557
|
: matched.redirect;
|
|
473
|
-
if (redirect)
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
558
|
+
if (!redirect)
|
|
559
|
+
return null;
|
|
560
|
+
// 重定向目标为路由索引(路径或名称)
|
|
561
|
+
if (hasValidRouteIndex(redirect)) {
|
|
562
|
+
context.checkRedirectLoop(String(redirect));
|
|
563
|
+
return this.navigate({ index: redirect }, context.from, context.redirectFrom ?? to);
|
|
564
|
+
}
|
|
565
|
+
// 重定向目标为完整的导航目标对象
|
|
566
|
+
if (hasValidNavTarget(redirect)) {
|
|
567
|
+
context.checkRedirectLoop(String(redirect.index));
|
|
568
|
+
return this.navigate(redirect, context.from, context.redirectFrom ?? to);
|
|
485
569
|
}
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
570
|
+
// 重定向配置无效且无组件定义,抛出错误
|
|
571
|
+
if (!matched.component) {
|
|
572
|
+
throw new Error(`[Router] Navigation failed: The redirect configuration for the matching destination route is invalid and the components are not defined, check the configuration of the ${to.path} route`);
|
|
573
|
+
}
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* 执行守卫流程
|
|
578
|
+
*
|
|
579
|
+
* 按顺序执行路由离开守卫和全局前置守卫,
|
|
580
|
+
* 在每个异步守卫执行后进行并发竞争检测。
|
|
581
|
+
* 如果守卫拦截导航或触发重定向,返回对应结果;
|
|
582
|
+
* 如果守卫全部通过,返回 null 表示继续导航。
|
|
583
|
+
*
|
|
584
|
+
* @param context - 导航上下文
|
|
585
|
+
* @returns 守卫拦截/重定向结果,或 null 表示守卫全部通过
|
|
586
|
+
*/
|
|
587
|
+
async executeGuards(context) {
|
|
588
|
+
if (!context.to)
|
|
589
|
+
return null;
|
|
489
590
|
try {
|
|
490
|
-
//
|
|
491
|
-
const leaveGuardResult = await this.runRouteLeaveGuards(to, from);
|
|
492
|
-
|
|
493
|
-
|
|
591
|
+
// 执行离开守卫(从内到外)
|
|
592
|
+
const leaveGuardResult = await this.runRouteLeaveGuards(context.to, context.from);
|
|
593
|
+
// 并发竞争检测:离开守卫是异步的,执行期间可能有新导航
|
|
594
|
+
if (context.hasChanged())
|
|
595
|
+
return context.result;
|
|
596
|
+
// 离开守卫拦截
|
|
494
597
|
if (!leaveGuardResult) {
|
|
495
|
-
result.state = NavState.aborted;
|
|
496
|
-
result.message = 'Navigation aborted by leave guard';
|
|
497
|
-
return result;
|
|
498
|
-
}
|
|
499
|
-
// 执行全局前置守卫
|
|
500
|
-
const guardResult = await this.runBeforeGuards(to, from);
|
|
501
|
-
// 7.1 并发竞争检查
|
|
502
|
-
if (hasChanged())
|
|
503
|
-
return result;
|
|
504
|
-
// 7.2 守卫拦截
|
|
505
|
-
if (guardResult === false) {
|
|
506
|
-
result.state = NavState.aborted;
|
|
507
|
-
result.message = 'Navigation aborted by before guard';
|
|
508
|
-
return result;
|
|
509
|
-
}
|
|
510
|
-
// 7.3 守卫重定向
|
|
511
|
-
if (hasValidNavTarget(guardResult)) {
|
|
512
|
-
checkRedirectLoop(String(guardResult.index));
|
|
513
|
-
// 直接返回递归结果,如果内部 Reject 会自动向上传播
|
|
514
|
-
return this.navigate(guardResult, from, redirectFrom ?? to);
|
|
598
|
+
context.result.state = NavState.aborted;
|
|
599
|
+
context.result.message = 'Navigation aborted by leave guard';
|
|
600
|
+
return context.result;
|
|
515
601
|
}
|
|
602
|
+
// 执行前置守卫(全局 → 路由独享)
|
|
603
|
+
const guardResult = await this.runBeforeGuards(context.to, context.from);
|
|
604
|
+
// 并发竞争检测:前置守卫也是异步的
|
|
605
|
+
if (context.hasChanged())
|
|
606
|
+
return context.result;
|
|
607
|
+
// 处理前置守卫结果
|
|
608
|
+
return this.handleGuardResult(context, guardResult);
|
|
516
609
|
}
|
|
517
610
|
catch (error) {
|
|
518
611
|
// 捕获守卫内部的同步/异步错误
|
|
519
|
-
this.reportError(error, to, from);
|
|
612
|
+
this.reportError(error, context.to, context.from);
|
|
520
613
|
return Promise.reject(error);
|
|
521
614
|
}
|
|
522
|
-
const scrollPosition = this[target.replace ? 'replaceHistory' : 'pushHistory'](to);
|
|
523
|
-
this.completeNavigation(to, from, scrollPosition ?? null);
|
|
524
|
-
return result;
|
|
525
615
|
}
|
|
616
|
+
/**
|
|
617
|
+
* 处理前置守卫执行结果
|
|
618
|
+
*
|
|
619
|
+
* 根据守卫返回值决定导航走向:
|
|
620
|
+
* - false: 拦截导航,返回 aborted 状态
|
|
621
|
+
* - NavTarget: 重定向到新目标
|
|
622
|
+
* - 其他(true/void): 放行,继续导航
|
|
623
|
+
*
|
|
624
|
+
* @param context - 导航上下文
|
|
625
|
+
* @param guardResult - 前置守卫的返回值
|
|
626
|
+
* @returns 拦截/重定向结果,或 null 表示守卫通过
|
|
627
|
+
*/
|
|
628
|
+
handleGuardResult(context, guardResult) {
|
|
629
|
+
// 守卫拦截
|
|
630
|
+
if (guardResult === false) {
|
|
631
|
+
context.result.state = NavState.aborted;
|
|
632
|
+
context.result.message = 'Navigation aborted by before guard';
|
|
633
|
+
return context.result;
|
|
634
|
+
}
|
|
635
|
+
// 守卫重定向
|
|
636
|
+
if (hasValidNavTarget(guardResult)) {
|
|
637
|
+
context.checkRedirectLoop(String(guardResult.index));
|
|
638
|
+
return this.navigate(guardResult, context.from, context.redirectFrom ?? context.to ?? undefined);
|
|
639
|
+
}
|
|
640
|
+
// 守卫通过,继续导航
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* 完成导航流程
|
|
645
|
+
*
|
|
646
|
+
* 守卫全部通过后执行最后的导航确认:
|
|
647
|
+
* 1. 根据替换标记更新历史记录(push 或 replace)
|
|
648
|
+
* 2. 调用 completeNavigation 更新路由状态、触发后置钩子和滚动行为
|
|
649
|
+
*
|
|
650
|
+
* @param context - 导航上下文
|
|
651
|
+
* @returns 导航成功结果
|
|
652
|
+
*/
|
|
653
|
+
finalizeNavigation(context) {
|
|
654
|
+
if (!context.to)
|
|
655
|
+
return context.result;
|
|
656
|
+
const scrollPosition = this[context.replace ? 'replaceHistory' : 'pushHistory'](context.to);
|
|
657
|
+
this.completeNavigation(context.to, context.from, scrollPosition ?? null);
|
|
658
|
+
return context.result;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* 导航到指定位置
|
|
662
|
+
*
|
|
663
|
+
* 作为导航流程的编排器,按顺序协调各场景处理方法的执行:
|
|
664
|
+
* 1. 创建导航上下文(路由匹配、并发控制初始化)
|
|
665
|
+
* 2. 处理 404 场景(路由未匹配)
|
|
666
|
+
* 3. 处理重复路由场景
|
|
667
|
+
* 4. 处理仅 hash 变化场景
|
|
668
|
+
* 5. 处理路由重定向场景
|
|
669
|
+
* 6. 执行守卫流程(离开守卫 → 前置守卫)
|
|
670
|
+
* 7. 完成导航(更新历史记录、触发后置钩子)
|
|
671
|
+
*
|
|
672
|
+
* @param target - 导航目标对象 | 路由位置对象
|
|
673
|
+
* @param fromRoute - 来源路由对象
|
|
674
|
+
* @param redirectFrom - 重定向来源对象
|
|
675
|
+
* @returns - 返回导航结果
|
|
676
|
+
*/
|
|
677
|
+
async navigate(target, fromRoute, redirectFrom) {
|
|
678
|
+
// 创建导航上下文
|
|
679
|
+
const context = this.createNavigationContext(target, fromRoute, redirectFrom);
|
|
680
|
+
// 场景1: 路由未匹配 (404)
|
|
681
|
+
if (!context.to) {
|
|
682
|
+
return this.handleNotFound(context, target);
|
|
683
|
+
}
|
|
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);
|
|
705
|
+
}
|
|
706
|
+
// ============================================================
|
|
707
|
+
// 导航完成与滚动行为
|
|
708
|
+
// ============================================================
|
|
526
709
|
/**
|
|
527
710
|
* 完成导航过程
|
|
528
711
|
* 更新路由状态并触发相关的生命周期钩子
|
|
@@ -581,6 +764,9 @@ export class Router {
|
|
|
581
764
|
}
|
|
582
765
|
});
|
|
583
766
|
}
|
|
767
|
+
// ============================================================
|
|
768
|
+
// 守卫与钩子执行
|
|
769
|
+
// ============================================================
|
|
584
770
|
/**
|
|
585
771
|
* 处理 404 错误
|
|
586
772
|
* @param target - 导航目标对象
|
|
@@ -739,6 +925,9 @@ export class Router {
|
|
|
739
925
|
}
|
|
740
926
|
}
|
|
741
927
|
}
|
|
928
|
+
// ============================================================
|
|
929
|
+
// 路由匹配与URL构建
|
|
930
|
+
// ============================================================
|
|
742
931
|
/**
|
|
743
932
|
* 创建缺失的路由
|
|
744
933
|
* @param component - 路由组件
|
|
@@ -798,18 +987,16 @@ export class Router {
|
|
|
798
987
|
let match;
|
|
799
988
|
if (isPath) {
|
|
800
989
|
match = this.manager.matchByPath(matchTarget);
|
|
990
|
+
if (!match && this.config.missing) {
|
|
991
|
+
return this.createMissingRoute(this.config.missing, matchTarget, target.query, target.hash);
|
|
992
|
+
}
|
|
801
993
|
}
|
|
802
994
|
else {
|
|
803
995
|
match = this.manager.matchByName(matchTarget, target.params);
|
|
804
996
|
}
|
|
805
|
-
//
|
|
806
|
-
if (!match)
|
|
807
|
-
const component = this.config.missing;
|
|
808
|
-
if (component && hasValidPath(target.index)) {
|
|
809
|
-
return this.createMissingRoute(component, target.index, target.query, target.hash);
|
|
810
|
-
}
|
|
997
|
+
// 如果没有匹配到路由,则返回 null
|
|
998
|
+
if (!match)
|
|
811
999
|
return null;
|
|
812
|
-
}
|
|
813
1000
|
// 获取匹配的路由信息
|
|
814
1001
|
const route = match.route;
|
|
815
1002
|
const path = match.path;
|
|
@@ -859,117 +1046,3 @@ Object.defineProperty(Router, "MAX_REDIRECTS", {
|
|
|
859
1046
|
writable: true,
|
|
860
1047
|
value: 16
|
|
861
1048
|
});
|
|
862
|
-
/**
|
|
863
|
-
* 检查路由器配置选项的合法性
|
|
864
|
-
* @param options 用户传入的 RouterOptions 对象
|
|
865
|
-
* @throws {Error} 当选项不合法时抛出错误
|
|
866
|
-
*/
|
|
867
|
-
const checkRouterOptions = (options) => {
|
|
868
|
-
// 1. 检查 options 是否为对象
|
|
869
|
-
if (typeof options !== 'object' || options === null) {
|
|
870
|
-
throw new Error('[Router] Router options must be an object.');
|
|
871
|
-
}
|
|
872
|
-
// 2. 检查 routes 是否存在且为有效类型
|
|
873
|
-
if (!('routes' in options) || options.routes === undefined) {
|
|
874
|
-
throw new Error('[Router] "routes" is a required option.');
|
|
875
|
-
}
|
|
876
|
-
// 更新判断逻辑以匹配新的 RouteManager 命名
|
|
877
|
-
if (!Array.isArray(options.routes) && !(options.routes instanceof RouteManager)) {
|
|
878
|
-
throw new Error('[Router] "routes" must be an array or a RouteManager instance.');
|
|
879
|
-
}
|
|
880
|
-
// 3. 检查 mode 的值是否合法
|
|
881
|
-
if ('mode' in options && options.mode !== undefined) {
|
|
882
|
-
const validModes = ['hash', 'path'];
|
|
883
|
-
if (!validModes.includes(options.mode)) {
|
|
884
|
-
throw new Error(`[Router] "mode" must be one of: ${validModes.join(', ')}. Received "${options.mode}".`);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
// 4. 检查 base 的格式
|
|
888
|
-
if ('base' in options && options.base !== undefined) {
|
|
889
|
-
if (typeof options.base !== 'string') {
|
|
890
|
-
throw new Error('[Router] "base" must be a string.');
|
|
891
|
-
}
|
|
892
|
-
if (!options.base.startsWith('/')) {
|
|
893
|
-
throw new Error('[Router] "base" must start with a slash (/).');
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
// 5. 检查 suffix 的格式
|
|
897
|
-
if ('suffix' in options && options.suffix !== undefined) {
|
|
898
|
-
if (typeof options.suffix !== 'string') {
|
|
899
|
-
throw new Error('[Router] "suffix" must be a string.');
|
|
900
|
-
}
|
|
901
|
-
if (!options.suffix.startsWith('.')) {
|
|
902
|
-
throw new Error('[Router] "suffix" must start with a dot (.).');
|
|
903
|
-
}
|
|
904
|
-
if (options.suffix === '.') {
|
|
905
|
-
throw new Error('[Router] "suffix" cannot be just a dot, please provide a valid extension like ".html".');
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
// 6. 检查 props 的类型
|
|
909
|
-
if ('props' in options && options.props !== undefined) {
|
|
910
|
-
if (!isBool(options.props) && !isFunction(options.props)) {
|
|
911
|
-
throw new Error('[Router] "props" must be a boolean or function.');
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
// 7. 检查 scrollBehavior 的类型
|
|
915
|
-
if ('scrollBehavior' in options && options.scrollBehavior !== undefined) {
|
|
916
|
-
if (!isFunction(options.scrollBehavior)) {
|
|
917
|
-
throw new Error('[Router] "scrollBehavior" must be a function.');
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
// 8. 检查钩子函数的类型
|
|
921
|
-
if ('beforeEach' in options && options.beforeEach !== undefined) {
|
|
922
|
-
if (!isFunction(options.beforeEach) && !Array.isArray(options.beforeEach)) {
|
|
923
|
-
throw new Error('[Router] "beforeEach" must be a function or an array of functions.');
|
|
924
|
-
}
|
|
925
|
-
if (Array.isArray(options.beforeEach)) {
|
|
926
|
-
options.beforeEach.forEach((hook, index) => {
|
|
927
|
-
if (!isFunction(hook)) {
|
|
928
|
-
throw new Error(`[Router] "beforeEach" must be a function or an array of functions. Index: ${index}`);
|
|
929
|
-
}
|
|
930
|
-
});
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
if ('afterEach' in options && options.afterEach !== undefined) {
|
|
934
|
-
if (!isFunction(options.afterEach) && !Array.isArray(options.afterEach)) {
|
|
935
|
-
throw new Error('[Router] "afterEach" must be a function or an array of functions.');
|
|
936
|
-
}
|
|
937
|
-
if (Array.isArray(options.afterEach)) {
|
|
938
|
-
options.afterEach.forEach((hook, index) => {
|
|
939
|
-
if (!isFunction(hook)) {
|
|
940
|
-
throw new Error(`[Router] "afterEach" must be a function or an array of functions. Index: ${index}`);
|
|
941
|
-
}
|
|
942
|
-
});
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
if ('onNotFound' in options && options.onNotFound !== undefined) {
|
|
946
|
-
if (!isFunction(options.onNotFound) && !Array.isArray(options.onNotFound)) {
|
|
947
|
-
throw new Error('[Router] "onNotFound" must be a function or an array of functions.');
|
|
948
|
-
}
|
|
949
|
-
if (Array.isArray(options.onNotFound)) {
|
|
950
|
-
options.onNotFound.forEach((hook, index) => {
|
|
951
|
-
if (!isFunction(hook)) {
|
|
952
|
-
throw new Error(`[Router] "onNotFound" must be a function or an array of functions. Index: ${index}`);
|
|
953
|
-
}
|
|
954
|
-
});
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
if ('onError' in options && options.onError !== undefined) {
|
|
958
|
-
if (!isFunction(options.onError) && !Array.isArray(options.onError)) {
|
|
959
|
-
throw new Error('[Router] "onError" must be a function or an array of functions.');
|
|
960
|
-
}
|
|
961
|
-
if (Array.isArray(options.onError)) {
|
|
962
|
-
options.onError.forEach((hook, index) => {
|
|
963
|
-
if (!isFunction(hook)) {
|
|
964
|
-
throw new Error(`[Router] "onError" must be a function or an array of functions. Index: ${index}`);
|
|
965
|
-
}
|
|
966
|
-
});
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
// 9. 检查 missing 组件的类型
|
|
970
|
-
if ('missing' in options && options.missing !== undefined) {
|
|
971
|
-
if (!isComponent(options.missing)) {
|
|
972
|
-
throw new Error('[Router] "missing" must be a valid component.');
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
};
|
|
@@ -2,7 +2,20 @@ import type { RouteLocation, RouterOptions, ScrollPosition, ScrollTarget } from
|
|
|
2
2
|
import { Router } from './router.js';
|
|
3
3
|
export declare class WebRouter extends Router {
|
|
4
4
|
private readonly history;
|
|
5
|
+
private initialized;
|
|
5
6
|
constructor(options: RouterOptions);
|
|
7
|
+
/**
|
|
8
|
+
* 初始化路由器实例。
|
|
9
|
+
*
|
|
10
|
+
* 如果实例已经初始化,则直接返回当前实例以防止重复初始化。
|
|
11
|
+
* 初始化过程中会执行以下操作:
|
|
12
|
+
* 1. 根据目标 URL 执行初始路由替换;
|
|
13
|
+
* 2. 监听 `popstate` 事件,以处理浏览器历史记录返回时的路由恢复;
|
|
14
|
+
* 3. 监听 `hashchange` 事件,以处理浏览器 hash 值变化时的路由恢复。
|
|
15
|
+
*
|
|
16
|
+
* @returns {this} 返回当前路由器实例,支持链式调用。
|
|
17
|
+
*/
|
|
18
|
+
init(): this;
|
|
6
19
|
/**
|
|
7
20
|
* @inheritDoc
|
|
8
21
|
*/
|