@wsxjs/wsx-router 0.0.16 → 0.0.18
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/LICENSE +2 -2
- package/README.md +3 -3
- package/dist/index.cjs +1 -1
- package/dist/index.js +1209 -582
- package/package.json +9 -7
- package/src/RouterUtils.ts +34 -4
- package/src/WsxLink.css +1 -1
- package/src/WsxLink.wsx +2 -1
- package/src/WsxRouter.css +6 -3
- package/src/WsxRouter.wsx +335 -44
- package/src/WsxView.wsx +104 -31
- package/src/index.ts +1 -1
- package/src/types/wsx.d.ts +3 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wsxjs/wsx-router",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "WSX Router - Native History API-based routing for
|
|
3
|
+
"version": "0.0.18",
|
|
4
|
+
"description": "WSX Router - Native History API-based routing for WSXJS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"!**/test"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@wsxjs/wsx-core": "0.0.
|
|
21
|
+
"@wsxjs/wsx-core": "0.0.18",
|
|
22
|
+
"@wsxjs/wsx-logger": "0.0.18"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
|
@@ -30,8 +31,8 @@
|
|
|
30
31
|
"typescript": "^5.0.0",
|
|
31
32
|
"vite": "^5.4.19",
|
|
32
33
|
"vitest": "^2.1.8",
|
|
33
|
-
"@wsxjs/eslint-plugin-wsx": "0.0.
|
|
34
|
-
"@wsxjs/wsx-vite-plugin": "0.0.
|
|
34
|
+
"@wsxjs/eslint-plugin-wsx": "0.0.18",
|
|
35
|
+
"@wsxjs/wsx-vite-plugin": "0.0.18"
|
|
35
36
|
},
|
|
36
37
|
"keywords": [
|
|
37
38
|
"wsx",
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
"navigation",
|
|
42
43
|
"spa"
|
|
43
44
|
],
|
|
44
|
-
"author": "
|
|
45
|
+
"author": "WSXJS Team",
|
|
45
46
|
"license": "MIT",
|
|
46
47
|
"repository": {
|
|
47
48
|
"type": "git",
|
|
@@ -56,7 +57,8 @@
|
|
|
56
57
|
"build:dev": "NODE_ENV=development vite build",
|
|
57
58
|
"dev": "NODE_ENV=development vite build --watch",
|
|
58
59
|
"clean": "rm -rf dist",
|
|
59
|
-
"test": "vitest",
|
|
60
|
+
"test": "vitest --run",
|
|
61
|
+
"test:watch": "vitest",
|
|
60
62
|
"test:ui": "vitest --ui",
|
|
61
63
|
"test:coverage": "vitest --coverage",
|
|
62
64
|
"typecheck": "tsc --noEmit",
|
package/src/RouterUtils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createLogger } from "@wsxjs/wsx-
|
|
1
|
+
import { createLogger } from "@wsxjs/wsx-logger";
|
|
2
2
|
|
|
3
3
|
const logger = createLogger("RouterUtils");
|
|
4
4
|
|
|
@@ -26,6 +26,20 @@ export interface RouteMatch {
|
|
|
26
26
|
* 路由工具类 - 提供路由相关的辅助函数
|
|
27
27
|
*/
|
|
28
28
|
export class RouterUtils {
|
|
29
|
+
/**
|
|
30
|
+
* 当前路由信息(由 WsxRouter 维护)
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
private static _currentRoute: RouteInfo | null = null;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 设置当前路由信息(由 WsxRouter 内部调用)
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
static _setCurrentRoute(routeInfo: RouteInfo): void {
|
|
40
|
+
RouterUtils._currentRoute = routeInfo;
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
/**
|
|
30
44
|
* 编程式导航
|
|
31
45
|
*/
|
|
@@ -43,12 +57,20 @@ export class RouterUtils {
|
|
|
43
57
|
|
|
44
58
|
/**
|
|
45
59
|
* 获取当前路由信息
|
|
60
|
+
* 如果路由已匹配(由 WsxRouter 维护),则返回包含参数的路由信息
|
|
61
|
+
* 否则返回基础路由信息(参数为空对象)
|
|
46
62
|
*/
|
|
47
63
|
static getCurrentRoute(): RouteInfo {
|
|
64
|
+
// 如果 WsxRouter 已经维护了当前路由信息,直接返回
|
|
65
|
+
if (RouterUtils._currentRoute) {
|
|
66
|
+
return RouterUtils._currentRoute;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 否则返回基础路由信息(向后兼容)
|
|
48
70
|
const url = new URL(window.location.href);
|
|
49
71
|
return {
|
|
50
72
|
path: url.pathname,
|
|
51
|
-
params: {},
|
|
73
|
+
params: {},
|
|
52
74
|
query: Object.fromEntries(url.searchParams.entries()),
|
|
53
75
|
hash: url.hash.slice(1), // 移除 # 号
|
|
54
76
|
};
|
|
@@ -241,6 +263,14 @@ export class RouterUtils {
|
|
|
241
263
|
|
|
242
264
|
/**
|
|
243
265
|
* 监听路由变化
|
|
266
|
+
*
|
|
267
|
+
* 只监听 route-changed 事件,该事件在 RouterUtils._setCurrentRoute() 之后触发。
|
|
268
|
+
* 不监听 popstate 事件,因为:
|
|
269
|
+
* 1. WsxRouter 已经监听 popstate 并会触发 route-changed
|
|
270
|
+
* 2. 同时监听两个事件会导致回调被调用两次,产生竞态条件
|
|
271
|
+
* 3. popstate 触发时 RouterUtils._currentRoute 可能还未更新
|
|
272
|
+
*
|
|
273
|
+
* @see RFC-0035: 路由导航竞态条件修复
|
|
244
274
|
*/
|
|
245
275
|
static onRouteChange(callback: (route: RouteInfo) => void): () => void {
|
|
246
276
|
const handler = () => {
|
|
@@ -248,12 +278,12 @@ export class RouterUtils {
|
|
|
248
278
|
callback(route);
|
|
249
279
|
};
|
|
250
280
|
|
|
251
|
-
|
|
281
|
+
// 只监听 route-changed 事件
|
|
282
|
+
// 该事件由 WsxRouter 在更新 RouterUtils._currentRoute 之后触发
|
|
252
283
|
document.addEventListener("route-changed", handler);
|
|
253
284
|
|
|
254
285
|
// 返回清理函数
|
|
255
286
|
return () => {
|
|
256
|
-
window.removeEventListener("popstate", handler);
|
|
257
287
|
document.removeEventListener("route-changed", handler);
|
|
258
288
|
};
|
|
259
289
|
}
|
package/src/WsxLink.css
CHANGED
package/src/WsxLink.wsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** @jsxImportSource @wsxjs/wsx-core */
|
|
2
|
-
import { WebComponent, autoRegister,
|
|
2
|
+
import { WebComponent, autoRegister, state } from "@wsxjs/wsx-core";
|
|
3
|
+
import { createLogger } from "@wsxjs/wsx-logger";
|
|
3
4
|
|
|
4
5
|
const logger = createLogger("WsxLink");
|
|
5
6
|
|
package/src/WsxRouter.css
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/* WSX Router 样式 */
|
|
2
2
|
:host {
|
|
3
|
-
display:
|
|
3
|
+
display: grid;
|
|
4
4
|
width: 100%;
|
|
5
|
-
height: 100%;
|
|
6
5
|
}
|
|
7
6
|
|
|
8
7
|
.router-outlet {
|
|
9
8
|
width: 100%;
|
|
10
|
-
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* 关键修复:使用 CSS Grid 让所有视图重叠在同一个 grid cell,防止页面高度闪烁 */
|
|
12
|
+
:host > wsx-view {
|
|
13
|
+
grid-area: 1 / 1;
|
|
11
14
|
}
|
package/src/WsxRouter.wsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** @jsxImportSource @wsxjs/wsx-core */
|
|
2
|
-
import { LightComponent, autoRegister
|
|
2
|
+
import { LightComponent, autoRegister } from "@wsxjs/wsx-core";
|
|
3
|
+
import { createLogger } from "@wsxjs/wsx-logger";
|
|
4
|
+
import { RouterUtils, type RouteInfo } from "./RouterUtils";
|
|
3
5
|
import styles from "./WsxRouter.css?inline";
|
|
4
6
|
|
|
5
7
|
const logger = createLogger("WsxRouter");
|
|
@@ -26,6 +28,14 @@ const logger = createLogger("WsxRouter");
|
|
|
26
28
|
export default class WsxRouter extends LightComponent {
|
|
27
29
|
private views: Map<string, HTMLElement> = new Map();
|
|
28
30
|
private currentView: HTMLElement | null = null;
|
|
31
|
+
private currentPath: string = "";
|
|
32
|
+
private isHandlingRoute: boolean = false; // 防止递归调用的标志
|
|
33
|
+
// RFC 0032: 路由匹配优化 - 缓存预编译的正则表达式和匹配结果
|
|
34
|
+
private compiledRoutes: Map<string, RegExp> = new Map();
|
|
35
|
+
private matchCache: Map<string, string> = new Map();
|
|
36
|
+
// RFC 0033: 现代路由特性
|
|
37
|
+
private enableViewTransitions: boolean = true;
|
|
38
|
+
private scrollRestoration: "auto" | "manual" = "manual";
|
|
29
39
|
|
|
30
40
|
constructor() {
|
|
31
41
|
super({
|
|
@@ -42,12 +52,30 @@ export default class WsxRouter extends LightComponent {
|
|
|
42
52
|
);
|
|
43
53
|
}
|
|
44
54
|
|
|
55
|
+
protected onRendered() {
|
|
56
|
+
// 在渲染完成后也收集视图和处理路由(作为备用)
|
|
57
|
+
// 注意:onRendered 可能不会被调用(如果 hasActualContent 为 true)
|
|
58
|
+
// 所以主要逻辑在 onConnected() 中
|
|
59
|
+
if (this.views.size === 0) {
|
|
60
|
+
this.collectViews();
|
|
61
|
+
logger.debug("WsxRouter collected views in onRendered:", this.views.size);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 如果视图已收集但还没有处理路由,现在处理
|
|
65
|
+
if (this.views.size > 0 && !this.currentView) {
|
|
66
|
+
requestAnimationFrame(() => {
|
|
67
|
+
this.handleRouteChange();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
45
72
|
protected onConnected() {
|
|
46
73
|
logger.debug("WsxRouter connected to DOM");
|
|
47
74
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
75
|
+
// RFC 0033: 设置浏览器滚动恢复模式
|
|
76
|
+
if ("scrollRestoration" in history) {
|
|
77
|
+
history.scrollRestoration = this.scrollRestoration;
|
|
78
|
+
}
|
|
51
79
|
|
|
52
80
|
// 监听原生 popstate 事件
|
|
53
81
|
window.addEventListener("popstate", this.handleRouteChange);
|
|
@@ -55,8 +83,23 @@ export default class WsxRouter extends LightComponent {
|
|
|
55
83
|
// 拦截所有链接点击,让 History API 接管
|
|
56
84
|
this.addEventListener("click", this.interceptLinks);
|
|
57
85
|
|
|
58
|
-
//
|
|
59
|
-
|
|
86
|
+
// 关键修复:在 onConnected() 中也收集视图和处理初始路由
|
|
87
|
+
// 因为 onRendered() 可能不会被调用(如果 hasActualContent 为 true)
|
|
88
|
+
// 这解决了页面刷新时路由无法恢复的问题
|
|
89
|
+
requestAnimationFrame(() => {
|
|
90
|
+
// 收集视图(如果还没有收集)
|
|
91
|
+
if (this.views.size === 0) {
|
|
92
|
+
this.collectViews();
|
|
93
|
+
logger.debug(`WsxRouter collected views in onConnected: ${this.views.size}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 处理初始路由(页面刷新时)
|
|
97
|
+
// 使用双重 requestAnimationFrame 确保所有子元素都已连接
|
|
98
|
+
requestAnimationFrame(() => {
|
|
99
|
+
logger.debug(`WsxRouter handling initial route: ${window.location.pathname}`);
|
|
100
|
+
this.handleRouteChange();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
60
103
|
}
|
|
61
104
|
|
|
62
105
|
protected onDisconnected() {
|
|
@@ -68,72 +111,292 @@ export default class WsxRouter extends LightComponent {
|
|
|
68
111
|
const views = Array.from(this.children).filter(
|
|
69
112
|
(el) => el.tagName.toLowerCase() === "wsx-view"
|
|
70
113
|
);
|
|
71
|
-
logger.debug(
|
|
114
|
+
logger.debug(`WsxRouter found ${views.length} views`);
|
|
115
|
+
|
|
116
|
+
// RFC 0032: 清除旧的缓存
|
|
117
|
+
this.views.clear();
|
|
118
|
+
this.compiledRoutes.clear();
|
|
119
|
+
this.matchCache.clear();
|
|
72
120
|
|
|
73
121
|
views.forEach((view) => {
|
|
74
122
|
const route = view.getAttribute("route") || "/";
|
|
75
123
|
this.views.set(route, view as HTMLElement);
|
|
76
|
-
|
|
124
|
+
logger.debug(`WsxRouter registered route: ${route}`);
|
|
125
|
+
|
|
126
|
+
// RFC 0032: 预编译参数路由的正则表达式
|
|
127
|
+
if (route.includes(":")) {
|
|
128
|
+
const pattern = route.replace(/:[^/]+/g, "([^/]+)");
|
|
129
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
130
|
+
this.compiledRoutes.set(route, regex);
|
|
131
|
+
logger.debug(`WsxRouter compiled regex for route: ${route} -> ${regex}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 初始状态:隐藏所有视图,等待路由匹配后再显示
|
|
135
|
+
// 这样可以确保路由遵循实际 URL,不会默认显示 home
|
|
77
136
|
(view as HTMLElement).style.display = "none";
|
|
78
|
-
logger.debug(`WsxRouter hiding view for route: ${route}`);
|
|
79
137
|
});
|
|
80
138
|
}
|
|
81
139
|
|
|
82
140
|
private handleRouteChange = () => {
|
|
83
|
-
|
|
84
|
-
|
|
141
|
+
// 防止递归调用
|
|
142
|
+
if (this.isHandlingRoute) {
|
|
143
|
+
logger.debug("Route change already in progress, skipping");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 如果视图为空,重新收集(可能子元素还未渲染)
|
|
148
|
+
if (this.views.size === 0) {
|
|
149
|
+
this.collectViews();
|
|
150
|
+
logger.debug(`WsxRouter re-collected views: ${this.views.size}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.isHandlingRoute = true;
|
|
85
154
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
155
|
+
try {
|
|
156
|
+
const nextPath = window.location.pathname;
|
|
157
|
+
logger.debug(`WsxRouter: Route changed to: ${nextPath}`);
|
|
158
|
+
|
|
159
|
+
// RFC 0033: 1. 触发 before-navigate 事件(可取消)
|
|
160
|
+
const beforeEvent = new CustomEvent("before-navigate", {
|
|
161
|
+
detail: {
|
|
162
|
+
to: nextPath,
|
|
163
|
+
from: this.currentPath,
|
|
164
|
+
},
|
|
165
|
+
bubbles: true,
|
|
166
|
+
cancelable: true,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!this.dispatchEvent(beforeEvent)) {
|
|
170
|
+
// 事件被取消,阻止导航
|
|
171
|
+
logger.debug(`Navigation to ${nextPath} was cancelled`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// RFC 0033: 2. 执行导航(可能带 View Transition)
|
|
176
|
+
this.performNavigation(nextPath);
|
|
177
|
+
} finally {
|
|
178
|
+
// 延迟重置标志,确保所有异步操作完成
|
|
179
|
+
requestAnimationFrame(() => {
|
|
180
|
+
this.isHandlingRoute = false;
|
|
181
|
+
});
|
|
90
182
|
}
|
|
183
|
+
};
|
|
91
184
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
185
|
+
/**
|
|
186
|
+
* RFC 0033: 执行导航(带 View Transitions 支持)
|
|
187
|
+
*/
|
|
188
|
+
private performNavigation(nextPath: string): void {
|
|
189
|
+
const performTransition = () => {
|
|
190
|
+
// 查找匹配的视图
|
|
191
|
+
const view = this.matchRoute(nextPath);
|
|
192
|
+
|
|
193
|
+
if (view) {
|
|
194
|
+
// 关键修复:先显示新视图,再隐藏其他所有视图
|
|
195
|
+
// 这样可以确保页面始终有内容,防止页脚跳动
|
|
196
|
+
// Step 1: 立即显示新视图
|
|
197
|
+
view.style.display = "block";
|
|
198
|
+
view.style.visibility = "visible";
|
|
199
|
+
const viewRoute = view.getAttribute("route") || "/";
|
|
200
|
+
logger.debug(`Showing view for route: ${viewRoute}`);
|
|
201
|
+
|
|
202
|
+
// Step 2: 隐藏所有其他视图(包括旧的 currentView)
|
|
203
|
+
for (const [_route, otherView] of this.views) {
|
|
204
|
+
if (otherView !== view) {
|
|
205
|
+
otherView.style.display = "none";
|
|
206
|
+
otherView.style.visibility = "";
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Step 3: 更新 currentView
|
|
211
|
+
this.currentView = view;
|
|
212
|
+
|
|
213
|
+
// 传递路由参数(延迟设置,避免触发 attributeChangedCallback 导致递归)
|
|
214
|
+
const params = this.extractParams(viewRoute, nextPath);
|
|
215
|
+
logger.debug(
|
|
216
|
+
`Extracted params for route ${viewRoute} and path ${nextPath}:`,
|
|
217
|
+
params
|
|
218
|
+
);
|
|
219
|
+
if (params) {
|
|
220
|
+
requestAnimationFrame(() => {
|
|
221
|
+
view.setAttribute("params", JSON.stringify(params));
|
|
222
|
+
logger.debug(`Set params attribute on view: ${JSON.stringify(params)}`);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// RFC-0035: 立即更新 RouterUtils,确保 getCurrentRoute() 返回最新信息
|
|
227
|
+
// 在显示视图之后、触发事件之前更新,确保时序正确
|
|
228
|
+
const href =
|
|
229
|
+
window.location.href ||
|
|
230
|
+
`${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
231
|
+
const url = new URL(href, window.location.origin || "http://localhost");
|
|
232
|
+
const routeInfo: RouteInfo = {
|
|
233
|
+
path: nextPath,
|
|
234
|
+
params: params || {},
|
|
235
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
236
|
+
hash: url.hash.slice(1),
|
|
237
|
+
};
|
|
238
|
+
RouterUtils._setCurrentRoute(routeInfo);
|
|
239
|
+
logger.debug(`Updated RouterUtils._currentRoute:`, routeInfo);
|
|
240
|
+
|
|
241
|
+
// RFC-0035: 触发 route-changed 事件,通知 RouterUtils.onRouteChange() 监听器
|
|
242
|
+
// 在 document 上触发,确保所有监听器都能收到
|
|
243
|
+
const routeChangedEvent = new CustomEvent("route-changed", {
|
|
244
|
+
detail: routeInfo,
|
|
245
|
+
bubbles: true,
|
|
246
|
+
});
|
|
247
|
+
document.dispatchEvent(routeChangedEvent);
|
|
248
|
+
} else {
|
|
249
|
+
// 如果没有找到视图,隐藏所有视图
|
|
250
|
+
for (const [_route, otherView] of this.views) {
|
|
251
|
+
otherView.style.display = "none";
|
|
252
|
+
}
|
|
253
|
+
this.currentView = null;
|
|
254
|
+
logger.warn(`No view found for path: ${nextPath}`);
|
|
255
|
+
|
|
256
|
+
// 更新 RouterUtils 中的全局路由信息(无参数)
|
|
257
|
+
const href =
|
|
258
|
+
window.location.href ||
|
|
259
|
+
`${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
260
|
+
const url = new URL(href, window.location.origin || "http://localhost");
|
|
261
|
+
const routeInfo: RouteInfo = {
|
|
262
|
+
path: nextPath,
|
|
263
|
+
params: {},
|
|
264
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
265
|
+
hash: url.hash.slice(1),
|
|
266
|
+
};
|
|
267
|
+
RouterUtils._setCurrentRoute(routeInfo);
|
|
268
|
+
|
|
269
|
+
// RFC-0035: 同样需要触发 route-changed 事件
|
|
270
|
+
const routeChangedEvent = new CustomEvent("route-changed", {
|
|
271
|
+
detail: routeInfo,
|
|
272
|
+
bubbles: true,
|
|
273
|
+
});
|
|
274
|
+
document.dispatchEvent(routeChangedEvent);
|
|
103
275
|
}
|
|
276
|
+
|
|
277
|
+
// RFC 0033: 滚动管理
|
|
278
|
+
this.handleScrollRestoration(nextPath);
|
|
279
|
+
|
|
280
|
+
// RFC 0033: 触发 after-navigate 事件
|
|
281
|
+
this.dispatchEvent(
|
|
282
|
+
new CustomEvent("after-navigate", {
|
|
283
|
+
detail: {
|
|
284
|
+
to: nextPath,
|
|
285
|
+
from: this.currentPath,
|
|
286
|
+
},
|
|
287
|
+
bubbles: true,
|
|
288
|
+
})
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// RFC-0035: 移除旧的 route-changed 事件触发
|
|
292
|
+
// 原因:已在 document 上触发 route-changed 事件(第244行)
|
|
293
|
+
// 如果在 WsxRouter 元素上再次触发,会因为 bubbles: true 导致
|
|
294
|
+
// document 上的监听器收到两次事件,产生竞态条件
|
|
295
|
+
|
|
296
|
+
// 更新当前路径
|
|
297
|
+
this.currentPath = nextPath;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// RFC 0033: View Transitions API(渐进增强)
|
|
301
|
+
if (this.enableViewTransitions && "startViewTransition" in document) {
|
|
302
|
+
(
|
|
303
|
+
document as Document & { startViewTransition: (callback: () => void) => void }
|
|
304
|
+
).startViewTransition(performTransition);
|
|
104
305
|
} else {
|
|
105
|
-
|
|
306
|
+
performTransition();
|
|
106
307
|
}
|
|
308
|
+
}
|
|
107
309
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
310
|
+
/**
|
|
311
|
+
* RFC 0033: 滚动恢复处理
|
|
312
|
+
*/
|
|
313
|
+
private handleScrollRestoration(_nextPath: string): void {
|
|
314
|
+
if (this.scrollRestoration === "manual") {
|
|
315
|
+
// 判断是否是后退导航
|
|
316
|
+
const isBackNavigation = this.isBackNavigation();
|
|
317
|
+
|
|
318
|
+
if (!isBackNavigation) {
|
|
319
|
+
// 前进导航:滚动到顶部
|
|
320
|
+
window.scrollTo(0, 0);
|
|
321
|
+
logger.debug("Scrolled to top for forward navigation");
|
|
322
|
+
}
|
|
323
|
+
// 后退导航:浏览器自动恢复滚动位置
|
|
324
|
+
}
|
|
325
|
+
// scrollRestoration === 'auto' 时,完全交给浏览器处理
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* RFC 0033: 检测是否是后退导航
|
|
330
|
+
*/
|
|
331
|
+
private isBackNavigation(): boolean {
|
|
332
|
+
// 通过 performance.navigation API 检测(已废弃但仍可用)
|
|
333
|
+
if (
|
|
334
|
+
"navigation" in performance &&
|
|
335
|
+
(performance as Performance & { navigation: { type: number } }).navigation
|
|
336
|
+
) {
|
|
337
|
+
const nav = (performance as Performance & { navigation: { type: number } }).navigation;
|
|
338
|
+
return nav.type === 2; // TYPE_BACK_FORWARD
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 降级方案:检查 history.state
|
|
342
|
+
if (window.history.state?.navigationType === "back") {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
117
348
|
|
|
118
349
|
private matchRoute(path: string): HTMLElement | null {
|
|
119
|
-
|
|
350
|
+
logger.warn(`matchRoute called for path: ${path}, views count: ${this.views.size}`);
|
|
351
|
+
|
|
352
|
+
// RFC 0032: 1. 检查匹配缓存
|
|
353
|
+
if (this.matchCache.has(path)) {
|
|
354
|
+
const route = this.matchCache.get(path)!;
|
|
355
|
+
logger.warn(`matchRoute: found in cache, route: ${route}`);
|
|
356
|
+
return this.views.get(route) || null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// RFC 0032: 2. 精确匹配
|
|
120
360
|
if (this.views.has(path)) {
|
|
361
|
+
this.matchCache.set(path, path);
|
|
362
|
+
logger.warn(`matchRoute: exact match found for ${path}`);
|
|
121
363
|
return this.views.get(path)!;
|
|
122
364
|
}
|
|
123
365
|
|
|
124
|
-
//
|
|
366
|
+
// RFC 0032: 3. 参数匹配(使用预编译的正则表达式)
|
|
367
|
+
logger.warn(
|
|
368
|
+
`matchRoute: checking parameter routes, compiledRoutes count: ${this.compiledRoutes.size}`
|
|
369
|
+
);
|
|
125
370
|
for (const [route, view] of this.views) {
|
|
126
371
|
if (route.includes(":")) {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
372
|
+
const regex = this.compiledRoutes.get(route);
|
|
373
|
+
if (regex) {
|
|
374
|
+
const matches = regex.test(path);
|
|
375
|
+
logger.warn(
|
|
376
|
+
`matchRoute: testing route ${route} against ${path}, matches: ${matches}`
|
|
377
|
+
);
|
|
378
|
+
if (matches) {
|
|
379
|
+
this.matchCache.set(path, route);
|
|
380
|
+
logger.warn(
|
|
381
|
+
`matchRoute: parameter match found, route: ${route}, path: ${path}`
|
|
382
|
+
);
|
|
383
|
+
return view;
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
logger.warn(`matchRoute: route ${route} has ':' but no compiled regex found`);
|
|
131
387
|
}
|
|
132
388
|
}
|
|
133
389
|
}
|
|
134
390
|
|
|
135
|
-
// 通配符匹配
|
|
136
|
-
|
|
391
|
+
// RFC 0032: 4. 通配符匹配
|
|
392
|
+
const wildcardView = this.views.get("*") || null;
|
|
393
|
+
if (wildcardView) {
|
|
394
|
+
this.matchCache.set(path, "*");
|
|
395
|
+
logger.warn(`matchRoute: wildcard match found for ${path}`);
|
|
396
|
+
} else {
|
|
397
|
+
logger.warn(`matchRoute: no match found for ${path}, no wildcard view available`);
|
|
398
|
+
}
|
|
399
|
+
return wildcardView;
|
|
137
400
|
}
|
|
138
401
|
|
|
139
402
|
private extractParams(route: string, path: string): Record<string, string> | null {
|
|
@@ -165,6 +428,34 @@ export default class WsxRouter extends LightComponent {
|
|
|
165
428
|
this.navigate(href);
|
|
166
429
|
};
|
|
167
430
|
|
|
431
|
+
/**
|
|
432
|
+
* RFC 0032: 清除匹配缓存
|
|
433
|
+
* 当路由配置变更时调用,确保使用最新的路由规则
|
|
434
|
+
*/
|
|
435
|
+
private clearMatchCache(): void {
|
|
436
|
+
this.matchCache.clear();
|
|
437
|
+
logger.debug("WsxRouter cleared match cache");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* RFC 0033: 配置 View Transitions
|
|
442
|
+
*/
|
|
443
|
+
public setViewTransitions(enabled: boolean): void {
|
|
444
|
+
this.enableViewTransitions = enabled;
|
|
445
|
+
logger.debug(`View Transitions ${enabled ? "enabled" : "disabled"}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* RFC 0033: 配置 Scroll Restoration
|
|
450
|
+
*/
|
|
451
|
+
public setScrollRestoration(mode: "auto" | "manual"): void {
|
|
452
|
+
this.scrollRestoration = mode;
|
|
453
|
+
if ("scrollRestoration" in history) {
|
|
454
|
+
history.scrollRestoration = mode;
|
|
455
|
+
}
|
|
456
|
+
logger.debug(`Scroll Restoration set to ${mode}`);
|
|
457
|
+
}
|
|
458
|
+
|
|
168
459
|
/**
|
|
169
460
|
* 编程式导航 - 使用原生 History API
|
|
170
461
|
*/
|