@viewfly/router 2.1.0 → 3.0.0-alpha.0

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 CHANGED
@@ -1,132 +1,78 @@
1
- Viewfly
2
- ================================
1
+ # @viewfly/router
3
2
 
4
- Viewfly 是一个简单、数据驱动的前端框架。此项目为 Viewfly 的路由库,可让 Viewfly 支持浏览器路由。
3
+ 基于 **Viewfly** 的浏览器端路由:声明式链接、嵌套路由出口、编程式导航等。需配合 **`@viewfly/core`** **`@viewfly/platform-browser`** 使用。
4
+
5
+ ---
5
6
 
6
7
  ## 安装
7
8
 
9
+ ```bash
10
+ pnpm add @viewfly/router @viewfly/platform-browser @viewfly/core
8
11
  ```
9
- npm install @viewfly/router
10
- ```
11
-
12
- ## 使用示例
13
12
 
14
- ```jsx
15
- import { createApp } from '@viewfly/platform-browser'
16
- import { RouterModule } from '@viewfly/router'
17
-
18
- function ListTab1() {
19
- return () => {
20
- return (
21
- <div>listTab1</div>
22
- )
23
- }
24
- }
13
+ ---
25
14
 
26
- function ListTab2() {
27
- return () => {
28
- return (
29
- <div>listTab2</div>
30
- )
31
- }
32
- }
15
+ ## 接入应用
33
16
 
34
- function ListTab3() {
35
- return () => {
36
- return (
37
- <div>listTab3</div>
38
- )
39
- }
40
- }
17
+ 1. 使用 **`RouterModule`** 作为应用级扩展(通过 `createApp(...).use(...)` 注册)。
18
+ 2. 在布局中用 **`Link`** 生成导航,用 **`RouterOutlet`** 根据配置渲染匹配到的组件。
19
+ 3. 在组件内通过 **`inject(Router)`** 拿到路由实例,调用 **`navigateTo`** 等方法做跳转。
41
20
 
42
- function List() {
43
- return () => {
44
- return (
45
- <div>
46
- <h3>list</h3>
47
- <div>
48
- <Link active="active" to='./tab1'>tab1</Link>
49
- <Link active="active" to='./tab2'>tab2</Link>
50
- <Link active="active" to='./tab3'>tab3</Link>
51
- </div>
52
- <div>
53
- <RouterOutlet config={[
54
- {
55
- name: 'tab1',
56
- component: ListTab1
57
- },
58
- {
59
- name: 'tab2',
60
- component: ListTab2
61
- },
62
- {
63
- name: 'tab3',
64
- component: ListTab3
65
- }
66
- ]}>没找到 Tab</RouterOutlet>
67
- </div>
68
- </div>
69
- )
70
- }
71
- }
21
+ 最小串联示例(节选,完整路由表与懒加载等见官网):
72
22
 
73
- function Detail() {
74
- return () => {
75
- return (
76
- <div>detail</div>
77
- )
78
- }
79
- }
23
+ ```tsx
24
+ import { inject } from '@viewfly/core'
25
+ import { createApp } from '@viewfly/platform-browser'
26
+ import { Link, Router, RouterModule, RouterOutlet } from '@viewfly/router'
80
27
 
81
28
  function Home() {
82
29
  const router = inject(Router)
83
- return () => {
84
- return (
85
- <div>
86
- <div>home</div>
87
- <button type="button" onClick={() => {
88
- router.navigateTo('../list')
89
- }
90
- }>跳转到列表
91
- </button>
92
- </div>
93
- )
94
- }
30
+ return () => (
31
+ <div>
32
+ <p>Home</p>
33
+ <button type="button" onClick={() => router.navigateTo('/list')}>去列表</button>
34
+ </div>
35
+ )
36
+ }
37
+
38
+ function List() {
39
+ return () => <div>List</div>
95
40
  }
96
41
 
97
42
  function App() {
98
- return () => {
99
- return (
100
- <div>
101
- <div>
102
- <Link active="active" exact to="/">Home</Link>
103
- <Link active="active" to="/list" queryParams={{ a: 'xx' }}>List</Link>
104
- <Link active="active" to="/detail">Detail</Link>
105
- </div>
106
- <div>
107
- <RouterOutlet config={[
108
- {
109
- name: 'home',
110
- component: Home
111
- },
112
- {
113
- name: 'list',
114
- asyncComponent: () => Promise.resolve().then(() => List)
115
- },
116
- {
117
- name: 'detail',
118
- component: Detail
119
- }
120
- ]}>
121
- 未匹配到任何路由
122
- </RouterOutlet>
123
- </div>
124
- </div>
125
- )
126
- }
43
+ return () => (
44
+ <div>
45
+ <nav>
46
+ <Link active="active" exact to="/">Home</Link>
47
+ <Link active="active" to="/list">List</Link>
48
+ </nav>
49
+ <RouterOutlet
50
+ config={[
51
+ { name: 'home', component: Home },
52
+ { name: 'list', component: List }
53
+ ]}
54
+ >
55
+ 未匹配到路由
56
+ </RouterOutlet>
57
+ </div>
58
+ )
127
59
  }
128
60
 
129
- createApp(<App/>)use(new RouterModule()).mount(document.getElementById('app')!)
61
+ createApp(<App />)
62
+ .use(new RouterModule())
63
+ .mount(document.getElementById('app')!)
130
64
  ```
131
65
 
132
- 完整文档请参考官方网站:[viewfly.org](https://viewfly.org)
66
+ **嵌套路由**:在子页面组件内再次放置 `RouterOutlet`,并为其传入子级 `config`(与官网「路由」章节一致)。
67
+
68
+ ---
69
+
70
+ ## 文档
71
+
72
+ - **官方文档**:[viewfly.org](https://viewfly.org)
73
+
74
+ ---
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1 @@
1
+ export * from './use-query-params';
@@ -0,0 +1,2 @@
1
+ import { UrlQueryParams } from '../providers/url-parser';
2
+ export declare function useQueryParams<T extends UrlQueryParams>(): T;
@@ -0,0 +1,5 @@
1
+ export * from './hooks/_api';
2
+ export * from './providers/_api';
3
+ export * from './link';
4
+ export * from './router-module';
5
+ export * from './router-outlet';
@@ -0,0 +1,479 @@
1
+ import { Injectable, comparePropsWithCallbacks, createContext, inject, internalWrite, jsx, makeError, onUnmounted, reactive, readonlyProxyHandler, shallowReactive } from "@viewfly/core";
2
+ import { Subject, Subscription, fromEvent } from "@tanbo/stream";
3
+ //#region src/providers/router.ts
4
+ var routerErrorFn$1 = makeError("Router");
5
+ var Router = class {
6
+ onRefresh;
7
+ get deep() {
8
+ return this.parent ? this.parent.deep + 1 : 0;
9
+ }
10
+ get path() {
11
+ return this.navigator.urlTree.paths.at(this.deep) || "";
12
+ }
13
+ refreshEvent = new Subject();
14
+ constructor(navigator, parent) {
15
+ this.navigator = navigator;
16
+ this.parent = parent;
17
+ this.onRefresh = this.refreshEvent.asObservable();
18
+ }
19
+ navigateTo(path, params, fragment) {
20
+ this.navigator.to(path, this, params, fragment || void 0);
21
+ }
22
+ replaceTo(path, params) {
23
+ this.navigator.replace(path, this, params);
24
+ }
25
+ refresh() {
26
+ this.refreshEvent.next();
27
+ }
28
+ consumeConfig(routes) {
29
+ return this.matchRoute(routes, this.path);
30
+ }
31
+ back() {
32
+ this.navigator.back();
33
+ }
34
+ forward() {
35
+ this.navigator.forward();
36
+ }
37
+ go(offset) {
38
+ this.navigator.go(offset);
39
+ }
40
+ matchRoute(configs, pathname) {
41
+ let matchedConfig = null;
42
+ let defaultConfig = null;
43
+ let fallbackConfig = null;
44
+ for (const item of configs) if (item.path === pathname) {
45
+ matchedConfig = item;
46
+ break;
47
+ } else if (item.path === "*") {
48
+ if (!fallbackConfig) fallbackConfig = item;
49
+ } else if (item.path === "") {
50
+ if (!defaultConfig) defaultConfig = item;
51
+ }
52
+ const config = matchedConfig || defaultConfig || fallbackConfig;
53
+ if (!config) return config;
54
+ if (typeof config.redirectTo === "function") {
55
+ const p = config.redirectTo(pathname);
56
+ if (typeof p === "string") this.navigateTo(p);
57
+ else if (typeof p === "object") this.navigateTo(p.pathname, p.queryParams, p.fragment);
58
+ else throw routerErrorFn$1(`Router redirect to '${pathname}' not supported`);
59
+ return null;
60
+ }
61
+ if (typeof config.redirectTo === "string") {
62
+ this.navigateTo(config.redirectTo);
63
+ return null;
64
+ }
65
+ return config;
66
+ }
67
+ };
68
+ //#endregion
69
+ //#region src/providers/url-parser.ts
70
+ var UrlParser = class {
71
+ index = 0;
72
+ url = "";
73
+ tokens = [];
74
+ parse(url) {
75
+ this.index = 0;
76
+ this.url = url;
77
+ this.tokens = [];
78
+ while (this.index < this.url.length) {
79
+ this.ignore("/");
80
+ if (this.peek("../")) {
81
+ this.tokens.push({ type: "toParent" });
82
+ this.index += 3;
83
+ } else if (this.peek("?")) {
84
+ this.index++;
85
+ this.tokens.push({
86
+ type: "query",
87
+ params: this.readQuery()
88
+ });
89
+ } else if (this.peek("#")) {
90
+ this.index++;
91
+ this.tokens.push({
92
+ type: "hash",
93
+ value: this.readHash()
94
+ });
95
+ } else {
96
+ if (this.peek("./")) this.index += 2;
97
+ const path = this.readPath();
98
+ if (path) this.tokens.push({
99
+ type: "toChild",
100
+ value: path
101
+ });
102
+ }
103
+ }
104
+ const urlTree = {
105
+ paths: [],
106
+ queryParams: {},
107
+ hash: null
108
+ };
109
+ for (const item of this.tokens) switch (item.type) {
110
+ case "toParent":
111
+ urlTree.paths.pop();
112
+ break;
113
+ case "toChild":
114
+ urlTree.paths.push(item.value);
115
+ break;
116
+ case "query":
117
+ urlTree.queryParams = item.params;
118
+ break;
119
+ case "hash": urlTree.hash = item.value;
120
+ }
121
+ return urlTree;
122
+ }
123
+ readHash() {
124
+ const hash = this.url.substring(this.index);
125
+ this.index = this.url.length;
126
+ return hash;
127
+ }
128
+ readQuery() {
129
+ const query = {};
130
+ while (this.index < this.url.length) {
131
+ const key = this.readQueryKey();
132
+ let value = "";
133
+ if (this.peek("=")) {
134
+ this.index++;
135
+ value = this.readQueryValue();
136
+ }
137
+ const oldValue = query[key];
138
+ if (oldValue) if (Array.isArray(oldValue)) oldValue.push(value);
139
+ else query[key] = [oldValue, value];
140
+ else query[key] = value;
141
+ if (this.peek("&")) {
142
+ this.index++;
143
+ continue;
144
+ }
145
+ break;
146
+ }
147
+ return query;
148
+ }
149
+ readQueryValue() {
150
+ const chars = [];
151
+ while (this.index < this.url.length) {
152
+ if (this.not("&#")) {
153
+ chars.push(this.url.at(this.index));
154
+ this.index++;
155
+ continue;
156
+ }
157
+ break;
158
+ }
159
+ return chars.join("");
160
+ }
161
+ readQueryKey() {
162
+ const chars = [];
163
+ while (this.index < this.url.length) {
164
+ if (this.not("=&#")) {
165
+ chars.push(this.url.at(this.index));
166
+ this.index++;
167
+ continue;
168
+ }
169
+ break;
170
+ }
171
+ return chars.join("");
172
+ }
173
+ readPath() {
174
+ const chars = [];
175
+ while (this.index < this.url.length) {
176
+ if (this.not("./?#")) {
177
+ chars.push(this.url.at(this.index));
178
+ this.index++;
179
+ continue;
180
+ }
181
+ break;
182
+ }
183
+ return chars.join("");
184
+ }
185
+ not(text) {
186
+ const ch = this.url.at(this.index);
187
+ return text.indexOf(ch) === -1;
188
+ }
189
+ peek(str) {
190
+ return this.url.slice(this.index, this.index + str.length) === str;
191
+ }
192
+ ignore(str) {
193
+ while (this.peek(str)) this.index++;
194
+ }
195
+ };
196
+ //#endregion
197
+ //#region \0@oxc-project+runtime@0.126.0/helpers/decorateMetadata.js
198
+ function __decorateMetadata(k, v) {
199
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
200
+ }
201
+ //#endregion
202
+ //#region \0@oxc-project+runtime@0.126.0/helpers/decorate.js
203
+ function __decorate(decorators, target, key, desc) {
204
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
205
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
206
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
207
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
208
+ }
209
+ //#endregion
210
+ //#region src/providers/navigator.ts
211
+ var Navigator = class {
212
+ constructor(baseUrl) {
213
+ this.baseUrl = baseUrl;
214
+ }
215
+ };
216
+ function formatUrl(pathname, urlFormatParams) {
217
+ pathname = pathname.replace(/\/+/g, "/");
218
+ const { queryParams, fragment } = urlFormatParams;
219
+ return pathname + (queryParams ? "?" + formatQueryParams(queryParams) : "") + (fragment ? "#" + fragment : "");
220
+ }
221
+ function formatQueryParams(queryParams) {
222
+ const params = [];
223
+ Object.keys(queryParams).forEach((key) => {
224
+ const values = queryParams[key];
225
+ if (Array.isArray(values)) values.forEach((i) => {
226
+ params.push(`${key}=${decodeURIComponent(i)}`);
227
+ });
228
+ else params.push(`${key}=${decodeURIComponent(values)}`);
229
+ });
230
+ return params.join("&");
231
+ }
232
+ var BrowserNavigator = class BrowserNavigator extends Navigator {
233
+ onUrlChanged;
234
+ /** 挂载在 location 上的路径前缀;'' 或 '/' 表示站点根,不做剥离 */
235
+ get basePathPrefix() {
236
+ return this.baseUrl === "/" || this.baseUrl === "" ? "" : this.baseUrl;
237
+ }
238
+ get pathname() {
239
+ const pathname = location.pathname;
240
+ if (!this.basePathPrefix) return pathname;
241
+ return pathname.startsWith(this.basePathPrefix) ? pathname.substring(this.basePathPrefix.length) : pathname;
242
+ }
243
+ urlParser = new UrlParser();
244
+ urlTree = this.getUrlTree();
245
+ urlChangeEvent = new Subject();
246
+ subscription = new Subscription();
247
+ constructor(baseUrl, hooks = {}) {
248
+ super(baseUrl);
249
+ this.hooks = hooks;
250
+ this.onUrlChanged = this.urlChangeEvent.asObservable();
251
+ this.subscription.add(fromEvent(window, "popstate").subscribe(() => {
252
+ this.urlTree = this.getUrlTree();
253
+ this.urlChangeEvent.next();
254
+ }));
255
+ if (this.basePathPrefix && !location.pathname.startsWith(this.basePathPrefix)) history.replaceState(null, "", this.baseUrl);
256
+ }
257
+ to(pathName, relative, queryParams, fragment) {
258
+ const url = this.join(pathName, relative, queryParams, fragment);
259
+ if (location.origin + url === location.href) return true;
260
+ this.runHooks({
261
+ pathname: this.pathname,
262
+ queryParams: this.urlTree.queryParams,
263
+ fragment: this.urlTree.hash
264
+ }, {
265
+ pathname: pathName,
266
+ queryParams: queryParams || {},
267
+ fragment: fragment || null
268
+ }, () => {
269
+ history.pushState(null, "", url);
270
+ this.urlTree = this.getUrlTree();
271
+ this.urlChangeEvent.next();
272
+ });
273
+ return true;
274
+ }
275
+ replace(pathName, relative, queryParams, fragment) {
276
+ const url = this.join(pathName, relative, queryParams, fragment);
277
+ if (location.origin + url === location.href) return true;
278
+ this.runHooks({
279
+ pathname: this.pathname,
280
+ queryParams: this.urlTree.queryParams,
281
+ fragment: this.urlTree.hash
282
+ }, {
283
+ pathname: pathName,
284
+ queryParams: queryParams || {},
285
+ fragment: fragment || null
286
+ }, () => {
287
+ history.replaceState(null, "", url);
288
+ this.urlTree = this.getUrlTree();
289
+ this.urlChangeEvent.next();
290
+ });
291
+ return true;
292
+ }
293
+ join(pathname, relative, queryParams, fragment) {
294
+ if (pathname.startsWith("/")) return formatUrl(this.baseUrl + pathname, {
295
+ queryParams,
296
+ fragment
297
+ });
298
+ const beforePath = this.urlTree.paths.slice(0, relative.deep);
299
+ while (true) {
300
+ if (pathname.startsWith("./")) {
301
+ pathname = pathname.substring(2);
302
+ continue;
303
+ }
304
+ if (pathname.startsWith("../")) {
305
+ pathname = pathname.substring(3);
306
+ beforePath.pop();
307
+ continue;
308
+ }
309
+ break;
310
+ }
311
+ return formatUrl(this.baseUrl + "/" + beforePath.join("/") + "/" + pathname, {
312
+ queryParams,
313
+ fragment
314
+ });
315
+ }
316
+ back() {
317
+ history.back();
318
+ }
319
+ forward() {
320
+ history.forward();
321
+ }
322
+ go(offset) {
323
+ history.go(offset);
324
+ }
325
+ destroy() {
326
+ this.subscription.unsubscribe();
327
+ }
328
+ runHooks(beforeParams, currentParams, next) {
329
+ if (typeof this.hooks.beforeEach === "function") this.hooks.beforeEach?.(beforeParams, currentParams, () => {
330
+ next();
331
+ this.hooks.afterEach?.(currentParams);
332
+ });
333
+ else {
334
+ next();
335
+ this.hooks.afterEach?.(currentParams);
336
+ }
337
+ }
338
+ getUrlTree() {
339
+ return this.urlParser.parse(this.pathname + location.search + location.hash);
340
+ }
341
+ };
342
+ BrowserNavigator = __decorate([Injectable(), __decorateMetadata("design:paramtypes", [String, Object])], BrowserNavigator);
343
+ //#endregion
344
+ //#region src/hooks/use-query-params.ts
345
+ function useQueryParams() {
346
+ const router = inject(Router);
347
+ const navigator = inject(Navigator);
348
+ const params = { ...navigator.urlTree.queryParams };
349
+ const queryParams = new Proxy(params, readonlyProxyHandler);
350
+ const subscription = router.onRefresh.subscribe(() => {
351
+ comparePropsWithCallbacks(params, navigator.urlTree.queryParams, (key) => {
352
+ internalWrite(() => {
353
+ Reflect.deleteProperty(params, key);
354
+ });
355
+ }, (key, value) => {
356
+ internalWrite(() => {
357
+ params[key] = value;
358
+ });
359
+ }, (key, value) => {
360
+ internalWrite(() => {
361
+ params[key] = value;
362
+ });
363
+ });
364
+ });
365
+ onUnmounted(() => {
366
+ subscription.unsubscribe();
367
+ });
368
+ return queryParams;
369
+ }
370
+ //#endregion
371
+ //#region src/link.tsx
372
+ function Link(props) {
373
+ const navigator = inject(Navigator);
374
+ const router = inject(Router);
375
+ function getActive() {
376
+ return props.exact ? navigator.pathname === navigator.join(props.to, router) || navigator.pathname + "/" === navigator.join(props.to, router) : navigator.pathname.startsWith(navigator.join(props.to, router));
377
+ }
378
+ const isActive = reactive({ value: getActive() });
379
+ const subscription = navigator.onUrlChanged.subscribe(() => {
380
+ isActive.value = getActive();
381
+ });
382
+ onUnmounted(() => {
383
+ subscription.unsubscribe();
384
+ });
385
+ function navigate(ev) {
386
+ if ((!props.tag || props.tag === "a") && props.target === "_blank") return;
387
+ ev.preventDefault();
388
+ router.navigateTo(props.to, props.queryParams, props.fragment);
389
+ }
390
+ return () => {
391
+ const Tag = props.tag || "a";
392
+ const attrs = Object.assign({}, props, {
393
+ onClick(ev) {
394
+ navigate(ev);
395
+ props.onClick?.(ev);
396
+ },
397
+ ...props
398
+ });
399
+ if (Tag === "a") attrs.href = navigator.join(props.to, router, props.queryParams, props.fragment);
400
+ if (isActive.value && props.active) attrs.class = [attrs.class, props.active];
401
+ return /* @__PURE__ */ jsx(Tag, {
402
+ ...attrs,
403
+ children: props.children
404
+ });
405
+ };
406
+ }
407
+ //#endregion
408
+ //#region src/router-module.ts
409
+ var RouterModule = class {
410
+ subscription = new Subscription();
411
+ navigator;
412
+ constructor(baseUrl = "", hooks = {}) {
413
+ this.baseUrl = baseUrl;
414
+ this.navigator = new BrowserNavigator(this.baseUrl, hooks);
415
+ }
416
+ setup(app) {
417
+ const navigator = this.navigator;
418
+ const router = new Router(navigator, null);
419
+ this.subscription.add(navigator.onUrlChanged.subscribe(() => {
420
+ router.refresh();
421
+ }));
422
+ app.provide([{
423
+ provide: Navigator,
424
+ useValue: navigator
425
+ }, {
426
+ provide: Router,
427
+ useValue: router
428
+ }]);
429
+ }
430
+ onDestroy() {
431
+ this.subscription.unsubscribe();
432
+ this.navigator.destroy();
433
+ }
434
+ };
435
+ //#endregion
436
+ //#region src/router-outlet.tsx
437
+ var routerErrorFn = makeError("RouterOutlet");
438
+ function RouterOutlet(props) {
439
+ const router = inject(Router, null);
440
+ if (router === null) throw routerErrorFn("cannot found parent Router.");
441
+ const childRouter = new Router(inject(Navigator), router);
442
+ const Context = createContext([{
443
+ provide: Router,
444
+ useValue: childRouter
445
+ }]);
446
+ const children = shallowReactive({ value: null });
447
+ const subscription = router.onRefresh.subscribe(() => {
448
+ updateChildren();
449
+ });
450
+ onUnmounted(() => {
451
+ subscription.unsubscribe();
452
+ });
453
+ let currentComponent = null;
454
+ async function updateChildren() {
455
+ const routeConfig = router.consumeConfig(props.config);
456
+ if (!routeConfig) {
457
+ currentComponent = null;
458
+ children.value = props.children || null;
459
+ return;
460
+ }
461
+ if (typeof routeConfig.beforeEach === "function") {
462
+ if (!await routeConfig.beforeEach()) return;
463
+ }
464
+ if (routeConfig.component) _updateChildren(routeConfig.component);
465
+ else if (routeConfig.asyncComponent) _updateChildren(await routeConfig.asyncComponent());
466
+ if (typeof routeConfig.afterEach === "function") routeConfig.afterEach();
467
+ }
468
+ function _updateChildren(Component) {
469
+ childRouter.refresh();
470
+ if (Component !== currentComponent) children.value = /* @__PURE__ */ jsx(Component, {});
471
+ currentComponent = Component;
472
+ }
473
+ updateChildren();
474
+ return () => {
475
+ return /* @__PURE__ */ jsx(Context, { children: children.value });
476
+ };
477
+ }
478
+ //#endregion
479
+ export { BrowserNavigator, Link, Navigator, Router, RouterModule, RouterOutlet, UrlParser, formatQueryParams, formatUrl, useQueryParams };