vitarx-router 4.0.0-beta.6 → 4.0.0-beta.8

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.
@@ -0,0 +1,11 @@
1
+ import type { RouterOptions } from '../types/index.js';
2
+ /**
3
+ * 检查路由器配置选项的合法性
4
+ *
5
+ * 在开发环境下对用户传入的 RouterOptions 进行校验,
6
+ * 确保必填字段存在、类型正确、值合法。
7
+ *
8
+ * @param options - 用户传入的路由器配置对象
9
+ * @throws {Error} 当选项不合法时抛出错误
10
+ */
11
+ export declare const checkRouterOptions: (options: RouterOptions) => void;
@@ -0,0 +1,119 @@
1
+ import { isComponent, isFunction } from 'vitarx';
2
+ import { RouteManager } from './manager.js';
3
+ /**
4
+ * 检查路由器配置选项的合法性
5
+ *
6
+ * 在开发环境下对用户传入的 RouterOptions 进行校验,
7
+ * 确保必填字段存在、类型正确、值合法。
8
+ *
9
+ * @param options - 用户传入的路由器配置对象
10
+ * @throws {Error} 当选项不合法时抛出错误
11
+ */
12
+ export const checkRouterOptions = (options) => {
13
+ // 1. 检查 options 是否为对象
14
+ if (typeof options !== 'object' || options === null) {
15
+ throw new Error('[Router] Router options must be an object.');
16
+ }
17
+ // 2. 检查 routes 是否存在且为有效类型
18
+ if (!('routes' in options) || options.routes === undefined) {
19
+ throw new Error('[Router] "routes" is a required option.');
20
+ }
21
+ if (!Array.isArray(options.routes) && !(options.routes instanceof RouteManager)) {
22
+ throw new Error('[Router] "routes" must be an array or a RouteManager instance.');
23
+ }
24
+ // 3. 检查 mode 的值是否合法
25
+ if ('mode' in options && options.mode !== undefined) {
26
+ const validModes = ['hash', 'path'];
27
+ if (!validModes.includes(options.mode)) {
28
+ throw new Error(`[Router] "mode" must be one of: ${validModes.join(', ')}. Received "${options.mode}".`);
29
+ }
30
+ }
31
+ // 4. 检查 base 的格式
32
+ if ('base' in options && options.base !== undefined) {
33
+ if (typeof options.base !== 'string') {
34
+ throw new Error('[Router] "base" must be a string.');
35
+ }
36
+ if (!options.base.startsWith('/')) {
37
+ throw new Error('[Router] "base" must start with a slash (/).');
38
+ }
39
+ }
40
+ // 5. 检查 suffix 的格式
41
+ if ('suffix' in options && options.suffix !== undefined) {
42
+ if (typeof options.suffix !== 'string') {
43
+ throw new Error('[Router] "suffix" must be a string.');
44
+ }
45
+ if (!options.suffix.startsWith('.')) {
46
+ throw new Error('[Router] "suffix" must start with a dot (.).');
47
+ }
48
+ if (options.suffix === '.') {
49
+ throw new Error('[Router] "suffix" cannot be just a dot, please provide a valid extension like ".html".');
50
+ }
51
+ }
52
+ // 6. 检查 props 的类型
53
+ if ('props' in options && options.props !== undefined) {
54
+ if (typeof options.props !== 'boolean' && !isFunction(options.props)) {
55
+ throw new Error('[Router] "props" must be a boolean or function.');
56
+ }
57
+ }
58
+ // 7. 检查 scrollBehavior 的类型
59
+ if ('scrollBehavior' in options && options.scrollBehavior !== undefined) {
60
+ if (!isFunction(options.scrollBehavior)) {
61
+ throw new Error('[Router] "scrollBehavior" must be a function.');
62
+ }
63
+ }
64
+ // 8. 检查钩子函数的类型
65
+ if ('beforeEach' in options && options.beforeEach !== undefined) {
66
+ if (!isFunction(options.beforeEach) && !Array.isArray(options.beforeEach)) {
67
+ throw new Error('[Router] "beforeEach" must be a function or an array of functions.');
68
+ }
69
+ if (Array.isArray(options.beforeEach)) {
70
+ options.beforeEach.forEach((hook, index) => {
71
+ if (!isFunction(hook)) {
72
+ throw new Error(`[Router] "beforeEach" must be a function or an array of functions. Index: ${index}`);
73
+ }
74
+ });
75
+ }
76
+ }
77
+ if ('afterEach' in options && options.afterEach !== undefined) {
78
+ if (!isFunction(options.afterEach) && !Array.isArray(options.afterEach)) {
79
+ throw new Error('[Router] "afterEach" must be a function or an array of functions.');
80
+ }
81
+ if (Array.isArray(options.afterEach)) {
82
+ options.afterEach.forEach((hook, index) => {
83
+ if (!isFunction(hook)) {
84
+ throw new Error(`[Router] "afterEach" must be a function or an array of functions. Index: ${index}`);
85
+ }
86
+ });
87
+ }
88
+ }
89
+ if ('onNotFound' in options && options.onNotFound !== undefined) {
90
+ if (!isFunction(options.onNotFound) && !Array.isArray(options.onNotFound)) {
91
+ throw new Error('[Router] "onNotFound" must be a function or an array of functions.');
92
+ }
93
+ if (Array.isArray(options.onNotFound)) {
94
+ options.onNotFound.forEach((hook, index) => {
95
+ if (!isFunction(hook)) {
96
+ throw new Error(`[Router] "onNotFound" must be a function or an array of functions. Index: ${index}`);
97
+ }
98
+ });
99
+ }
100
+ }
101
+ if ('onError' in options && options.onError !== undefined) {
102
+ if (!isFunction(options.onError) && !Array.isArray(options.onError)) {
103
+ throw new Error('[Router] "onError" must be a function or an array of functions.');
104
+ }
105
+ if (Array.isArray(options.onError)) {
106
+ options.onError.forEach((hook, index) => {
107
+ if (!isFunction(hook)) {
108
+ throw new Error(`[Router] "onError" must be a function or an array of functions. Index: ${index}`);
109
+ }
110
+ });
111
+ }
112
+ }
113
+ // 9. 检查 missing 组件的类型
114
+ if ('missing' in options && options.missing !== undefined) {
115
+ if (!isComponent(options.missing)) {
116
+ throw new Error('[Router] "missing" must be a valid component.');
117
+ }
118
+ }
119
+ };
@@ -1,6 +1,47 @@
1
1
  import { App, type ReadonlyObject } from 'vitarx';
2
2
  import type { AfterCallback, NavigateResult, NavigationGuard, NavOptions, NavTarget, ResolvedRouterConfig, Route, RouteIndex, RouteLocation, RouteRecord, RouterOptions, ScrollPosition, ScrollTarget, URLHash, URLQuery } from '../types/index.js';
3
3
  import { RouteManager } from './manager.js';
4
+ /**
5
+ * 导航上下文
6
+ *
7
+ * 在每次导航过程中创建,封装该次导航所需的所有状态和操作。
8
+ * 作为各导航阶段处理方法之间的共享数据载体,避免方法间传递大量独立参数。
9
+ *
10
+ * @internal
11
+ */
12
+ export interface NavigationContext {
13
+ /** 当前导航任务的唯一标识,用于并发导航竞争检测 */
14
+ taskId: number;
15
+ /** 导航结果对象,各阶段处理方法可修改其 state 和 message */
16
+ result: NavigateResult;
17
+ /** 目标路由位置,匹配失败时为 null */
18
+ to: RouteLocation | null;
19
+ /** 来源路由位置 */
20
+ from: RouteLocation;
21
+ /** 重定向来源路由位置,仅在重定向链中存在 */
22
+ redirectFrom: RouteLocation | undefined;
23
+ /** 是否替换当前历史记录(而非推入新记录) */
24
+ replace: boolean;
25
+ /**
26
+ * 检测重定向循环
27
+ *
28
+ * 每次重定向时调用,递增重定向计数器。
29
+ * 当重定向次数超过最大限制时抛出错误,防止无限循环。
30
+ *
31
+ * @param path - 重定向目标路径,用于错误信息
32
+ * @throws {Error} 当重定向次数超过 MAX_REDIRECTS 时
33
+ */
34
+ checkRedirectLoop: (path: string) => void;
35
+ /**
36
+ * 检测并发导航竞争
37
+ *
38
+ * 判断当前导航任务是否已被更新的导航任务取代。
39
+ * 如果已被取代,则将结果状态标记为 cancelled。
40
+ *
41
+ * @returns true 表示当前导航已被新导航取代,应中止执行
42
+ */
43
+ hasChanged: () => boolean;
44
+ }
4
45
  /**
5
46
  * 路由器抽象基类
6
47
  *
@@ -257,8 +298,115 @@ export declare abstract class Router {
257
298
  * @returns 返回标准的导航结果 Promise
258
299
  */
259
300
  private initialNavigation;
301
+ /**
302
+ * 创建导航上下文
303
+ *
304
+ * 在每次导航开始时构建上下文对象,封装该次导航所需的所有状态:
305
+ * - 生成唯一任务ID,用于并发导航竞争检测
306
+ * - 重置重定向计数器(非重定向场景下)
307
+ * - 执行路由匹配,确定目标路由位置
308
+ * - 构建 NavigateResult 基础对象
309
+ * - 提供 checkRedirectLoop 和 hasChanged 闭包方法
310
+ *
311
+ * @param target - 导航目标
312
+ * @param fromRoute - 来源路由,默认使用当前路由位置
313
+ * @param redirectFrom - 重定向来源路由
314
+ * @returns 导航上下文对象
315
+ */
316
+ private createNavigationContext;
317
+ /**
318
+ * 处理路由未匹配(404)场景
319
+ *
320
+ * 当目标路由无法匹配时执行:
321
+ * 1. 触发全局 onNotFound 钩子
322
+ * 2. 如果钩子返回了新的导航目标,进行重定向
323
+ * 3. 否则返回 notfound 状态
324
+ *
325
+ * @param context - 导航上下文
326
+ * @param target - 原始导航目标
327
+ * @returns 导航结果
328
+ */
329
+ private handleNotFound;
330
+ /**
331
+ * 处理重复路由场景
332
+ *
333
+ * 当目标路由与当前路由的 href 和最终匹配记录完全相同时,
334
+ * 视为重复导航,返回 duplicated 状态,不执行后续流程。
335
+ *
336
+ * @param context - 导航上下文
337
+ * @returns 重复路由结果,或 null 表示非重复路由
338
+ */
339
+ private handleDuplicatedRoute;
340
+ /**
341
+ * 处理仅 hash 变化场景
342
+ *
343
+ * 当路由路径和查询参数相同,仅 hash 部分发生变化时,
344
+ * 直接更新 hash 值并触发 hashUpdate 回调,不执行完整的导航流程。
345
+ *
346
+ * @param context - 导航上下文
347
+ * @returns hash 变化结果,或 null 表示非仅 hash 变化
348
+ */
349
+ private handleHashOnlyChange;
350
+ /**
351
+ * 处理路由重定向场景
352
+ *
353
+ * 当目标路由配置了 redirect 字段时,解析重定向目标并递归执行导航:
354
+ * 1. 支持 redirect 为函数(动态重定向)或静态值
355
+ * 2. 重定向目标可以是路由索引(RouteIndex)或导航目标(NavTarget)
356
+ * 3. 如果重定向配置无效且无组件定义,抛出错误
357
+ *
358
+ * @param context - 导航上下文
359
+ * @returns 重定向导航结果,或 null 表示无需重定向
360
+ */
361
+ private handleRedirect;
362
+ /**
363
+ * 执行守卫流程
364
+ *
365
+ * 按顺序执行路由离开守卫和全局前置守卫,
366
+ * 在每个异步守卫执行后进行并发竞争检测。
367
+ * 如果守卫拦截导航或触发重定向,返回对应结果;
368
+ * 如果守卫全部通过,返回 null 表示继续导航。
369
+ *
370
+ * @param context - 导航上下文
371
+ * @returns 守卫拦截/重定向结果,或 null 表示守卫全部通过
372
+ */
373
+ private executeGuards;
374
+ /**
375
+ * 处理前置守卫执行结果
376
+ *
377
+ * 根据守卫返回值决定导航走向:
378
+ * - false: 拦截导航,返回 aborted 状态
379
+ * - NavTarget: 重定向到新目标
380
+ * - 其他(true/void): 放行,继续导航
381
+ *
382
+ * @param context - 导航上下文
383
+ * @param guardResult - 前置守卫的返回值
384
+ * @returns 拦截/重定向结果,或 null 表示守卫通过
385
+ */
386
+ private handleGuardResult;
387
+ /**
388
+ * 完成导航流程
389
+ *
390
+ * 守卫全部通过后执行最后的导航确认:
391
+ * 1. 根据替换标记更新历史记录(push 或 replace)
392
+ * 2. 调用 completeNavigation 更新路由状态、触发后置钩子和滚动行为
393
+ *
394
+ * @param context - 导航上下文
395
+ * @returns 导航成功结果
396
+ */
397
+ private finalizeNavigation;
260
398
  /**
261
399
  * 导航到指定位置
400
+ *
401
+ * 作为导航流程的编排器,按顺序协调各场景处理方法的执行:
402
+ * 1. 创建导航上下文(路由匹配、并发控制初始化)
403
+ * 2. 处理 404 场景(路由未匹配)
404
+ * 3. 处理重复路由场景
405
+ * 4. 处理仅 hash 变化场景
406
+ * 5. 处理路由重定向场景
407
+ * 6. 执行守卫流程(离开守卫 → 前置守卫)
408
+ * 7. 完成导航(更新历史记录、触发后置钩子)
409
+ *
262
410
  * @param target - 导航目标对象 | 路由位置对象
263
411
  * @param fromRoute - 来源路由对象
264
412
  * @param redirectFrom - 重定向来源对象