@esmx/router 3.0.0-rc.12

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.
Files changed (61) hide show
  1. package/dist/history/abstract.d.ts +29 -0
  2. package/dist/history/abstract.mjs +107 -0
  3. package/dist/history/base.d.ts +79 -0
  4. package/dist/history/base.mjs +275 -0
  5. package/dist/history/html.d.ts +22 -0
  6. package/dist/history/html.mjs +181 -0
  7. package/dist/history/index.d.ts +7 -0
  8. package/dist/history/index.mjs +16 -0
  9. package/dist/index.d.ts +3 -0
  10. package/dist/index.mjs +3 -0
  11. package/dist/matcher/create-matcher.d.ts +5 -0
  12. package/dist/matcher/create-matcher.mjs +218 -0
  13. package/dist/matcher/create-matcher.spec.d.ts +1 -0
  14. package/dist/matcher/create-matcher.spec.mjs +0 -0
  15. package/dist/matcher/index.d.ts +1 -0
  16. package/dist/matcher/index.mjs +1 -0
  17. package/dist/router.d.ts +111 -0
  18. package/dist/router.mjs +399 -0
  19. package/dist/task-pipe/index.d.ts +1 -0
  20. package/dist/task-pipe/index.mjs +1 -0
  21. package/dist/task-pipe/task.d.ts +30 -0
  22. package/dist/task-pipe/task.mjs +66 -0
  23. package/dist/utils/bom.d.ts +5 -0
  24. package/dist/utils/bom.mjs +10 -0
  25. package/dist/utils/encoding.d.ts +48 -0
  26. package/dist/utils/encoding.mjs +44 -0
  27. package/dist/utils/guards.d.ts +9 -0
  28. package/dist/utils/guards.mjs +12 -0
  29. package/dist/utils/index.d.ts +7 -0
  30. package/dist/utils/index.mjs +27 -0
  31. package/dist/utils/path.d.ts +60 -0
  32. package/dist/utils/path.mjs +264 -0
  33. package/dist/utils/path.spec.d.ts +1 -0
  34. package/dist/utils/path.spec.mjs +30 -0
  35. package/dist/utils/scroll.d.ts +25 -0
  36. package/dist/utils/scroll.mjs +59 -0
  37. package/dist/utils/utils.d.ts +16 -0
  38. package/dist/utils/utils.mjs +11 -0
  39. package/dist/utils/warn.d.ts +2 -0
  40. package/dist/utils/warn.mjs +12 -0
  41. package/package.json +66 -0
  42. package/src/history/abstract.ts +149 -0
  43. package/src/history/base.ts +408 -0
  44. package/src/history/html.ts +231 -0
  45. package/src/history/index.ts +20 -0
  46. package/src/index.ts +3 -0
  47. package/src/matcher/create-matcher.spec.ts +3 -0
  48. package/src/matcher/create-matcher.ts +293 -0
  49. package/src/matcher/index.ts +1 -0
  50. package/src/router.ts +521 -0
  51. package/src/task-pipe/index.ts +1 -0
  52. package/src/task-pipe/task.ts +97 -0
  53. package/src/utils/bom.ts +14 -0
  54. package/src/utils/encoding.ts +153 -0
  55. package/src/utils/guards.ts +25 -0
  56. package/src/utils/index.ts +27 -0
  57. package/src/utils/path.spec.ts +44 -0
  58. package/src/utils/path.ts +397 -0
  59. package/src/utils/scroll.ts +120 -0
  60. package/src/utils/utils.ts +30 -0
  61. package/src/utils/warn.ts +13 -0
@@ -0,0 +1,149 @@
1
+ import type {
2
+ Route,
3
+ RouteRecord,
4
+ RouterInstance,
5
+ RouterRawLocation
6
+ } from '../types';
7
+ import { isPathWithProtocolOrDomain, normalizeLocation } from '../utils';
8
+ import { BaseRouterHistory } from './base';
9
+
10
+ export class AbstractHistory extends BaseRouterHistory {
11
+ index: number;
12
+ stack: RouteRecord[];
13
+
14
+ constructor(router: RouterInstance) {
15
+ super(router);
16
+ this.index = -1;
17
+ this.stack = [];
18
+ this.init();
19
+ }
20
+
21
+ async init({ replace }: { replace?: boolean } = { replace: true }) {
22
+ const { initUrl } = this.router.options;
23
+ if (initUrl !== undefined) {
24
+ // 存在 initUrl 则用 initUrl 进行初始化
25
+ if (replace) {
26
+ await this.replace(initUrl);
27
+ } else {
28
+ await this.push(initUrl);
29
+ }
30
+ }
31
+ }
32
+
33
+ destroy() {}
34
+
35
+ // 设置监听函数
36
+ setupListeners() {}
37
+
38
+ // 服务端判断是否是相同域名
39
+ isSameHost(location: RouterRawLocation, route: Route) {
40
+ const rawLocation =
41
+ typeof location === 'string' ? { path: location } : location;
42
+ if (rawLocation.path === undefined) {
43
+ rawLocation.path = this.current.fullPath;
44
+ }
45
+
46
+ // 服务端 base 需要包含域名,判断 base 是否包含传入路径的域名即可
47
+ const { base } = normalizeLocation(rawLocation, this.router.base);
48
+
49
+ return base.includes(route.hostname);
50
+ }
51
+
52
+ // 处理外站跳转逻辑
53
+ handleOutside(
54
+ location: RouterRawLocation,
55
+ replace = false,
56
+ isTriggerWithWindow = false
57
+ ) {
58
+ const { flag, route } = isPathWithProtocolOrDomain(location);
59
+ if (!flag) {
60
+ // 如果不以域名开头则跳出
61
+ return false;
62
+ }
63
+
64
+ const router = this.router;
65
+ const { validateOutside, handleOutside } = router.options;
66
+
67
+ // 如果域名相同 和 非外站(存在就算同域也会被视为外站的情况) 则跳出
68
+ const isSameHost = this.isSameHost(location, route);
69
+ if (isSameHost && !validateOutside?.({ router, location, route })) {
70
+ return false;
71
+ }
72
+
73
+ // 如果有配置跳转外站函数,则执行配置函数
74
+ handleOutside?.({
75
+ router,
76
+ route,
77
+ replace,
78
+ isTriggerWithWindow,
79
+ isSameHost
80
+ });
81
+
82
+ return true;
83
+ }
84
+
85
+ // 新增路由记录跳转
86
+ async push(location: RouterRawLocation) {
87
+ await this.jump(location, false);
88
+ }
89
+
90
+ /**
91
+ * 新开浏览器窗口的方法,在服务端会调用 push 作为替代
92
+ */
93
+ async pushWindow(location: RouterRawLocation) {
94
+ await this._jump(location, false, true);
95
+ }
96
+
97
+ // 替换当前路由记录跳转
98
+ async replace(location: RouterRawLocation) {
99
+ await this.jump(location, true);
100
+ }
101
+
102
+ /**
103
+ * 替换当前浏览器窗口的方法,在服务端会调用 replace 作为替代
104
+ */
105
+ async replaceWindow(location: RouterRawLocation) {
106
+ await this._jump(location, true, true);
107
+ }
108
+
109
+ // 跳转方法
110
+ async jump(location: RouterRawLocation, replace = false) {
111
+ await this._jump(location, replace);
112
+ }
113
+
114
+ private async _jump(
115
+ location: RouterRawLocation,
116
+ replace = false,
117
+ isTriggerWithWindow = false
118
+ ) {
119
+ if (this.handleOutside(location, replace, isTriggerWithWindow)) {
120
+ return;
121
+ }
122
+
123
+ await this.transitionTo(location, (route) => {
124
+ const index = replace ? this.index : this.index + 1;
125
+ this.stack = this.stack.slice(0, index).concat(route);
126
+ });
127
+ }
128
+
129
+ go(delta: number): void {
130
+ const targetIndex = this.index + delta;
131
+ // 浏览器在跳转到不存在的历史记录时不会进行跳转
132
+ if (targetIndex < 0 || targetIndex >= this.stack.length) {
133
+ return;
134
+ }
135
+ const route = this.stack[targetIndex];
136
+ this.index = targetIndex;
137
+ this.updateRoute(route);
138
+ }
139
+
140
+ /* 路由历史记录前进方法 */
141
+ forward() {
142
+ this.go(1);
143
+ }
144
+
145
+ /* 路由历史记录后退方法 */
146
+ back() {
147
+ this.go(-1);
148
+ }
149
+ }
@@ -0,0 +1,408 @@
1
+ import { type Tasks, createTasks } from '../task-pipe';
2
+ import type {
3
+ Awaitable,
4
+ NavigationGuard,
5
+ NavigationGuardAfter,
6
+ RouteRecord,
7
+ RouterHistory,
8
+ RouterInstance,
9
+ RouterRawLocation
10
+ } from '../types';
11
+ import {
12
+ isESModule,
13
+ isEqualRoute,
14
+ isSameRoute,
15
+ normalizeLocation,
16
+ stringifyPath
17
+ } from '../utils';
18
+
19
+ /**
20
+ * 创建一个路由记录
21
+ */
22
+ function createRouteRecord(route: Partial<RouteRecord> = {}): RouteRecord {
23
+ return {
24
+ base: '',
25
+ path: '/',
26
+ fullPath: '/',
27
+ meta: {},
28
+ matched: [],
29
+ query: {},
30
+ queryArray: {},
31
+ params: {},
32
+ hash: '',
33
+ state: {},
34
+ ...route
35
+ };
36
+ }
37
+
38
+ export abstract class BaseRouterHistory implements RouterHistory {
39
+ /**
40
+ * 路由类实例
41
+ */
42
+ public router: RouterInstance;
43
+
44
+ /**
45
+ * 路由是否冻结
46
+ */
47
+ public get isFrozen() {
48
+ return this.router.isFrozen;
49
+ }
50
+
51
+ /**
52
+ * 匹配的当前路由
53
+ */
54
+ public current: RouteRecord = createRouteRecord();
55
+
56
+ constructor(router: RouterInstance) {
57
+ this.router = router;
58
+ Object.defineProperty(this, 'router', {
59
+ enumerable: false
60
+ });
61
+ }
62
+
63
+ /**
64
+ * 更新当前路由
65
+ */
66
+ updateRoute(route: RouteRecord) {
67
+ this.current = route;
68
+ this.router.updateRoute(route);
69
+ }
70
+
71
+ /**
72
+ * 解析路由
73
+ */
74
+ resolve(location: RouterRawLocation): RouteRecord {
75
+ const rawLocation =
76
+ typeof location === 'string' ? { path: location } : location;
77
+ if (rawLocation.path === undefined) {
78
+ rawLocation.path = this.current.fullPath;
79
+ }
80
+ const { base, ...normalizedLocation } = normalizeLocation(
81
+ rawLocation,
82
+ this.router.base
83
+ );
84
+
85
+ // 匹配成功则返回匹配值
86
+ const matcher = this.router.matcher.match(normalizedLocation, { base });
87
+ if (matcher) {
88
+ return matcher;
89
+ }
90
+
91
+ // 匹配失败则返回目标路径
92
+ const {
93
+ path = '',
94
+ params = {},
95
+ query = {},
96
+ queryArray = {},
97
+ hash = '',
98
+ state = {}
99
+ } = normalizedLocation;
100
+ const route = createRouteRecord({
101
+ base,
102
+ fullPath: stringifyPath({
103
+ pathname: path,
104
+ query,
105
+ queryArray,
106
+ hash
107
+ }),
108
+ path,
109
+ params,
110
+ query,
111
+ queryArray,
112
+ hash,
113
+ state
114
+ });
115
+ return route;
116
+ }
117
+
118
+ /**
119
+ * 核心跳转方法
120
+ */
121
+ async transitionTo(
122
+ location: RouterRawLocation,
123
+ onComplete?: (route: RouteRecord) => void
124
+ ) {
125
+ if (this.isFrozen) return;
126
+ // 寻找即将跳转路径匹配到的路由对象
127
+ const route = this.resolve(location);
128
+
129
+ this.abortTask();
130
+
131
+ // 禁止重复跳转
132
+ if (isEqualRoute(this.current, route)) {
133
+ return;
134
+ }
135
+
136
+ await this.runTask(this.current, route, onComplete);
137
+ }
138
+
139
+ /**
140
+ * TODO 逻辑解耦,抽离到task
141
+ * 重定向方法
142
+ */
143
+ async redirectTo(
144
+ location: RouterRawLocation,
145
+ from: RouteRecord,
146
+ onComplete?: (route: RouteRecord) => void
147
+ ) {
148
+ // 寻找即将跳转路径匹配到的路由对象
149
+ const route = this.resolve(location);
150
+ this.abortTask();
151
+
152
+ // 禁止重复跳转
153
+ if (isEqualRoute(this.current, route)) {
154
+ return;
155
+ }
156
+
157
+ await this.runTask(
158
+ this.current,
159
+ {
160
+ ...route,
161
+ redirectedFrom: from
162
+ },
163
+ onComplete
164
+ );
165
+ }
166
+
167
+ /* 当前执行的任务 */
168
+ tasks: Tasks | null = null;
169
+
170
+ /* 取消任务 */
171
+ async abortTask() {
172
+ this.tasks?.abort();
173
+ }
174
+
175
+ /**
176
+ * 执行任务
177
+ * 任务分为三部分: 前置守卫(beforeEach、beforeEnter、beforeUpdate、beforeLeave)、加载路由(loadRoute)、后置守卫(afterEach)
178
+ * 根据触发方式不同,执行顺序分别为:
179
+ * 进入路由时: beforeEach -> beforeEnter -> loadRoute -> afterEach
180
+ * 更新路由时: beforeEach -> beforeUpdate -> afterEach
181
+ * 离开路由进入新路由时: beforeLeave -> beforeEach -> beforeEnter -> loadRoute -> afterEach
182
+ * @param from
183
+ * @param to
184
+ */
185
+ async runTask(
186
+ from: RouteRecord,
187
+ to: RouteRecord,
188
+ onComplete?: (route: RouteRecord) => void
189
+ ) {
190
+ const {
191
+ beforeEach,
192
+ beforeEnter,
193
+ beforeUpdate,
194
+ beforeLeave,
195
+ afterEach,
196
+ loadRoute
197
+ } = getNavigationHooks(this.router, from, to);
198
+
199
+ /* 前置钩子任务 */
200
+ const guardBeforeTasks: Tasks<NavigationGuard> = createTasks();
201
+
202
+ /* 后置钩子任务 */
203
+ const guardAfterTasks: Tasks<NavigationGuardAfter> =
204
+ createTasks<NavigationGuardAfter>();
205
+
206
+ /* 加载路由任务 */
207
+ const loadRouteTasks: Tasks<() => Awaitable<any>> =
208
+ createTasks<() => Awaitable<any>>();
209
+
210
+ if (isSameRoute(from, to)) {
211
+ // from 和 to 是相同路由说明为更新路由
212
+ guardBeforeTasks.add([...beforeEach, ...beforeUpdate]);
213
+
214
+ guardAfterTasks.add(afterEach);
215
+ } else {
216
+ // 反之为进入新路由
217
+ guardBeforeTasks.add([
218
+ ...beforeLeave,
219
+ ...beforeEach,
220
+ ...beforeEnter
221
+ ]);
222
+ loadRouteTasks.add(loadRoute);
223
+
224
+ guardAfterTasks.add(afterEach);
225
+ }
226
+
227
+ this.tasks = guardBeforeTasks;
228
+ await guardBeforeTasks.run({
229
+ cb: async (res) => {
230
+ switch (typeof res) {
231
+ case 'boolean':
232
+ if (!res) {
233
+ this.tasks?.abort();
234
+ }
235
+ break;
236
+
237
+ case 'undefined':
238
+ break;
239
+
240
+ default:
241
+ await this.redirectTo(res, from, onComplete);
242
+ break;
243
+ }
244
+ },
245
+ final: async () => {
246
+ this.tasks = loadRouteTasks;
247
+ await loadRouteTasks.run();
248
+ }
249
+ });
250
+
251
+ if (this.tasks?.status === 'finished') {
252
+ this.tasks = null;
253
+ guardAfterTasks.run();
254
+
255
+ onComplete && onComplete(to);
256
+ this.updateRoute(to);
257
+ }
258
+ }
259
+
260
+ /**
261
+ * 新开浏览器窗口的方法,在服务端会调用 push 作为替代
262
+ */
263
+ abstract pushWindow(location: RouterRawLocation): void;
264
+
265
+ /**
266
+ * 替换当前浏览器窗口的方法,在服务端会调用 replace 作为替代
267
+ */
268
+ abstract replaceWindow(location: RouterRawLocation): void;
269
+
270
+ /**
271
+ * 跳转方法,会创建新的历史纪录
272
+ */
273
+ abstract push(location: RouterRawLocation): Promise<void>;
274
+
275
+ /**
276
+ * 跳转方法,替换当前历史记录
277
+ */
278
+ abstract replace(location: RouterRawLocation): Promise<void>;
279
+
280
+ /**
281
+ * 路由移动到指定历史记录方法
282
+ */
283
+ abstract go(delta: number): void;
284
+
285
+ /* 路由历史记录前进方法 */
286
+ abstract forward(): void;
287
+
288
+ /* 路由历史记录后退方法 */
289
+ abstract back(): void;
290
+
291
+ /**
292
+ * 初始化方法
293
+ */
294
+ abstract init(params?: { replace?: boolean }): Promise<void>;
295
+
296
+ /**
297
+ * 卸载方法
298
+ */
299
+ abstract destroy(): void;
300
+ }
301
+
302
+ /**
303
+ * 获取路由导航钩子
304
+ * 路由守卫钩子时顺序执行的,但是加载路由的钩子不依赖顺序
305
+ */
306
+ function getNavigationHooks(
307
+ router: RouterInstance,
308
+ from: RouteRecord,
309
+ to: RouteRecord
310
+ ): {
311
+ beforeEach: NavigationGuard[];
312
+ afterEach: NavigationGuardAfter[];
313
+ beforeEnter: NavigationGuard[];
314
+ beforeUpdate: NavigationGuard[];
315
+ beforeLeave: NavigationGuard[];
316
+ loadRoute: Array<() => Promise<any>>;
317
+ } {
318
+ const beforeEach = router.guards.beforeEach.map((guard) => {
319
+ return () => {
320
+ return guard(from, to);
321
+ };
322
+ });
323
+ const afterEach = router.guards.afterEach.map((guard) => {
324
+ return () => {
325
+ return guard(from, to);
326
+ };
327
+ });
328
+
329
+ const { beforeLeave } = from.matched.reduce<{
330
+ beforeLeave: NavigationGuard[];
331
+ }>(
332
+ (acc, { beforeLeave }) => {
333
+ if (beforeLeave) {
334
+ acc.beforeLeave.unshift(() => {
335
+ return beforeLeave(from, to);
336
+ });
337
+ }
338
+ return acc;
339
+ },
340
+ {
341
+ beforeLeave: []
342
+ }
343
+ );
344
+
345
+ const { beforeEnter, beforeUpdate } = to.matched.reduce<{
346
+ beforeEnter: NavigationGuard[];
347
+ beforeUpdate: NavigationGuard[];
348
+ }>(
349
+ (acc, { beforeEnter, beforeUpdate }) => {
350
+ if (beforeEnter) {
351
+ acc.beforeEnter.push(() => {
352
+ return beforeEnter(from, to);
353
+ });
354
+ }
355
+
356
+ if (beforeUpdate) {
357
+ acc.beforeUpdate.push(() => {
358
+ return beforeUpdate(from, to);
359
+ });
360
+ }
361
+ return acc;
362
+ },
363
+ {
364
+ beforeEnter: [],
365
+ beforeUpdate: []
366
+ }
367
+ );
368
+
369
+ const loadRoute = [
370
+ async () => {
371
+ return Promise.all(
372
+ to.matched.reduce<Array<Promise<any>>>((acc, route) => {
373
+ if (!route.component && route.asyncComponent) {
374
+ acc.push(
375
+ new Promise<void>((resolve, reject) => {
376
+ if (!route.component && route.asyncComponent) {
377
+ route.asyncComponent().then((resolved) => {
378
+ if (!resolved) {
379
+ reject(
380
+ new Error(
381
+ `Couldn't resolve component at "${route.path}". Ensure you passed a function that returns a promise.`
382
+ )
383
+ );
384
+ }
385
+ route.component = isESModule(resolved)
386
+ ? resolved.default
387
+ : resolved;
388
+ resolve();
389
+ });
390
+ }
391
+ })
392
+ );
393
+ }
394
+ return acc;
395
+ }, [])
396
+ );
397
+ }
398
+ ];
399
+
400
+ return {
401
+ beforeEach,
402
+ afterEach,
403
+ beforeEnter,
404
+ beforeUpdate,
405
+ beforeLeave,
406
+ loadRoute
407
+ };
408
+ }