@route-forge/core 0.3.0 → 1.0.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 +91 -2
- package/dist/codegen.d.cts +1 -1
- package/dist/codegen.d.ts +1 -1
- package/dist/index.cjs +119 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -10
- package/dist/index.d.ts +4 -10
- package/dist/index.js +119 -54
- package/dist/index.js.map +1 -1
- package/dist/{types-CBurRtGK.d.cts → types-yzY9-FWd.d.cts} +92 -13
- package/dist/{types-CBurRtGK.d.ts → types-yzY9-FWd.d.ts} +92 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @route-forge/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
框架无关的命名路由客户端核心:分级懒加载、隔离缓存、并发去重、拦截器。
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
pnpm add @route-forge/core
|
|
9
|
-
#
|
|
9
|
+
# 可选:axios 适配器
|
|
10
10
|
pnpm add axios
|
|
11
11
|
```
|
|
12
12
|
|
|
@@ -29,6 +29,13 @@ const url = forge.route('public', 'login.show')
|
|
|
29
29
|
// url() 是 route() 的语义别名
|
|
30
30
|
const url2 = forge.url('public', 'login.show')
|
|
31
31
|
|
|
32
|
+
// 检查路由是否存在
|
|
33
|
+
forge.hasRoute('admin', 'users.show') // true / false
|
|
34
|
+
|
|
35
|
+
// 获取路由元信息
|
|
36
|
+
const routes = forge.getRoutes('admin') // 指定层级
|
|
37
|
+
const allRoutes = forge.getRoutes() // 全部层级
|
|
38
|
+
|
|
32
39
|
// 检查层级是否已加载
|
|
33
40
|
if (!forge.isLoaded('admin')) {
|
|
34
41
|
await forge.load('admin')
|
|
@@ -86,6 +93,88 @@ forge.api('admin', 'search.show', {
|
|
|
86
93
|
|
|
87
94
|
规则:`params` 优先 > 平铺 `string|number` → 路径参数 > 对象类型按原定义(`query`/`body`/`headers`)。
|
|
88
95
|
|
|
96
|
+
## 认证(Authentication)
|
|
97
|
+
|
|
98
|
+
Route Forge 不内置登录态管理,认证逻辑通过拦截器实现,灵活且完全可控。
|
|
99
|
+
|
|
100
|
+
### Token 注入
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
// 声明式(初始化时配置)
|
|
104
|
+
const forge = createRouteForge({
|
|
105
|
+
endpoint: '/_forge/routes',
|
|
106
|
+
interceptors: {
|
|
107
|
+
request: [
|
|
108
|
+
(config) => {
|
|
109
|
+
const token = authStore.getToken()
|
|
110
|
+
if (token) {
|
|
111
|
+
config.headers.Authorization = `Bearer ${token}`
|
|
112
|
+
}
|
|
113
|
+
return config
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// 或运行时动态注册
|
|
120
|
+
forge.interceptors.request.use((config) => {
|
|
121
|
+
const token = authStore.getToken()
|
|
122
|
+
if (token) {
|
|
123
|
+
config.headers.Authorization = `Bearer ${token}`
|
|
124
|
+
}
|
|
125
|
+
return config
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 401 响应处理
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
forge.interceptors.response.use(
|
|
133
|
+
(res) => res, // 2xx 正常通过
|
|
134
|
+
(err) => {
|
|
135
|
+
if (err instanceof HTTPError && err.context?.status === 401) {
|
|
136
|
+
authStore.logout()
|
|
137
|
+
window.location.href = '/login'
|
|
138
|
+
}
|
|
139
|
+
return Promise.reject(err)
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 登出清理
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
function logout() {
|
|
148
|
+
authStore.clearToken()
|
|
149
|
+
forge.invalidate() // 清空路由缓存
|
|
150
|
+
forge.interceptors.request.clear() // 清空拦截器
|
|
151
|
+
forge.interceptors.response.clear()
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
> 拦截器 API 与 axios 完全一致(`use` / `eject` / `clear`),如果项目已使用 axios 拦截器,可跳过此节——
|
|
156
|
+
> `adapter: 'auto'` 模式下宿主 axios 的拦截器会自动生效。
|
|
157
|
+
|
|
158
|
+
## 加载状态跟踪
|
|
159
|
+
|
|
160
|
+
核心始终跟踪并发 API 请求的加载状态,无需配置。不需要使用时,不调用相关 API 即可。
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// 查询当前是否处于加载中
|
|
164
|
+
forge.isLoading() // boolean
|
|
165
|
+
|
|
166
|
+
// 订阅状态变更
|
|
167
|
+
const unsub = forge.onLoadingChange((event) => {
|
|
168
|
+
console.log(event.loading) // true / false
|
|
169
|
+
console.log(event.count) // 当前并发请求数
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// 取消订阅
|
|
173
|
+
unsub()
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
> 加载状态始终跟踪,用户不使用则不订阅即可。Vue/React 包可通过 `onLoadingChange` 订阅状态变更驱动组件显隐。
|
|
177
|
+
|
|
89
178
|
## 文档
|
|
90
179
|
|
|
91
180
|
- 仓库主页: https://github.com/xyj2156/route-forge
|
package/dist/codegen.d.cts
CHANGED
package/dist/codegen.d.ts
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -50,7 +50,12 @@ var RouteCache = class {
|
|
|
50
50
|
return entry;
|
|
51
51
|
}
|
|
52
52
|
set(resp) {
|
|
53
|
-
|
|
53
|
+
let ttl;
|
|
54
|
+
if (resp.cache !== void 0 && resp.cache !== null) {
|
|
55
|
+
ttl = resp.cache > 0 ? Math.min(resp.cache, this.fallbackTtl) : resp.cache;
|
|
56
|
+
} else {
|
|
57
|
+
ttl = this.fallbackTtl;
|
|
58
|
+
}
|
|
54
59
|
const entry = {
|
|
55
60
|
level: resp.level,
|
|
56
61
|
routes: resp.routes,
|
|
@@ -135,14 +140,6 @@ var MissingRouteParamError = class extends ForgeError {
|
|
|
135
140
|
});
|
|
136
141
|
}
|
|
137
142
|
};
|
|
138
|
-
var InsufficientAuthError = class extends ForgeError {
|
|
139
|
-
constructor(level) {
|
|
140
|
-
super(`Insufficient auth: level "${level}" requires login`, {
|
|
141
|
-
code: "RF_FE_004",
|
|
142
|
-
level
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
143
|
var AdapterNotFoundError = class extends ForgeError {
|
|
147
144
|
constructor(adapter) {
|
|
148
145
|
super(`Adapter "${adapter}" not available; install axios or use 'builtin'`, {
|
|
@@ -369,6 +366,65 @@ async function resolveAdapter(opts) {
|
|
|
369
366
|
return createBuiltinHttp(opts.forgeInterceptors);
|
|
370
367
|
}
|
|
371
368
|
|
|
369
|
+
// src/loading.ts
|
|
370
|
+
var LoadingTracker = class {
|
|
371
|
+
constructor() {
|
|
372
|
+
/** 当前并发请求计数 */
|
|
373
|
+
this.count = 0;
|
|
374
|
+
/** 订阅者集合 */
|
|
375
|
+
this.subscribers = /* @__PURE__ */ new Set();
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* 开始一次加载(计数器 +1)
|
|
379
|
+
*/
|
|
380
|
+
start() {
|
|
381
|
+
this.count++;
|
|
382
|
+
this.notify();
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* 结束一次加载(计数器 -1)
|
|
386
|
+
*/
|
|
387
|
+
stop() {
|
|
388
|
+
this.count = Math.max(0, this.count - 1);
|
|
389
|
+
this.notify();
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* 查询当前是否处于加载中
|
|
393
|
+
*/
|
|
394
|
+
isLoading() {
|
|
395
|
+
return this.count > 0;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* 获取当前并发计数
|
|
399
|
+
*/
|
|
400
|
+
getCount() {
|
|
401
|
+
return this.count;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* 订阅加载状态变更
|
|
405
|
+
* @returns 取消订阅函数
|
|
406
|
+
*/
|
|
407
|
+
subscribe(cb) {
|
|
408
|
+
this.subscribers.add(cb);
|
|
409
|
+
return () => {
|
|
410
|
+
this.subscribers.delete(cb);
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/** 通知所有订阅者 */
|
|
414
|
+
notify() {
|
|
415
|
+
const event = {
|
|
416
|
+
loading: this.count > 0,
|
|
417
|
+
count: this.count
|
|
418
|
+
};
|
|
419
|
+
for (const cb of this.subscribers) {
|
|
420
|
+
try {
|
|
421
|
+
cb(event);
|
|
422
|
+
} catch {
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
|
|
372
428
|
// src/forge.ts
|
|
373
429
|
var DEFAULT_TIMEOUT = 3e4;
|
|
374
430
|
var DEFAULT_CACHE_TTL = 3600;
|
|
@@ -378,10 +434,10 @@ function createRouteForge(options) {
|
|
|
378
434
|
adapter = "auto",
|
|
379
435
|
timeout = DEFAULT_TIMEOUT,
|
|
380
436
|
baseURL = "",
|
|
381
|
-
auth,
|
|
382
437
|
interceptors: declarativeInterceptors,
|
|
383
438
|
cache: cacheOpts = {}
|
|
384
439
|
} = options;
|
|
440
|
+
const loadingTracker = new LoadingTracker();
|
|
385
441
|
const explicitLevels = options.levels;
|
|
386
442
|
const explicitEager = options.eager;
|
|
387
443
|
const explicitStrict = options.strict ?? false;
|
|
@@ -442,8 +498,12 @@ function createRouteForge(options) {
|
|
|
442
498
|
} else {
|
|
443
499
|
effectiveLevels = backendLevels;
|
|
444
500
|
}
|
|
501
|
+
const backendEager = backendLevels.filter((lvl) => summary.levels[lvl]?.load === "eager");
|
|
445
502
|
if (!explicitEager) {
|
|
446
|
-
effectiveEager =
|
|
503
|
+
effectiveEager = backendEager;
|
|
504
|
+
} else {
|
|
505
|
+
const union = /* @__PURE__ */ new Set([...backendEager, ...explicitEager]);
|
|
506
|
+
effectiveEager = [...union];
|
|
447
507
|
}
|
|
448
508
|
});
|
|
449
509
|
autoDiscoveryPromise.catch(() => {
|
|
@@ -454,15 +514,23 @@ function createRouteForge(options) {
|
|
|
454
514
|
const requestInterceptors = new InterceptorManagerImpl();
|
|
455
515
|
const responseInterceptors = new InterceptorManagerImpl();
|
|
456
516
|
if (declarativeInterceptors?.request) {
|
|
457
|
-
for (const
|
|
458
|
-
|
|
459
|
-
|
|
517
|
+
for (const entry of declarativeInterceptors.request) {
|
|
518
|
+
if (typeof entry === "function") {
|
|
519
|
+
requestInterceptors.use(entry);
|
|
520
|
+
} else {
|
|
521
|
+
const [onFulfilled, onRejected] = entry;
|
|
522
|
+
requestInterceptors.use(onFulfilled, onRejected);
|
|
523
|
+
}
|
|
460
524
|
}
|
|
461
525
|
}
|
|
462
526
|
if (declarativeInterceptors?.response) {
|
|
463
|
-
for (const
|
|
464
|
-
|
|
465
|
-
|
|
527
|
+
for (const entry of declarativeInterceptors.response) {
|
|
528
|
+
if (typeof entry === "function") {
|
|
529
|
+
responseInterceptors.use(entry);
|
|
530
|
+
} else {
|
|
531
|
+
const [onFulfilled, onRejected] = entry;
|
|
532
|
+
responseInterceptors.use(onFulfilled, onRejected);
|
|
533
|
+
}
|
|
466
534
|
}
|
|
467
535
|
}
|
|
468
536
|
const adapterPromise = resolveAdapter({
|
|
@@ -482,14 +550,6 @@ function createRouteForge(options) {
|
|
|
482
550
|
return adapterObj;
|
|
483
551
|
}
|
|
484
552
|
const inflight = /* @__PURE__ */ new Map();
|
|
485
|
-
function isAuthRequired(level) {
|
|
486
|
-
return Boolean(auth?.levels?.[level]);
|
|
487
|
-
}
|
|
488
|
-
function assertAuth(level) {
|
|
489
|
-
if (isAuthRequired(level) && auth?.state && !auth.state()) {
|
|
490
|
-
throw new InsufficientAuthError(level);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
553
|
function assertLevelDeclared(level) {
|
|
494
554
|
if (!effectiveLevels.includes(level)) {
|
|
495
555
|
throw new UnknownLevelError(level);
|
|
@@ -527,7 +587,6 @@ function createRouteForge(options) {
|
|
|
527
587
|
}
|
|
528
588
|
async function loadOne(level) {
|
|
529
589
|
assertLevelDeclared(level);
|
|
530
|
-
assertAuth(level);
|
|
531
590
|
if (cache.get(level)) return;
|
|
532
591
|
const existing = inflight.get(level);
|
|
533
592
|
if (existing) return existing;
|
|
@@ -603,7 +662,6 @@ function createRouteForge(options) {
|
|
|
603
662
|
return doApiCall(meta, params);
|
|
604
663
|
}
|
|
605
664
|
async function doApiCall(meta, params) {
|
|
606
|
-
assertAuth(meta.level ?? "");
|
|
607
665
|
const { pathParams, query, body, headers } = resolveApiParams(params);
|
|
608
666
|
const method = pickMethod(meta);
|
|
609
667
|
const urlWithQuery = appendQuery(buildRequestUrl(meta, pathParams), query);
|
|
@@ -620,34 +678,39 @@ function createRouteForge(options) {
|
|
|
620
678
|
};
|
|
621
679
|
const adp = await ensureAdapter();
|
|
622
680
|
const finalConfig = adp.runsInterceptors ? config : await runRequestInterceptors(requestInterceptors, config);
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
route
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
681
|
+
loadingTracker.start();
|
|
682
|
+
try {
|
|
683
|
+
const source = adp.request(finalConfig).then(
|
|
684
|
+
(resp) => {
|
|
685
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
686
|
+
throw new HTTPError(
|
|
687
|
+
`HTTP ${resp.status} for route "${resp.route}" (${resp.method} ${resp.url})`,
|
|
688
|
+
{
|
|
689
|
+
route: resp.route,
|
|
690
|
+
level: resp.level,
|
|
691
|
+
status: resp.status,
|
|
692
|
+
url: resp.url,
|
|
693
|
+
method: resp.method
|
|
694
|
+
}
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
return resp;
|
|
698
|
+
},
|
|
699
|
+
(err) => {
|
|
700
|
+
if (err instanceof ForgeError) throw err;
|
|
701
|
+
throw new NetworkError(
|
|
702
|
+
err instanceof Error ? err.message : String(err),
|
|
703
|
+
meta.name,
|
|
704
|
+
meta.level,
|
|
705
|
+
err
|
|
635
706
|
);
|
|
636
707
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
meta.name,
|
|
644
|
-
meta.level,
|
|
645
|
-
err
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
);
|
|
649
|
-
if (adp.runsInterceptors) return source;
|
|
650
|
-
return runResponseInterceptors(responseInterceptors, source);
|
|
708
|
+
);
|
|
709
|
+
const result = adp.runsInterceptors ? await source : await runResponseInterceptors(responseInterceptors, source);
|
|
710
|
+
return result;
|
|
711
|
+
} finally {
|
|
712
|
+
loadingTracker.stop();
|
|
713
|
+
}
|
|
651
714
|
}
|
|
652
715
|
function invalidate(level) {
|
|
653
716
|
if (level) cache.del(level);
|
|
@@ -700,6 +763,8 @@ function createRouteForge(options) {
|
|
|
700
763
|
isLoaded,
|
|
701
764
|
hasRoute,
|
|
702
765
|
getRoutes,
|
|
766
|
+
isLoading: () => loadingTracker.isLoading(),
|
|
767
|
+
onLoadingChange: (cb) => loadingTracker.subscribe(cb),
|
|
703
768
|
interceptors: {
|
|
704
769
|
request: requestInterceptors,
|
|
705
770
|
response: responseInterceptors
|
|
@@ -765,9 +830,9 @@ function resolveApiParams(input) {
|
|
|
765
830
|
exports.AdapterNotFoundError = AdapterNotFoundError;
|
|
766
831
|
exports.ForgeError = ForgeError;
|
|
767
832
|
exports.HTTPError = HTTPError;
|
|
768
|
-
exports.InsufficientAuthError = InsufficientAuthError;
|
|
769
833
|
exports.InterceptorManagerImpl = InterceptorManagerImpl;
|
|
770
834
|
exports.InvalidInterceptorReturnError = InvalidInterceptorReturnError;
|
|
835
|
+
exports.LoadingTracker = LoadingTracker;
|
|
771
836
|
exports.MissingRouteParamError = MissingRouteParamError;
|
|
772
837
|
exports.NetworkError = NetworkError;
|
|
773
838
|
exports.RouteCache = RouteCache;
|