@viewfly/router 3.0.0-alpha.8 → 3.0.1
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 +12 -12
- package/dist/hooks/_api.d.ts +1 -0
- package/dist/hooks/use-params.d.ts +2 -0
- package/dist/index.esm.js +526 -105
- package/dist/index.js +525 -102
- package/dist/link.d.ts +1 -1
- package/dist/providers/_api.d.ts +1 -0
- package/dist/providers/navigator.d.ts +23 -9
- package/dist/providers/query-encoding.d.ts +3 -0
- package/dist/providers/router.d.ts +29 -13
- package/dist/providers/routes.d.ts +32 -0
- package/dist/providers/url-parser.d.ts +1 -1
- package/dist/router-module.d.ts +8 -2
- package/dist/router-outlet.d.ts +1 -2
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3,13 +3,38 @@ let _viewfly_core = require("@viewfly/core");
|
|
|
3
3
|
let _tanbo_stream = require("@tanbo/stream");
|
|
4
4
|
//#region src/providers/router.ts
|
|
5
5
|
var routerErrorFn$1 = (0, _viewfly_core.makeError)("Router");
|
|
6
|
-
|
|
6
|
+
/** 与 matchRoute 的 pathname 参数同粒度比较(去掉首尾 `/`) */
|
|
7
|
+
function normalizeRoutePathSegment(path) {
|
|
8
|
+
return path.replace(/^\/+|\/+$/g, "") || "";
|
|
9
|
+
}
|
|
10
|
+
var Router = class Router {
|
|
7
11
|
onRefresh;
|
|
12
|
+
lastResolvedParams = null;
|
|
13
|
+
/** 当前重定向链上已解析过的 path 段(规范化后);非重定向成功匹配后清空 */
|
|
14
|
+
redirectTrail = null;
|
|
15
|
+
static maxRedirectHops = 32;
|
|
16
|
+
/** 当前层最近一次匹配所消费的 URL 段数(默认 1,保持历史行为) */
|
|
17
|
+
consumedSegments = 1;
|
|
18
|
+
routeParams = {};
|
|
19
|
+
/**
|
|
20
|
+
* 本次 `resolve` 命中的 `route.path` 上的动态段(供子 Router、`canActivate`、`redirectTo`)。
|
|
21
|
+
* 与 `params` 按层隔离(对齐 Angular):有 `parent` 时 `params` 只表示本层注入域,不由子级匹配覆盖。
|
|
22
|
+
*/
|
|
23
|
+
lastResolvePathParams = {};
|
|
8
24
|
get deep() {
|
|
9
|
-
return this.parent ? this.parent.deep +
|
|
25
|
+
return this.parent ? this.parent.deep + this.parent.consumedSegments : 0;
|
|
26
|
+
}
|
|
27
|
+
get remainingPaths() {
|
|
28
|
+
return this.navigator.urlTree.paths.slice(this.deep);
|
|
10
29
|
}
|
|
11
30
|
get path() {
|
|
12
|
-
return this.
|
|
31
|
+
return this.remainingPaths[0] || "";
|
|
32
|
+
}
|
|
33
|
+
get params() {
|
|
34
|
+
return this.routeParams;
|
|
35
|
+
}
|
|
36
|
+
setParams(params) {
|
|
37
|
+
this.routeParams = { ...params };
|
|
13
38
|
}
|
|
14
39
|
refreshEvent = new _tanbo_stream.Subject();
|
|
15
40
|
constructor(navigator, parent) {
|
|
@@ -17,17 +42,17 @@ var Router = class {
|
|
|
17
42
|
this.parent = parent;
|
|
18
43
|
this.onRefresh = this.refreshEvent.asObservable();
|
|
19
44
|
}
|
|
20
|
-
navigateTo(path,
|
|
21
|
-
this.navigator.to(path, this,
|
|
45
|
+
navigateTo(path, queryParams, hash) {
|
|
46
|
+
this.navigator.to(path, this, queryParams, hash);
|
|
22
47
|
}
|
|
23
|
-
replaceTo(path,
|
|
24
|
-
this.navigator.replace(path, this,
|
|
48
|
+
replaceTo(path, queryParams, hash) {
|
|
49
|
+
this.navigator.replace(path, this, queryParams, hash);
|
|
25
50
|
}
|
|
26
51
|
refresh() {
|
|
27
52
|
this.refreshEvent.next();
|
|
28
53
|
}
|
|
29
|
-
|
|
30
|
-
return this.matchRoute(routes, this.
|
|
54
|
+
resolve(routes) {
|
|
55
|
+
return this.matchRoute(routes, this.remainingPaths);
|
|
31
56
|
}
|
|
32
57
|
back() {
|
|
33
58
|
this.navigator.back();
|
|
@@ -38,35 +63,217 @@ var Router = class {
|
|
|
38
63
|
go(offset) {
|
|
39
64
|
this.navigator.go(offset);
|
|
40
65
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
66
|
+
clearRedirectTrail() {
|
|
67
|
+
this.redirectTrail = null;
|
|
68
|
+
}
|
|
69
|
+
getNavigatorParams() {
|
|
70
|
+
const pathname = "/" + this.navigator.urlTree.paths.join("/");
|
|
71
|
+
return {
|
|
72
|
+
pathname: pathname === "/" ? pathname : pathname.replace(/\/+/g, "/"),
|
|
73
|
+
queryParams: this.navigator.urlTree.queryParams,
|
|
74
|
+
hash: this.navigator.urlTree.hash
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
cloneNavigatorParams(params) {
|
|
78
|
+
return {
|
|
79
|
+
pathname: params.pathname,
|
|
80
|
+
queryParams: { ...params.queryParams },
|
|
81
|
+
hash: params.hash
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
beginRedirectResolution(pathname) {
|
|
85
|
+
const key = normalizeRoutePathSegment(pathname);
|
|
86
|
+
if (this.redirectTrail === null) this.redirectTrail = /* @__PURE__ */ new Set();
|
|
87
|
+
if (this.redirectTrail.size >= Router.maxRedirectHops) {
|
|
88
|
+
const chain = [...this.redirectTrail].join(" -> ");
|
|
89
|
+
this.clearRedirectTrail();
|
|
90
|
+
throw routerErrorFn$1(`Redirect chain exceeded ${Router.maxRedirectHops} hops (last segment: '${pathname}', chain: ${chain})`);
|
|
91
|
+
}
|
|
92
|
+
this.redirectTrail.add(key);
|
|
93
|
+
}
|
|
94
|
+
assertRedirectTarget(pathname, target) {
|
|
95
|
+
const cur = normalizeRoutePathSegment(pathname);
|
|
96
|
+
const next = normalizeRoutePathSegment(target);
|
|
97
|
+
if (next === cur) {
|
|
98
|
+
this.clearRedirectTrail();
|
|
99
|
+
throw routerErrorFn$1(`Self-redirect at '${pathname}' (redirect target equals current path)`);
|
|
52
100
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
101
|
+
if (this.redirectTrail.has(next)) {
|
|
102
|
+
const chain = `${[...this.redirectTrail].join(" -> ")} -> ${next}`;
|
|
103
|
+
this.clearRedirectTrail();
|
|
104
|
+
throw routerErrorFn$1(`Redirect cycle detected (chain: ${chain})`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
splitRoutePath(path) {
|
|
108
|
+
const normalized = normalizeRoutePathSegment(path);
|
|
109
|
+
return normalized ? normalized.split("/") : [];
|
|
110
|
+
}
|
|
111
|
+
/** 单段路由:`user` | `:id` | `:id?`(`?` 仅表示可选;整条 path 里 `:…?` 只允许出现在最后一段,由 matchRoutePath 校验) */
|
|
112
|
+
parseRouteSegment(segment) {
|
|
113
|
+
if (!segment) return null;
|
|
114
|
+
if (segment.startsWith(":")) {
|
|
115
|
+
const optionalMatch = /^:([^:?]+)\?$/.exec(segment);
|
|
116
|
+
if (optionalMatch) {
|
|
117
|
+
const name = optionalMatch[1];
|
|
118
|
+
return name ? {
|
|
119
|
+
kind: "param",
|
|
120
|
+
name,
|
|
121
|
+
optional: true
|
|
122
|
+
} : null;
|
|
123
|
+
}
|
|
124
|
+
const name = segment.slice(1);
|
|
125
|
+
if (!name || name.includes("?")) return null;
|
|
126
|
+
return {
|
|
127
|
+
kind: "param",
|
|
128
|
+
name,
|
|
129
|
+
optional: false
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
kind: "static",
|
|
134
|
+
value: segment
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
matchRoutePath(routePath, remainingPaths) {
|
|
138
|
+
if (!routePath || routePath === "*") return null;
|
|
139
|
+
const rawSegments = this.splitRoutePath(routePath);
|
|
140
|
+
const segments = [];
|
|
141
|
+
for (const s of rawSegments) {
|
|
142
|
+
const parsed = this.parseRouteSegment(s);
|
|
143
|
+
if (!parsed) {
|
|
144
|
+
if (s.startsWith(":")) throw routerErrorFn$1(`Empty or invalid path parameter segment '${s}' in '${routePath}' (use ':name' with a non-empty name).`);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
segments.push(parsed);
|
|
148
|
+
}
|
|
149
|
+
for (let i = 0; i < segments.length; i++) {
|
|
150
|
+
const s = segments[i];
|
|
151
|
+
if (s.kind === "param" && s.optional && i !== segments.length - 1) throw routerErrorFn$1(`Optional path parameter ':${s.name}?' must be the last segment of '${routePath}' (optional params are only allowed at the end of the path).`);
|
|
152
|
+
}
|
|
153
|
+
const seenParamNames = /* @__PURE__ */ new Set();
|
|
154
|
+
for (const s of segments) if (s.kind === "param") {
|
|
155
|
+
if (seenParamNames.has(s.name)) throw routerErrorFn$1(`Duplicate path parameter ':${s.name}' in '${routePath}' (each name must appear at most once).`);
|
|
156
|
+
seenParamNames.add(s.name);
|
|
157
|
+
}
|
|
158
|
+
const urlLen = remainingPaths.length;
|
|
159
|
+
let ui = 0;
|
|
160
|
+
const pathParams = {};
|
|
161
|
+
for (let ri = 0; ri < segments.length; ri++) {
|
|
162
|
+
const seg = segments[ri];
|
|
163
|
+
if (seg.kind === "static") {
|
|
164
|
+
if (ui >= urlLen || remainingPaths[ui] !== seg.value) return null;
|
|
165
|
+
ui++;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!seg.optional) {
|
|
169
|
+
if (ui >= urlLen) return null;
|
|
170
|
+
pathParams[seg.name] = remainingPaths[ui];
|
|
171
|
+
ui++;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (ui >= urlLen) {
|
|
175
|
+
pathParams[seg.name] = "";
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
pathParams[seg.name] = remainingPaths[ui];
|
|
179
|
+
ui++;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
consumedSegments: ui,
|
|
183
|
+
params: pathParams
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
matchRoute(routes, remainingPaths) {
|
|
187
|
+
this.lastResolvePathParams = {};
|
|
188
|
+
const pathname = remainingPaths[0] || "";
|
|
189
|
+
this.beginRedirectResolution(pathname);
|
|
190
|
+
let matchedRoute = null;
|
|
191
|
+
let matchedRouteResult = null;
|
|
192
|
+
let defaultRoute = null;
|
|
193
|
+
let fallbackRoute = null;
|
|
194
|
+
for (const item of routes) {
|
|
195
|
+
const matchResult = this.matchRoutePath(item.path, remainingPaths);
|
|
196
|
+
if (matchResult) {
|
|
197
|
+
matchedRoute = item;
|
|
198
|
+
matchedRouteResult = matchResult;
|
|
199
|
+
break;
|
|
200
|
+
} else if (item.path === "*") {
|
|
201
|
+
if (!fallbackRoute) fallbackRoute = item;
|
|
202
|
+
} else if (item.path === "") {
|
|
203
|
+
if (!defaultRoute) defaultRoute = item;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const route = matchedRoute || defaultRoute || fallbackRoute;
|
|
207
|
+
if (!route) {
|
|
208
|
+
this.consumedSegments = 0;
|
|
209
|
+
this.routeParams = {};
|
|
210
|
+
this.lastResolvePathParams = {};
|
|
211
|
+
this.clearRedirectTrail();
|
|
212
|
+
this.lastResolvedParams = this.cloneNavigatorParams(this.getNavigatorParams());
|
|
213
|
+
return route;
|
|
214
|
+
}
|
|
215
|
+
if (route === defaultRoute) {
|
|
216
|
+
this.consumedSegments = 0;
|
|
217
|
+
this.routeParams = {};
|
|
218
|
+
this.lastResolvePathParams = {};
|
|
219
|
+
} else if (route === matchedRoute && matchedRouteResult) {
|
|
220
|
+
this.consumedSegments = matchedRouteResult.consumedSegments;
|
|
221
|
+
this.lastResolvePathParams = { ...matchedRouteResult.params };
|
|
222
|
+
if (this.parent === null) this.routeParams = { ...matchedRouteResult.params };
|
|
223
|
+
} else {
|
|
224
|
+
this.consumedSegments = remainingPaths.length > 0 ? 1 : 0;
|
|
225
|
+
this.routeParams = {};
|
|
226
|
+
this.lastResolvePathParams = {};
|
|
227
|
+
}
|
|
228
|
+
if (typeof route.redirectTo === "function") {
|
|
229
|
+
const to = this.cloneNavigatorParams(this.getNavigatorParams());
|
|
230
|
+
const from = this.lastResolvedParams ? this.cloneNavigatorParams(this.lastResolvedParams) : null;
|
|
231
|
+
const p = route.redirectTo({
|
|
232
|
+
to,
|
|
233
|
+
from,
|
|
234
|
+
router: this,
|
|
235
|
+
params: this.lastResolvePathParams
|
|
236
|
+
});
|
|
237
|
+
if (typeof p === "string") {
|
|
238
|
+
this.assertRedirectTarget(pathname, p);
|
|
239
|
+
this.navigateTo(p);
|
|
240
|
+
} else if (typeof p === "object") {
|
|
241
|
+
this.assertRedirectTarget(pathname, p.pathname);
|
|
242
|
+
this.navigateTo(p.pathname, p.queryParams, p.hash);
|
|
243
|
+
} else {
|
|
244
|
+
this.clearRedirectTrail();
|
|
245
|
+
throw routerErrorFn$1(`Router redirect to '${pathname}' not supported`);
|
|
246
|
+
}
|
|
60
247
|
return null;
|
|
61
248
|
}
|
|
62
|
-
if (typeof
|
|
63
|
-
this.
|
|
249
|
+
if (typeof route.redirectTo === "string") {
|
|
250
|
+
this.assertRedirectTarget(pathname, route.redirectTo);
|
|
251
|
+
this.navigateTo(route.redirectTo);
|
|
64
252
|
return null;
|
|
65
253
|
}
|
|
66
|
-
|
|
254
|
+
this.clearRedirectTrail();
|
|
255
|
+
this.lastResolvedParams = this.cloneNavigatorParams(this.getNavigatorParams());
|
|
256
|
+
return route;
|
|
67
257
|
}
|
|
68
258
|
};
|
|
69
259
|
//#endregion
|
|
260
|
+
//#region src/providers/query-encoding.ts
|
|
261
|
+
/** 查询串组件:写入 URL 时 encode,从 URL 解析出对象时 decode(与常见路由约定一致)。 */
|
|
262
|
+
function encodeQueryParamComponent(value) {
|
|
263
|
+
try {
|
|
264
|
+
return encodeURIComponent(value);
|
|
265
|
+
} catch {
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function decodeQueryParamComponent(value) {
|
|
270
|
+
try {
|
|
271
|
+
return decodeURIComponent(value.replace(/\+/g, " "));
|
|
272
|
+
} catch {
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
//#endregion
|
|
70
277
|
//#region src/providers/url-parser.ts
|
|
71
278
|
var UrlParser = class {
|
|
72
279
|
index = 0;
|
|
@@ -85,7 +292,7 @@ var UrlParser = class {
|
|
|
85
292
|
this.index++;
|
|
86
293
|
this.tokens.push({
|
|
87
294
|
type: "query",
|
|
88
|
-
|
|
295
|
+
queryParams: this.readQuery()
|
|
89
296
|
});
|
|
90
297
|
} else if (this.peek("#")) {
|
|
91
298
|
this.index++;
|
|
@@ -115,7 +322,7 @@ var UrlParser = class {
|
|
|
115
322
|
urlTree.paths.push(item.value);
|
|
116
323
|
break;
|
|
117
324
|
case "query":
|
|
118
|
-
urlTree.queryParams = item.
|
|
325
|
+
urlTree.queryParams = item.queryParams;
|
|
119
326
|
break;
|
|
120
327
|
case "hash": urlTree.hash = item.value;
|
|
121
328
|
}
|
|
@@ -129,11 +336,11 @@ var UrlParser = class {
|
|
|
129
336
|
readQuery() {
|
|
130
337
|
const query = {};
|
|
131
338
|
while (this.index < this.url.length) {
|
|
132
|
-
const key = this.readQueryKey();
|
|
339
|
+
const key = decodeQueryParamComponent(this.readQueryKey());
|
|
133
340
|
let value = "";
|
|
134
341
|
if (this.peek("=")) {
|
|
135
342
|
this.index++;
|
|
136
|
-
value = this.readQueryValue();
|
|
343
|
+
value = decodeQueryParamComponent(this.readQueryValue());
|
|
137
344
|
}
|
|
138
345
|
const oldValue = query[key];
|
|
139
346
|
if (oldValue) if (Array.isArray(oldValue)) oldValue.push(value);
|
|
@@ -216,33 +423,33 @@ var Navigator = class {
|
|
|
216
423
|
};
|
|
217
424
|
function formatUrl(pathname, urlFormatParams) {
|
|
218
425
|
pathname = pathname.replace(/\/+/g, "/");
|
|
219
|
-
const { queryParams,
|
|
220
|
-
return pathname + (queryParams ? "?" + formatQueryParams(queryParams) : "") + (
|
|
426
|
+
const { queryParams, hash } = urlFormatParams;
|
|
427
|
+
return pathname + (queryParams ? "?" + formatQueryParams(queryParams) : "") + (hash !== void 0 && hash !== null ? "#" + hash : "");
|
|
221
428
|
}
|
|
222
429
|
function formatQueryParams(queryParams) {
|
|
223
430
|
const params = [];
|
|
224
431
|
Object.keys(queryParams).forEach((key) => {
|
|
432
|
+
const encKey = encodeQueryParamComponent(key);
|
|
225
433
|
const values = queryParams[key];
|
|
226
434
|
if (Array.isArray(values)) values.forEach((i) => {
|
|
227
|
-
params.push(`${
|
|
435
|
+
params.push(`${encKey}=${encodeQueryParamComponent(i)}`);
|
|
228
436
|
});
|
|
229
|
-
else params.push(`${
|
|
437
|
+
else params.push(`${encKey}=${encodeQueryParamComponent(values)}`);
|
|
230
438
|
});
|
|
231
439
|
return params.join("&");
|
|
232
440
|
}
|
|
233
441
|
var BrowserNavigator = class BrowserNavigator extends Navigator {
|
|
234
442
|
onUrlChanged;
|
|
443
|
+
pendingNavigation = null;
|
|
235
444
|
/** 挂载在 location 上的路径前缀;'' 或 '/' 表示站点根,不做剥离 */
|
|
236
445
|
get basePathPrefix() {
|
|
237
446
|
return this.baseUrl === "/" || this.baseUrl === "" ? "" : this.baseUrl;
|
|
238
447
|
}
|
|
239
448
|
get pathname() {
|
|
240
|
-
|
|
241
|
-
if (!this.basePathPrefix) return pathname;
|
|
242
|
-
return pathname.startsWith(this.basePathPrefix) ? pathname.substring(this.basePathPrefix.length) : pathname;
|
|
449
|
+
return this.stripBaseFromLocationPathname(location.pathname);
|
|
243
450
|
}
|
|
244
451
|
urlParser = new UrlParser();
|
|
245
|
-
urlTree = this.
|
|
452
|
+
urlTree = this.readUrlTreeFromLocation();
|
|
246
453
|
urlChangeEvent = new _tanbo_stream.Subject();
|
|
247
454
|
subscription = new _tanbo_stream.Subscription();
|
|
248
455
|
constructor(baseUrl, hooks = {}) {
|
|
@@ -250,51 +457,62 @@ var BrowserNavigator = class BrowserNavigator extends Navigator {
|
|
|
250
457
|
this.hooks = hooks;
|
|
251
458
|
this.onUrlChanged = this.urlChangeEvent.asObservable();
|
|
252
459
|
this.subscription.add((0, _tanbo_stream.fromEvent)(window, "popstate").subscribe(() => {
|
|
253
|
-
this.urlTree = this.
|
|
460
|
+
this.urlTree = this.readUrlTreeFromLocation();
|
|
254
461
|
this.urlChangeEvent.next();
|
|
255
462
|
}));
|
|
256
|
-
if (this.basePathPrefix && !location.pathname.startsWith(this.basePathPrefix))
|
|
463
|
+
if (this.basePathPrefix && !location.pathname.startsWith(this.basePathPrefix)) {
|
|
464
|
+
history.replaceState(null, "", this.baseUrl);
|
|
465
|
+
this.urlTree = this.readUrlTreeFromHistoryHref(this.baseUrl);
|
|
466
|
+
}
|
|
257
467
|
}
|
|
258
|
-
to(pathName, relative, queryParams,
|
|
259
|
-
const url = this.join(pathName, relative, queryParams,
|
|
468
|
+
to(pathName, relative, queryParams, hash) {
|
|
469
|
+
const url = this.join(pathName, relative, queryParams, hash);
|
|
260
470
|
if (location.origin + url === location.href) return true;
|
|
261
471
|
this.runHooks({
|
|
262
472
|
pathname: this.pathname,
|
|
263
473
|
queryParams: this.urlTree.queryParams,
|
|
264
|
-
|
|
474
|
+
hash: this.urlTree.hash
|
|
265
475
|
}, {
|
|
266
476
|
pathname: pathName,
|
|
267
477
|
queryParams: queryParams || {},
|
|
268
|
-
|
|
478
|
+
hash: hash ?? null
|
|
269
479
|
}, () => {
|
|
480
|
+
this.pendingNavigation = {
|
|
481
|
+
type: "push",
|
|
482
|
+
from: this.getCurrentUrl()
|
|
483
|
+
};
|
|
270
484
|
history.pushState(null, "", url);
|
|
271
|
-
this.urlTree = this.
|
|
485
|
+
this.urlTree = this.readUrlTreeFromHistoryHref(url);
|
|
272
486
|
this.urlChangeEvent.next();
|
|
273
487
|
});
|
|
274
488
|
return true;
|
|
275
489
|
}
|
|
276
|
-
replace(pathName, relative, queryParams,
|
|
277
|
-
const url = this.join(pathName, relative, queryParams,
|
|
490
|
+
replace(pathName, relative, queryParams, hash) {
|
|
491
|
+
const url = this.join(pathName, relative, queryParams, hash);
|
|
278
492
|
if (location.origin + url === location.href) return true;
|
|
279
493
|
this.runHooks({
|
|
280
494
|
pathname: this.pathname,
|
|
281
495
|
queryParams: this.urlTree.queryParams,
|
|
282
|
-
|
|
496
|
+
hash: this.urlTree.hash
|
|
283
497
|
}, {
|
|
284
498
|
pathname: pathName,
|
|
285
499
|
queryParams: queryParams || {},
|
|
286
|
-
|
|
500
|
+
hash: hash ?? null
|
|
287
501
|
}, () => {
|
|
502
|
+
this.pendingNavigation = {
|
|
503
|
+
type: "replace",
|
|
504
|
+
from: this.getCurrentUrl()
|
|
505
|
+
};
|
|
288
506
|
history.replaceState(null, "", url);
|
|
289
|
-
this.urlTree = this.
|
|
507
|
+
this.urlTree = this.readUrlTreeFromHistoryHref(url);
|
|
290
508
|
this.urlChangeEvent.next();
|
|
291
509
|
});
|
|
292
510
|
return true;
|
|
293
511
|
}
|
|
294
|
-
join(pathname, relative, queryParams,
|
|
512
|
+
join(pathname, relative, queryParams, hash) {
|
|
295
513
|
if (pathname.startsWith("/")) return formatUrl(this.baseUrl + pathname, {
|
|
296
514
|
queryParams,
|
|
297
|
-
|
|
515
|
+
hash
|
|
298
516
|
});
|
|
299
517
|
const beforePath = this.urlTree.paths.slice(0, relative.deep);
|
|
300
518
|
while (true) {
|
|
@@ -309,9 +527,11 @@ var BrowserNavigator = class BrowserNavigator extends Navigator {
|
|
|
309
527
|
}
|
|
310
528
|
break;
|
|
311
529
|
}
|
|
312
|
-
|
|
530
|
+
const tail = [...beforePath, pathname].join("/");
|
|
531
|
+
const base = this.baseUrl.replace(/\/+$/, "");
|
|
532
|
+
return formatUrl((base ? `${base}/${tail}` : `/${tail}`).replace(/\/+/g, "/"), {
|
|
313
533
|
queryParams,
|
|
314
|
-
|
|
534
|
+
hash
|
|
315
535
|
});
|
|
316
536
|
}
|
|
317
537
|
back() {
|
|
@@ -323,22 +543,64 @@ var BrowserNavigator = class BrowserNavigator extends Navigator {
|
|
|
323
543
|
go(offset) {
|
|
324
544
|
history.go(offset);
|
|
325
545
|
}
|
|
546
|
+
confirmNavigation() {
|
|
547
|
+
this.pendingNavigation = null;
|
|
548
|
+
}
|
|
549
|
+
cancelNavigation() {
|
|
550
|
+
const pending = this.pendingNavigation;
|
|
551
|
+
this.pendingNavigation = null;
|
|
552
|
+
if (!pending) return;
|
|
553
|
+
if (pending.type === "push") {
|
|
554
|
+
history.back();
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
history.replaceState(null, "", pending.from);
|
|
558
|
+
this.urlTree = this.readUrlTreeFromHistoryHref(pending.from);
|
|
559
|
+
this.urlChangeEvent.next();
|
|
560
|
+
}
|
|
326
561
|
destroy() {
|
|
327
562
|
this.subscription.unsubscribe();
|
|
328
563
|
}
|
|
329
564
|
runHooks(beforeParams, currentParams, next) {
|
|
330
|
-
if (typeof this.hooks.beforeEach === "function")
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
565
|
+
if (typeof this.hooks.beforeEach === "function") {
|
|
566
|
+
let called = false;
|
|
567
|
+
let warningTimer = null;
|
|
568
|
+
const proceed = () => {
|
|
569
|
+
if (called) return;
|
|
570
|
+
called = true;
|
|
571
|
+
if (warningTimer) clearTimeout(warningTimer);
|
|
572
|
+
next();
|
|
573
|
+
this.hooks.afterEach?.(currentParams);
|
|
574
|
+
};
|
|
575
|
+
this.hooks.beforeEach?.(beforeParams, currentParams, proceed);
|
|
576
|
+
const env = globalThis?.process?.env;
|
|
577
|
+
if (!(env?.env === "production" || env?.NODE_ENV === "production") && !called) warningTimer = setTimeout(() => {
|
|
578
|
+
if (!called) console.warn("[Viewfly Router] NavigatorHooks.beforeEach did not call next(); navigation remains pending.");
|
|
579
|
+
}, 300);
|
|
580
|
+
} else {
|
|
335
581
|
next();
|
|
336
582
|
this.hooks.afterEach?.(currentParams);
|
|
337
583
|
}
|
|
338
584
|
}
|
|
339
|
-
|
|
585
|
+
stripBaseFromLocationPathname(fullPathname) {
|
|
586
|
+
if (!this.basePathPrefix) return fullPathname;
|
|
587
|
+
return fullPathname.startsWith(this.basePathPrefix) ? fullPathname.substring(this.basePathPrefix.length) : fullPathname;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* 按与 `history.pushState` / `replaceState` 写入会话的 URL 解析 urlTree(与传入 `join` 的相对路径一致)。
|
|
591
|
+
* History 更新后若仍仅从 `Location` 读路径,可能与实际会话 URL 不一致(如 Location 与 document 的同步时序、部分宿主环境)。
|
|
592
|
+
*/
|
|
593
|
+
readUrlTreeFromHistoryHref(hrefRelativeToOrigin) {
|
|
594
|
+
const abs = new URL(hrefRelativeToOrigin, location.origin);
|
|
595
|
+
const logicalPathname = this.stripBaseFromLocationPathname(abs.pathname);
|
|
596
|
+
return this.urlParser.parse(`${logicalPathname}${abs.search}${abs.hash}`);
|
|
597
|
+
}
|
|
598
|
+
readUrlTreeFromLocation() {
|
|
340
599
|
return this.urlParser.parse(this.pathname + location.search + location.hash);
|
|
341
600
|
}
|
|
601
|
+
getCurrentUrl() {
|
|
602
|
+
return location.pathname + location.search + location.hash;
|
|
603
|
+
}
|
|
342
604
|
};
|
|
343
605
|
BrowserNavigator = __decorate([(0, _viewfly_core.Injectable)(), __decorateMetadata("design:paramtypes", [String, Object])], BrowserNavigator);
|
|
344
606
|
//#endregion
|
|
@@ -369,12 +631,68 @@ function useQueryParams() {
|
|
|
369
631
|
return queryParams;
|
|
370
632
|
}
|
|
371
633
|
//#endregion
|
|
634
|
+
//#region src/hooks/use-params.ts
|
|
635
|
+
function useParams() {
|
|
636
|
+
const router = (0, _viewfly_core.inject)(Router);
|
|
637
|
+
const pathParams = { ...router.params };
|
|
638
|
+
const readonlyParams = new Proxy(pathParams, _viewfly_core.readonlyProxyHandler);
|
|
639
|
+
const subscription = router.onRefresh.subscribe(() => {
|
|
640
|
+
(0, _viewfly_core.comparePropsWithCallbacks)(pathParams, router.params, (key) => {
|
|
641
|
+
(0, _viewfly_core.internalWrite)(() => {
|
|
642
|
+
Reflect.deleteProperty(pathParams, key);
|
|
643
|
+
});
|
|
644
|
+
}, (key, value) => {
|
|
645
|
+
(0, _viewfly_core.internalWrite)(() => {
|
|
646
|
+
pathParams[key] = value;
|
|
647
|
+
});
|
|
648
|
+
}, (key, value) => {
|
|
649
|
+
(0, _viewfly_core.internalWrite)(() => {
|
|
650
|
+
pathParams[key] = value;
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
(0, _viewfly_core.onUnmounted)(() => {
|
|
655
|
+
subscription.unsubscribe();
|
|
656
|
+
});
|
|
657
|
+
return readonlyParams;
|
|
658
|
+
}
|
|
659
|
+
//#endregion
|
|
660
|
+
//#region src/providers/routes.ts
|
|
661
|
+
var Routes = new _viewfly_core.InjectionToken("Routes");
|
|
662
|
+
//#endregion
|
|
372
663
|
//#region src/link.tsx
|
|
373
664
|
function Link(props) {
|
|
374
665
|
const navigator = (0, _viewfly_core.inject)(Navigator);
|
|
375
666
|
const router = (0, _viewfly_core.inject)(Router);
|
|
667
|
+
function buildDomAttrs() {
|
|
668
|
+
const attrs = {};
|
|
669
|
+
Object.keys(props).forEach((key) => {
|
|
670
|
+
switch (key) {
|
|
671
|
+
case "to":
|
|
672
|
+
case "active":
|
|
673
|
+
case "exact":
|
|
674
|
+
case "queryParams":
|
|
675
|
+
case "hash":
|
|
676
|
+
case "tag":
|
|
677
|
+
case "children": return;
|
|
678
|
+
default: attrs[key] = props[key];
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
return attrs;
|
|
682
|
+
}
|
|
683
|
+
function normalizePathname(path) {
|
|
684
|
+
const pathname = (path.split("#")[0].split("?")[0] || "/").replace(/\/+$/, "") || "/";
|
|
685
|
+
const baseUrl = navigator.baseUrl === "/" || navigator.baseUrl === "" ? "" : navigator.baseUrl;
|
|
686
|
+
if (!baseUrl) return pathname;
|
|
687
|
+
const base = baseUrl.replace(/\/+$/, "") || "/";
|
|
688
|
+
if (pathname === base) return "/";
|
|
689
|
+
if (pathname.startsWith(base + "/")) return pathname.substring(base.length);
|
|
690
|
+
return pathname;
|
|
691
|
+
}
|
|
376
692
|
function getActive() {
|
|
377
|
-
|
|
693
|
+
const currentPathname = normalizePathname(navigator.pathname);
|
|
694
|
+
const targetPathname = normalizePathname(navigator.join(props.to, router));
|
|
695
|
+
return props.exact ? currentPathname === targetPathname : currentPathname === targetPathname || currentPathname.startsWith(targetPathname + "/");
|
|
378
696
|
}
|
|
379
697
|
const isActive = (0, _viewfly_core.reactive)({ value: getActive() });
|
|
380
698
|
const subscription = navigator.onUrlChanged.subscribe(() => {
|
|
@@ -383,21 +701,27 @@ function Link(props) {
|
|
|
383
701
|
(0, _viewfly_core.onUnmounted)(() => {
|
|
384
702
|
subscription.unsubscribe();
|
|
385
703
|
});
|
|
704
|
+
/** 仅左键且无修饰键时由 SPA 接管,其余交给浏览器(新标签、下载默认等) */
|
|
705
|
+
function shouldHandleSpaClick(ev) {
|
|
706
|
+
if (ev.button !== 0) return false;
|
|
707
|
+
if (ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return false;
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
386
710
|
function navigate(ev) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
711
|
+
const isAnchorTag = !props.tag || props.tag === "a";
|
|
712
|
+
if (isAnchorTag && props.target === "_blank") return;
|
|
713
|
+
if (isAnchorTag && ev.cancelable) ev.preventDefault();
|
|
714
|
+
router.navigateTo(props.to, props.queryParams, props.hash);
|
|
390
715
|
}
|
|
391
716
|
return () => {
|
|
392
717
|
const Tag = props.tag || "a";
|
|
393
|
-
const attrs = Object.assign(
|
|
394
|
-
onClick(ev)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (Tag === "a") attrs.href = navigator.join(props.to, router, props.queryParams, props.fragment);
|
|
718
|
+
const attrs = Object.assign(buildDomAttrs(), { onClick(ev) {
|
|
719
|
+
props.onClick?.(ev);
|
|
720
|
+
if (ev.defaultPrevented) return;
|
|
721
|
+
if (!shouldHandleSpaClick(ev)) return;
|
|
722
|
+
navigate(ev);
|
|
723
|
+
} });
|
|
724
|
+
if (Tag === "a") attrs.href = navigator.join(props.to, router, props.queryParams, props.hash);
|
|
401
725
|
if (isActive.value && props.active) attrs.class = [attrs.class, props.active];
|
|
402
726
|
return /* @__PURE__ */ (0, _viewfly_core.jsx)(Tag, {
|
|
403
727
|
...attrs,
|
|
@@ -410,9 +734,9 @@ function Link(props) {
|
|
|
410
734
|
var RouterModule = class {
|
|
411
735
|
subscription = new _tanbo_stream.Subscription();
|
|
412
736
|
navigator;
|
|
413
|
-
constructor(
|
|
414
|
-
this.
|
|
415
|
-
this.navigator = new BrowserNavigator(
|
|
737
|
+
constructor(config) {
|
|
738
|
+
this.config = config;
|
|
739
|
+
this.navigator = new BrowserNavigator(config.baseUrl || "", config.hooks);
|
|
416
740
|
}
|
|
417
741
|
setup(app) {
|
|
418
742
|
const navigator = this.navigator;
|
|
@@ -420,13 +744,20 @@ var RouterModule = class {
|
|
|
420
744
|
this.subscription.add(navigator.onUrlChanged.subscribe(() => {
|
|
421
745
|
router.refresh();
|
|
422
746
|
}));
|
|
423
|
-
app.provide([
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
747
|
+
app.provide([
|
|
748
|
+
{
|
|
749
|
+
provide: Navigator,
|
|
750
|
+
useValue: navigator
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
provide: Router,
|
|
754
|
+
useValue: router
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
provide: Routes,
|
|
758
|
+
useValue: this.config.routes || []
|
|
759
|
+
}
|
|
760
|
+
]);
|
|
430
761
|
}
|
|
431
762
|
onDestroy() {
|
|
432
763
|
this.subscription.unsubscribe();
|
|
@@ -439,37 +770,127 @@ var routerErrorFn = (0, _viewfly_core.makeError)("RouterOutlet");
|
|
|
439
770
|
function RouterOutlet(props) {
|
|
440
771
|
const router = (0, _viewfly_core.inject)(Router, null);
|
|
441
772
|
if (router === null) throw routerErrorFn("cannot found parent Router.");
|
|
442
|
-
const
|
|
773
|
+
const currentRouter = router;
|
|
774
|
+
const routes = (0, _viewfly_core.inject)(Routes, []);
|
|
775
|
+
const navigator = (0, _viewfly_core.inject)(Navigator);
|
|
776
|
+
const childRouter = new Router(navigator, currentRouter);
|
|
443
777
|
const Context = (0, _viewfly_core.createContext)([{
|
|
444
778
|
provide: Router,
|
|
445
779
|
useValue: childRouter
|
|
446
780
|
}]);
|
|
447
781
|
const children = (0, _viewfly_core.shallowReactive)({ value: null });
|
|
448
|
-
|
|
782
|
+
let confirmedParams = null;
|
|
783
|
+
const subscription = currentRouter.onRefresh.pipe((0, _tanbo_stream.microTask)()).subscribe(() => {
|
|
449
784
|
updateChildren();
|
|
450
785
|
});
|
|
786
|
+
/** 每次 `updateChildren` 调用递增;并发/卸载时旧的一次性任务应放弃写 UI */
|
|
787
|
+
let navigationGeneration = 0;
|
|
451
788
|
(0, _viewfly_core.onUnmounted)(() => {
|
|
789
|
+
navigationGeneration += 1;
|
|
452
790
|
subscription.unsubscribe();
|
|
453
791
|
});
|
|
454
|
-
let
|
|
792
|
+
let activateRoute = null;
|
|
793
|
+
let activateRouteParamsKey = "";
|
|
794
|
+
(0, _viewfly_core.watch)(() => props.name, () => {
|
|
795
|
+
activateRoute = null;
|
|
796
|
+
updateChildren();
|
|
797
|
+
});
|
|
798
|
+
(0, _viewfly_core.watch)(() => props.children, () => {
|
|
799
|
+
if (activateRoute) return;
|
|
800
|
+
updateChildren();
|
|
801
|
+
});
|
|
802
|
+
function isStaleNavigation(token) {
|
|
803
|
+
return token !== navigationGeneration;
|
|
804
|
+
}
|
|
805
|
+
function getParamsKey(params) {
|
|
806
|
+
return Object.keys(params).sort().map((key) => `${key}=${params[key]}`).join("&");
|
|
807
|
+
}
|
|
808
|
+
function getNavigatorParams() {
|
|
809
|
+
const pathname = "/" + navigator.urlTree.paths.join("/");
|
|
810
|
+
return {
|
|
811
|
+
pathname: pathname === "/" ? pathname : pathname.replace(/\/+/g, "/"),
|
|
812
|
+
queryParams: navigator.urlTree.queryParams,
|
|
813
|
+
hash: navigator.urlTree.hash
|
|
814
|
+
};
|
|
815
|
+
}
|
|
455
816
|
async function updateChildren() {
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
817
|
+
const token = navigationGeneration += 1;
|
|
818
|
+
const to = getNavigatorParams();
|
|
819
|
+
const route = currentRouter.resolve(routes);
|
|
820
|
+
if (!route) {
|
|
821
|
+
navigator.confirmNavigation();
|
|
822
|
+
childRouter.setParams({});
|
|
823
|
+
confirmedParams = to;
|
|
824
|
+
activateRoute = null;
|
|
825
|
+
activateRouteParamsKey = "";
|
|
459
826
|
children.value = props.children || null;
|
|
460
827
|
return;
|
|
461
828
|
}
|
|
462
|
-
|
|
463
|
-
|
|
829
|
+
childRouter.setParams({ ...currentRouter.lastResolvePathParams });
|
|
830
|
+
if (route === activateRoute && activateRouteParamsKey === getParamsKey(currentRouter.lastResolvePathParams)) {
|
|
831
|
+
navigator.confirmNavigation();
|
|
832
|
+
confirmedParams = to;
|
|
833
|
+
childRouter.refresh();
|
|
834
|
+
return;
|
|
464
835
|
}
|
|
465
|
-
if (
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
836
|
+
if (typeof route.canActivate === "function") {
|
|
837
|
+
const ok = await route.canActivate({
|
|
838
|
+
to,
|
|
839
|
+
from: confirmedParams,
|
|
840
|
+
router: childRouter,
|
|
841
|
+
params: currentRouter.lastResolvePathParams
|
|
842
|
+
});
|
|
843
|
+
if (isStaleNavigation(token)) return;
|
|
844
|
+
if (!ok) {
|
|
845
|
+
navigator.cancelNavigation();
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
await applyRoute(route, token);
|
|
850
|
+
if (isStaleNavigation(token)) return;
|
|
851
|
+
navigator.confirmNavigation();
|
|
852
|
+
confirmedParams = to;
|
|
853
|
+
}
|
|
854
|
+
async function applyRoute(route, token) {
|
|
855
|
+
let Component = null;
|
|
856
|
+
if (props.name) {
|
|
857
|
+
const namedComponents = route.namedComponents || [];
|
|
858
|
+
for (const named of namedComponents) if (named.name === props.name) {
|
|
859
|
+
if (named.component) Component = named.component;
|
|
860
|
+
else if (typeof named.asyncComponent === "function") {
|
|
861
|
+
Component = await named.asyncComponent();
|
|
862
|
+
if (isStaleNavigation(token)) return;
|
|
863
|
+
}
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
} else if (route.component) Component = route.component;
|
|
867
|
+
else if (typeof route.asyncComponent === "function") {
|
|
868
|
+
Component = await route.asyncComponent();
|
|
869
|
+
if (isStaleNavigation(token)) return;
|
|
870
|
+
if (!Component) {
|
|
871
|
+
activateRoute = null;
|
|
872
|
+
children.value = props.children || null;
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (!Component) Component = RouterOutlet;
|
|
877
|
+
let subRoutes = [];
|
|
878
|
+
if (route.path !== "*") {
|
|
879
|
+
if (Array.isArray(route.children)) subRoutes = route.children;
|
|
880
|
+
else if (typeof route.children === "function") {
|
|
881
|
+
subRoutes = await route.children();
|
|
882
|
+
if (isStaleNavigation(token)) return;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (!Array.isArray(subRoutes)) subRoutes = [];
|
|
886
|
+
const Context = (0, _viewfly_core.createContext)([{
|
|
887
|
+
provide: Routes,
|
|
888
|
+
useValue: subRoutes
|
|
889
|
+
}]);
|
|
890
|
+
if (isStaleNavigation(token)) return;
|
|
891
|
+
activateRoute = route;
|
|
892
|
+
activateRouteParamsKey = getParamsKey(currentRouter.lastResolvePathParams);
|
|
893
|
+
children.value = /* @__PURE__ */ (0, _viewfly_core.jsx)(Context, { children: /* @__PURE__ */ (0, _viewfly_core.jsx)(Component, {}) });
|
|
473
894
|
}
|
|
474
895
|
updateChildren();
|
|
475
896
|
return () => {
|
|
@@ -488,7 +909,9 @@ exports.Navigator = Navigator;
|
|
|
488
909
|
exports.Router = Router;
|
|
489
910
|
exports.RouterModule = RouterModule;
|
|
490
911
|
exports.RouterOutlet = RouterOutlet;
|
|
912
|
+
exports.Routes = Routes;
|
|
491
913
|
exports.UrlParser = UrlParser;
|
|
492
914
|
exports.formatQueryParams = formatQueryParams;
|
|
493
915
|
exports.formatUrl = formatUrl;
|
|
916
|
+
exports.useParams = useParams;
|
|
494
917
|
exports.useQueryParams = useQueryParams;
|