@route-forge/vue 2.1.0 → 2.2.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 +199 -82
- package/README_zh.md +261 -0
- package/dist/index.cjs +107 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +94 -5
- package/dist/index.d.ts +94 -5
- package/dist/index.js +107 -6
- package/dist/index.js.map +1 -1
- package/package.json +17 -4
package/README.md
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
# @route-forge/vue
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
`useForgeRoute`)。
|
|
3
|
+
**English** | [中文](./README_zh.md)
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
The Vue 3 integration for Route Forge: a plugin (`createRouteForgePlugin`), three composables (`useForge` / `useForgeApi` / `useForgeRoute`), and two components (`ForgeRoute` / `ForgeLink`) for calling APIs **by route name** and generating reactive links inside components, with loading and error states managed for you.
|
|
6
|
+
|
|
7
|
+
> All core capabilities (tiered lazy loading, isolated cache, interceptors, request cancellation, type safety) come from [@route-forge/core](../core/README.md); this package only adds Vue reactivity: `levelLoaded` is a `Ref<boolean>`, `pending` / `error` are `Ref`s, and URL generation returns a `ComputedRef<string>`.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
7
10
|
|
|
8
11
|
```bash
|
|
9
12
|
pnpm add @route-forge/vue @route-forge/core
|
|
10
13
|
```
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
Requires Vue 3.3+ (Vue 2 is not supported).
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
13
18
|
|
|
14
19
|
```ts
|
|
15
20
|
// main.ts
|
|
@@ -23,122 +28,234 @@ const plugin = createRouteForgePlugin({
|
|
|
23
28
|
|
|
24
29
|
const app = createApp(App)
|
|
25
30
|
app.use(plugin)
|
|
26
|
-
//
|
|
27
|
-
//
|
|
31
|
+
// Recommended: mount the app after ready() (summary + eager levels fully loaded);
|
|
32
|
+
// sync methods like route()/hasRoute() are then safe immediately
|
|
28
33
|
plugin.ready()
|
|
29
34
|
.then(() => app.mount('#app'))
|
|
30
35
|
.catch((err) => {
|
|
31
|
-
//
|
|
36
|
+
// Always handle this: the summary endpoint may be unreachable
|
|
37
|
+
// (network error / non-2xx / timeout) — avoid a silent blank page
|
|
32
38
|
console.error('[route-forge] init failed', err)
|
|
33
39
|
})
|
|
34
40
|
```
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
> Plugin options are exactly `createRouteForge(options)` (`endpoint` / `summary` / `levels` / `eager` / `adapter` / `cache` / `interceptors` / `timeout` / `baseURL`); full options table in the [core README](../core/README.md#options-createrouteforgeoptions). `options` itself is optional — when the summary is embedded in the page via `@forgeSummary` (`window.__ROUTE_FORGE__`), install with a bare `createRouteForgePlugin()`.
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
## useForge — the core composable
|
|
45
|
+
|
|
46
|
+
Without `level` it returns the full `RouteForge` instance; with `level` it delegates to `forge.use(level, prefix?)` (triggering the level load) and returns a `VueBoundForge`:
|
|
39
47
|
|
|
40
48
|
```ts
|
|
41
49
|
import { useForge } from '@route-forge/vue'
|
|
42
50
|
|
|
43
|
-
//
|
|
51
|
+
// Unbound — full RouteForge instance
|
|
44
52
|
const forge = useForge()
|
|
45
|
-
forge.api('admin', 'users.show', { user: 1 }) //
|
|
46
|
-
forge.ready().then(f => f.use('admin')) //
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
forge.invalidate('admin') // 失效缓存
|
|
53
|
+
forge.api('admin', 'users.show', { user: 1 }) // call with level + route name
|
|
54
|
+
forge.ready().then(f => f.use('admin')) // bind a level once ready
|
|
55
|
+
|
|
56
|
+
// Bound to a level — callable directly, load triggered automatically
|
|
57
|
+
const users = useForge('admin')
|
|
58
|
+
users.level // → 'admin'
|
|
59
|
+
users.levelLoaded // Ref<boolean>, flips to true once loaded
|
|
60
|
+
users('users.show', { user: 1 }) // callable (= users.api() shorthand)
|
|
61
|
+
users.api('users.show', { user: 1 }) // same thing
|
|
62
|
+
users.route('users.show', { user: 1 }) // build a URL
|
|
63
|
+
users.url('users.show', { user: 1 }) // semantic alias of route()
|
|
64
|
+
await users.onLevelLoaded() // wait until the level is loaded
|
|
65
|
+
users.useRoutePrefix('users') // returns a NEW BoundForge with the new prefix
|
|
66
|
+
|
|
67
|
+
// Bound level + prefix — route names joined automatically (ambiguity resolved smartly)
|
|
68
|
+
const userApi = useForge('admin', 'users')
|
|
69
|
+
userApi.prefix // → 'users'
|
|
70
|
+
userApi('show', { user: 1 }) // → forge.api('admin', 'users.show', ...)
|
|
71
|
+
userApi.route('show', { user: 1 }) // → forge.route('admin', 'users.show', ...)
|
|
72
|
+
|
|
73
|
+
// Generic methods: in bound form they act on the bound level (no arguments)
|
|
74
|
+
users.load() // load the bound level
|
|
75
|
+
users.isLoaded() // check the bound level's cache
|
|
76
|
+
users.invalidate() // invalidate the bound level's cache
|
|
77
|
+
// Global methods: isLoading() / onLoadingChange() / hasRoute(name) / getRoutes()
|
|
71
78
|
```
|
|
72
79
|
|
|
73
|
-
>
|
|
74
|
-
|
|
80
|
+
> **Note**: the unbound full instance may throw the guard error (`RF_FE_010`) from `route()` / `hasRoute()` before auto-discovery completes. Prefer `await forge.ready()`, or use `useForgeRoute` for links.
|
|
81
|
+
|
|
82
|
+
> **Level binding is static**: `level` (and `prefix`) are fixed at call time and cannot be switched later — create another `useForge` call for another level (the overhead is acceptable).
|
|
75
83
|
|
|
76
|
-
|
|
84
|
+
## useForgeApi — event-style calls with loading/error state
|
|
77
85
|
|
|
78
|
-
`
|
|
79
|
-
key 冲突时,`string|number` 值自动识别为路径参数,对象值按原定义处理。也可通过 `params`
|
|
80
|
-
显式指定路径参数(优先级最高):
|
|
86
|
+
For imperative scenarios like click handlers: it never throws — errors are written to the `error` state and returned as `{ data: undefined, error }`:
|
|
81
87
|
|
|
82
88
|
```ts
|
|
83
|
-
|
|
84
|
-
forge.api('search.show', { query: 'keyword' })
|
|
89
|
+
import { useForgeApi } from '@route-forge/vue'
|
|
85
90
|
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
// Three binding forms (same level semantics as useForge)
|
|
92
|
+
const api = useForgeApi() // unbound: call(level, name, params)
|
|
93
|
+
const admin = useForgeApi('admin') // bound: call(name, params)
|
|
94
|
+
const users = useForgeApi('admin', 'users') // bound + prefix: call(suffix, params)
|
|
95
|
+
|
|
96
|
+
const { data, error } = await admin.call('users.show', { user: 1 })
|
|
91
97
|
```
|
|
92
98
|
|
|
93
|
-
|
|
99
|
+
- `pending`: `Ref<boolean>` — reference-counted; concurrent `call`s keep it `true` until all settle
|
|
100
|
+
- `error`: `Ref<unknown>` — the latest failure (reset to `null` on success)
|
|
94
101
|
|
|
95
|
-
|
|
96
|
-
import {
|
|
97
|
-
useForgeApi,
|
|
98
|
-
useForgeRoute,
|
|
99
|
-
} from '@route-forge/vue'
|
|
100
|
-
|
|
101
|
-
// useForgeApi — 带 loading/error 状态的 API 调用(事件处理器场景)
|
|
102
|
-
// 不抛异常:错误作为 { data: undefined, error } 返回,同时写入 error 状态
|
|
103
|
-
// 三种调用形态(level 绑定与 useForge 一致):
|
|
104
|
-
const api = useForgeApi() // 未绑定:call(level, name, params)
|
|
105
|
-
const admin = useForgeApi('admin') // 绑定层级:call(name, params)
|
|
106
|
-
const users = useForgeApi('admin', 'users') // 绑定层级 + 前缀:call(suffix, params)
|
|
102
|
+
## useForgeRoute — reactive links in templates
|
|
107
103
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
Returns a `ComputedRef<string>` (auto-unwrapped in templates, no `.value` needed). It handles the loading state internally: returns `''` until the level loads (the render never crashes), then recomputes automatically when the level loads or dependencies change — you never need to look at `levelLoaded`:
|
|
105
|
+
|
|
106
|
+
```vue
|
|
107
|
+
<template>
|
|
108
|
+
<!-- 1. Static URL -->
|
|
109
|
+
<a :href="login">Login</a>
|
|
110
|
+
|
|
111
|
+
<!-- 2. Reactive params: URL recomputes when userId changes -->
|
|
112
|
+
<a :href="profile">User home</a>
|
|
113
|
+
|
|
114
|
+
<!-- 3. Reactive route name: recomputes when currentName changes -->
|
|
115
|
+
<NuxtLink :to="dynamic">Dynamic entry</NuxtLink>
|
|
116
|
+
</template>
|
|
111
117
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
118
|
+
<script setup>
|
|
119
|
+
import { ref } from 'vue'
|
|
120
|
+
import { useForgeRoute } from '@route-forge/vue'
|
|
121
|
+
|
|
122
|
+
const userId = ref(1)
|
|
123
|
+
const currentName = ref('dashboard.show')
|
|
124
|
+
|
|
125
|
+
// Static level + static route name
|
|
126
|
+
const login = useForgeRoute('public', 'login.show')
|
|
127
|
+
|
|
128
|
+
// params as a getter → tracked reactively; URL follows userId
|
|
116
129
|
const profile = useForgeRoute('admin', 'users.show', () => ({ user: userId.value }))
|
|
117
|
-
|
|
130
|
+
|
|
131
|
+
// name as a getter → the route name itself can switch reactively
|
|
132
|
+
const dynamic = useForgeRoute('admin', () => currentName.value)
|
|
133
|
+
</script>
|
|
118
134
|
```
|
|
119
135
|
|
|
120
|
-
|
|
136
|
+
Contract details:
|
|
137
|
+
|
|
138
|
+
- `level` is a **static string** binding: fixed once at setup and cannot be switched later — create another `useForgeRoute` call for another level (same contract as `useForge`); the `name` / `params` getters stay reactive. A non-string `level` throws `TypeError` at runtime
|
|
139
|
+
- Render-time errors (unknown route, missing required param…) **degrade to `''` so rendering never breaks**, while a styled `console.warn` prints the full error (with stack) — visible during development, harmless in production
|
|
140
|
+
|
|
141
|
+
## Components — ForgeRoute / ForgeLink
|
|
142
|
+
|
|
143
|
+
Both components wrap `useForgeRoute`, so templates don't repeat the "empty string first, update later" handling. While the level is not loaded (or route resolution fails), the `loading` slot renders (nothing by default); once loaded, the link renders.
|
|
144
|
+
|
|
145
|
+
**`ForgeLink`** — the convenient one: renders `<a :href>` directly with slot content as the link text:
|
|
121
146
|
|
|
122
147
|
```vue
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
148
|
+
<script setup>
|
|
149
|
+
import { ForgeLink } from '@route-forge/vue'
|
|
150
|
+
</script>
|
|
126
151
|
|
|
127
|
-
|
|
128
|
-
<a
|
|
152
|
+
<template>
|
|
153
|
+
<!-- Renders <a href="/login">Login</a> once the level is loaded -->
|
|
154
|
+
<ForgeLink level="public" name="login.show">Login</ForgeLink>
|
|
155
|
+
|
|
156
|
+
<!-- Reactive params (object or getter form) + attrs passthrough (class / target / …) -->
|
|
157
|
+
<ForgeLink level="admin" name="users.show" :params="{ user: userId }" class="btn">
|
|
158
|
+
View user
|
|
159
|
+
</ForgeLink>
|
|
160
|
+
|
|
161
|
+
<!-- Custom placeholder while loading -->
|
|
162
|
+
<ForgeLink level="admin" name="users.show" :params="() => ({ user: userId })">
|
|
163
|
+
View user
|
|
164
|
+
<template #loading>Preparing link…</template>
|
|
165
|
+
</ForgeLink>
|
|
129
166
|
</template>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Router integration is **zero-dependency and automatic**: if [vue-router](https://router.vuejs.org/) is installed (`app.use(router)`), `ForgeLink` renders `<RouterLink :to="href">` for in-app navigation; otherwise it renders a native `<a>`. Nothing to configure. (A `RouterLink` registered only locally in a parent component is not detected — the global registration from `app.use(router)` is what's probed.)
|
|
130
170
|
|
|
171
|
+
**`ForgeRoute`** — the flexible one: exposes `{ href, loaded }` through a scoped slot for full control:
|
|
172
|
+
|
|
173
|
+
```vue
|
|
131
174
|
<script setup>
|
|
132
|
-
import {
|
|
133
|
-
const url = useForgeRoute('public', 'login.show')
|
|
175
|
+
import { ForgeRoute } from '@route-forge/vue'
|
|
134
176
|
</script>
|
|
177
|
+
|
|
178
|
+
<template>
|
|
179
|
+
<ForgeRoute level="admin" name="users.show" :params="() => ({ user: userId })">
|
|
180
|
+
<template #default="{ href }">
|
|
181
|
+
<a :href="href">View user</a>
|
|
182
|
+
</template>
|
|
183
|
+
<template #loading>Preparing link…</template>
|
|
184
|
+
</ForgeRoute>
|
|
185
|
+
</template>
|
|
135
186
|
```
|
|
136
187
|
|
|
137
|
-
|
|
188
|
+
Shared contract (both components):
|
|
189
|
+
|
|
190
|
+
- `level` is a **static string** binding (same contract as `useForgeRoute`; a non-string throws `TypeError` at runtime); `name` / `params` accept both plain values and getter functions and stay reactive
|
|
191
|
+
- `loaded` = `href !== ''` — inside the `default` slot it is always `true` (the slot only renders once loaded); it exists for symmetry with the React render-prop API
|
|
192
|
+
- Console behavior: while the level is not loaded each instance `console.warn`s **once** (a normal transient state — no spam); route resolution failures log `console.error` every time (rendering still never breaks)
|
|
193
|
+
- SSR: the components simply render the `loading` slot (or nothing) until the level cache is populated — preload the level on the server, or let the link appear after client hydration
|
|
194
|
+
|
|
195
|
+
## About the `$forge` global property (not recommended in templates)
|
|
196
|
+
|
|
197
|
+
Installing the plugin injects `app.config.globalProperties.$forge` (with `route(level, name, params?)`). It is a **low-level fallback entry**: safe to call only after the target `level` has loaded (e.g. after `plugin.ready()` resolves). During rendering, if the level is not ready yet, `$forge.route()` throws on unready route data and breaks the render — uncontrollable. Prefer `useForgeRoute` for template links; keep `$forge` for the rare imperative scenarios where timing is guaranteed.
|
|
198
|
+
|
|
199
|
+
## Smart parameter resolution
|
|
200
|
+
|
|
201
|
+
`api()` supports the same smart resolution as core: flattened path parameters, with `query` / `body` / `headers` as fixed keys; when a path parameter name collides with a fixed key, `string|number` values are detected as path parameters, and the explicit `params` key always wins:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
// Conflict resolution: route /search/{query} — string `query` → path parameter
|
|
205
|
+
users.api('search.show', { query: 'keyword' })
|
|
206
|
+
|
|
207
|
+
// Explicit params: need BOTH a path param and a query string
|
|
208
|
+
users.api('search.show', {
|
|
209
|
+
params: { query: 'keyword' },
|
|
210
|
+
query: { page: 1 },
|
|
211
|
+
})
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Full rules and the `timeout` override: [core README](../core/README.md#smart-parameter-resolution).
|
|
215
|
+
|
|
216
|
+
## Type safety (optional but recommended)
|
|
217
|
+
|
|
218
|
+
Once `ForgeRouteMap` is defined (codegen or module augmentation), level, route names, and params are inferred in `useForge` / `useForgeApi` / `useForgeRoute`:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
npx route-forge-codegen --endpoint http://localhost/_forge/routes --out src/types/forge-routes.d.ts
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
// Typo'd route name / param name → compile error; correct call → autocompletion
|
|
226
|
+
const users = useForge('admin', 'users')
|
|
227
|
+
await users('show', { user: 1 }) // ✅ params checked at compile time
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
See the [core README "Type safety" section](../core/README.md#type-safety-optional-but-recommended).
|
|
231
|
+
|
|
232
|
+
## Differences vs core / react
|
|
233
|
+
|
|
234
|
+
| Capability | @route-forge/core | @route-forge/vue | @route-forge/react |
|
|
235
|
+
|------------|-------------------|------------------|--------------------|
|
|
236
|
+
| `levelLoaded` | `Promise<void>` | `Ref<boolean>` | `boolean` |
|
|
237
|
+
| `useForgeApi` `pending` / `error` | — (use `LoadingTracker`) | `Ref<boolean>` / `Ref<unknown>` | `boolean` / `unknown` |
|
|
238
|
+
| URL generation returns | `string` (sync; throws when unready) | `ComputedRef<string>` (`''` until ready) | `string` (`''` until ready) |
|
|
239
|
+
| `useForgeRoute` params | — | getter function | plain object (content-compared deps) |
|
|
240
|
+
| Binding signature | `forge.use(level, prefix?)` | `useForge(level?, prefix?)` | `useForge({ level?, prefix? })` |
|
|
241
|
+
|
|
242
|
+
## FAQ
|
|
243
|
+
|
|
244
|
+
**`$forge.route()` throws in a template?**
|
|
245
|
+
The level hasn't loaded yet. Use `useForgeRoute` instead (handles the loading state and degrades to `''`), or make sure the dependent components render only after `plugin.ready()` resolves.
|
|
246
|
+
|
|
247
|
+
**`useForge()` says "must be used inside an app with createRouteForgePlugin() installed"?**
|
|
248
|
+
A composable ran before the plugin was installed, or in a component tree without it; confirm `app.use(plugin)` happened first.
|
|
249
|
+
|
|
250
|
+
**Need to re-fetch the route table after navigating?**
|
|
251
|
+
Call `forge.invalidate()` (or `users.invalidate()` in bound form); the next `api()` / `load()` refetches.
|
|
252
|
+
|
|
253
|
+
## Documentation
|
|
138
254
|
|
|
139
|
-
-
|
|
140
|
-
-
|
|
141
|
-
-
|
|
255
|
+
- Repository: <https://github.com/route-forge/route-forge>
|
|
256
|
+
- Core package: [@route-forge/core](../core/README.md)
|
|
257
|
+
- Design notes: <https://github.com/route-forge/route-forge/blob/main/.docs/DESIGN.md>
|
|
258
|
+
- Specification: <https://github.com/route-forge/route-forge/blob/main/.docs/SPEC.md>
|
|
142
259
|
|
|
143
260
|
## License
|
|
144
261
|
|
package/README_zh.md
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# @route-forge/vue
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | **中文**
|
|
4
|
+
|
|
5
|
+
Route Forge 的 Vue 3 集成:插件(`createRouteForgePlugin`)+ 三个 composable(`useForge` / `useForgeApi` / `useForgeRoute`)+ 两个组件(`ForgeRoute` / `ForgeLink`),在组件里用**路由名**调用 API、生成响应式链接,加载态与错误自动管理。
|
|
6
|
+
|
|
7
|
+
> 核心能力(分级懒加载、隔离缓存、拦截器、请求取消、类型安全)全部来自 [@route-forge/core](../core/README_zh.md),本包只做 Vue 响应式适配:`levelLoaded` 是 `Ref<boolean>`、`pending` / `error` 是 `Ref`、URL 生成返回 `ComputedRef<string>`。
|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @route-forge/vue @route-forge/core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
要求 Vue 3.3+(不支持 Vue 2)。
|
|
16
|
+
|
|
17
|
+
## 快速开始
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// main.ts
|
|
21
|
+
import { createApp } from 'vue'
|
|
22
|
+
import { createRouteForgePlugin } from '@route-forge/vue'
|
|
23
|
+
import App from './App.vue'
|
|
24
|
+
|
|
25
|
+
const plugin = createRouteForgePlugin({
|
|
26
|
+
endpoint: '/_forge/routes',
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const app = createApp(App)
|
|
30
|
+
app.use(plugin)
|
|
31
|
+
// 推荐:ready()(摘要 + eager 层级全部完成)后再挂载应用,
|
|
32
|
+
// 挂载后 route()/hasRoute() 等同步方法即刻可用
|
|
33
|
+
plugin.ready()
|
|
34
|
+
.then(() => app.mount('#app'))
|
|
35
|
+
.catch((err) => {
|
|
36
|
+
// 失败必须接住:摘要端点不可达(网络错误/非 2xx/超时)时避免静默白屏
|
|
37
|
+
console.error('[route-forge] init failed', err)
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
> 插件选项与 `createRouteForge(options)` 完全一致(`endpoint` / `summary` / `levels` / `eager` / `adapter` / `cache` / `interceptors` / `timeout` / `baseURL`),完整选项表见 [core README](../core/README_zh.md#配置选项createruteforgeoptions)。`options` 本身可选——摘要由页面内嵌 `@forgeSummary`(`window.__ROUTE_FORGE__`)提供时,可直接 `createRouteForgePlugin()` 无参安装。
|
|
42
|
+
|
|
43
|
+
## useForge — 核心 composable
|
|
44
|
+
|
|
45
|
+
无 `level` 时返回完整 `RouteForge` 实例;传入 `level` 时内部调用 `forge.use(level, prefix?)`(自动触发层级加载),返回 `VueBoundForge`:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { useForge } from '@route-forge/vue'
|
|
49
|
+
|
|
50
|
+
// 不绑定层级 — 返回完整 RouteForge 实例
|
|
51
|
+
const forge = useForge()
|
|
52
|
+
forge.api('admin', 'users.show', { user: 1 }) // 通过层级 + 路由名调用
|
|
53
|
+
forge.ready().then(f => f.use('admin')) // 等待就绪后绑定层级
|
|
54
|
+
|
|
55
|
+
// 绑定层级 — 可直接调用,自动触发 load
|
|
56
|
+
const users = useForge('admin')
|
|
57
|
+
users.level // → 'admin'
|
|
58
|
+
users.levelLoaded // Ref<boolean>,加载完成后变为 true
|
|
59
|
+
users('users.show', { user: 1 }) // 可直接调用(= users.api() 快捷方式)
|
|
60
|
+
users.api('users.show', { user: 1 }) // 同上
|
|
61
|
+
users.route('users.show', { user: 1 }) // 生成 URL
|
|
62
|
+
users.url('users.show', { user: 1 }) // route() 语义别名
|
|
63
|
+
await users.onLevelLoaded() // 等待 level 加载完成
|
|
64
|
+
users.useRoutePrefix('users') // 以新前缀返回新的 BoundForge
|
|
65
|
+
|
|
66
|
+
// 绑定层级 + 前缀 — 路由名自动拼接(歧义时智能消解)
|
|
67
|
+
const userApi = useForge('admin', 'users')
|
|
68
|
+
userApi.prefix // → 'users'
|
|
69
|
+
userApi('show', { user: 1 }) // → forge.api('admin', 'users.show', ...)
|
|
70
|
+
userApi.route('show', { user: 1 }) // → forge.route('admin', 'users.show', ...)
|
|
71
|
+
|
|
72
|
+
// 通用方法:绑定形态下作用于绑定的层级(无参数)
|
|
73
|
+
users.load() // 加载绑定层级
|
|
74
|
+
users.isLoaded() // 检查绑定层级缓存
|
|
75
|
+
users.invalidate() // 失效绑定层级缓存
|
|
76
|
+
// 全局方法:isLoading() / onLoadingChange() / hasRoute(name) / getRoutes()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> **注意**:`useForge()` 无 `level` 时返回的完整实例在 auto-discovery 未完成前调用 `route()` / `hasRoute()` 可能抛守卫错误(`RF_FE_010`)。建议 `await forge.ready()`,或生成链接直接用 `useForgeRoute`。
|
|
80
|
+
|
|
81
|
+
> **level 是静态绑定**:`level`(与 `prefix`)在调用时固定,不支持中途切换层级——换层级请另起一次 `useForge` 调用(开销可接受)。
|
|
82
|
+
|
|
83
|
+
## useForgeApi — 带 loading/error 的事件型调用
|
|
84
|
+
|
|
85
|
+
面向点击事件等命令式场景:不抛异常,错误写入 `error` 状态并作为 `{ data: undefined, error }` 返回:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { useForgeApi } from '@route-forge/vue'
|
|
89
|
+
|
|
90
|
+
// 三种调用形态(level 绑定语义与 useForge 一致)
|
|
91
|
+
const api = useForgeApi() // 未绑定:call(level, name, params)
|
|
92
|
+
const admin = useForgeApi('admin') // 绑定层级:call(name, params)
|
|
93
|
+
const users = useForgeApi('admin', 'users') // 绑定层级 + 前缀:call(suffix, params)
|
|
94
|
+
|
|
95
|
+
const { data, error } = await admin.call('users.show', { user: 1 })
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- `pending`: `Ref<boolean>`——引用计数,并发多个 `call` 时全部完成才置 `false`
|
|
99
|
+
- `error`: `Ref<unknown>`——最近一次失败信息(成功时清为 `null`)
|
|
100
|
+
|
|
101
|
+
## useForgeRoute — 模板里的响应式链接
|
|
102
|
+
|
|
103
|
+
返回 `ComputedRef<string>`,模板自动解包无需 `.value`。内部处理加载态:`level` 未加载时返回 `''`(渲染不崩),加载完成或依赖变化后自动重算——你不必关心 `levelLoaded`:
|
|
104
|
+
|
|
105
|
+
```vue
|
|
106
|
+
<template>
|
|
107
|
+
<!-- 1. 静态 URL -->
|
|
108
|
+
<a :href="login">登录</a>
|
|
109
|
+
|
|
110
|
+
<!-- 2. 响应式参数:userId 变化时 URL 自动重算 -->
|
|
111
|
+
<a :href="profile">用户主页</a>
|
|
112
|
+
|
|
113
|
+
<!-- 3. 响应式路由名:currentName 变化时自动重算 -->
|
|
114
|
+
<NuxtLink :to="dynamic">动态入口</NuxtLink>
|
|
115
|
+
</template>
|
|
116
|
+
|
|
117
|
+
<script setup>
|
|
118
|
+
import { ref } from 'vue'
|
|
119
|
+
import { useForgeRoute } from '@route-forge/vue'
|
|
120
|
+
|
|
121
|
+
const userId = ref(1)
|
|
122
|
+
const currentName = ref('dashboard.show')
|
|
123
|
+
|
|
124
|
+
// 静态层级 + 静态路由名
|
|
125
|
+
const login = useForgeRoute('public', 'login.show')
|
|
126
|
+
|
|
127
|
+
// params 传 getter → 追踪响应式,userId 变了 URL 跟着变
|
|
128
|
+
const profile = useForgeRoute('admin', 'users.show', () => ({ user: userId.value }))
|
|
129
|
+
|
|
130
|
+
// name 传 getter → 路由名本身也可响应式切换
|
|
131
|
+
const dynamic = useForgeRoute('admin', () => currentName.value)
|
|
132
|
+
</script>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
契约细节:
|
|
136
|
+
|
|
137
|
+
- `level` 为**静态字符串**绑定:setup 时一次固定,不支持中途切换——需要另一个层级请新建一次 `useForgeRoute` 调用(与 `useForge` 契约一致);`name` / `params` 的 getter 保持响应式。传非 string 的 `level` 运行时直接抛 `TypeError`
|
|
138
|
+
- 路由名不存在、必填参数缺失等渲染期错误:**降级为 `''` 保证渲染不中断**,同时以样式化 `console.warn` 输出完整错误(含堆栈),开发期一眼可见、生产无副作用
|
|
139
|
+
|
|
140
|
+
## 组件 — ForgeRoute / ForgeLink
|
|
141
|
+
|
|
142
|
+
两个组件封装了 `useForgeRoute`,省去模板里重复的"先空串、后更新"处理。`level` 未加载(或路由解析失败)时渲染 `loading` 插槽(默认什么都不渲染),加载完成后渲染链接。
|
|
143
|
+
|
|
144
|
+
**`ForgeLink`** —— 便捷形态:加载完成后直接渲染 `<a :href>`,默认插槽内容即链接文本:
|
|
145
|
+
|
|
146
|
+
```vue
|
|
147
|
+
<script setup>
|
|
148
|
+
import { ForgeLink } from '@route-forge/vue'
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<template>
|
|
152
|
+
<!-- 层级加载完成后渲染 <a href="/login">登录</a> -->
|
|
153
|
+
<ForgeLink level="public" name="login.show">登录</ForgeLink>
|
|
154
|
+
|
|
155
|
+
<!-- 响应式参数(对象或 getter 形式)+ attrs 透传(class / target / …) -->
|
|
156
|
+
<ForgeLink level="admin" name="users.show" :params="{ user: userId }" class="btn">
|
|
157
|
+
查看用户
|
|
158
|
+
</ForgeLink>
|
|
159
|
+
|
|
160
|
+
<!-- 加载中的自定义占位 -->
|
|
161
|
+
<ForgeLink level="admin" name="users.show" :params="() => ({ user: userId })">
|
|
162
|
+
查看用户
|
|
163
|
+
<template #loading>正在准备链接…</template>
|
|
164
|
+
</ForgeLink>
|
|
165
|
+
</template>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
路由库集成**零依赖、全自动**:安装了 [vue-router](https://router.vuejs.org/)(`app.use(router)`)时,`ForgeLink` 自动渲染 `<RouterLink :to="href">` 做 SPA 内部跳转;否则渲染原生 `<a>`。无需任何配置。(仅在父组件局部注册的 `RouterLink` 探测不到——探测的是 `app.use(router)` 产生的全局注册。)
|
|
169
|
+
|
|
170
|
+
**`ForgeRoute`** —— 灵活形态:通过作用域插槽暴露 `{ href, loaded }`,完全自主控制渲染:
|
|
171
|
+
|
|
172
|
+
```vue
|
|
173
|
+
<script setup>
|
|
174
|
+
import { ForgeRoute } from '@route-forge/vue'
|
|
175
|
+
</script>
|
|
176
|
+
|
|
177
|
+
<template>
|
|
178
|
+
<ForgeRoute level="admin" name="users.show" :params="() => ({ user: userId })">
|
|
179
|
+
<template #default="{ href }">
|
|
180
|
+
<a :href="href">查看用户</a>
|
|
181
|
+
</template>
|
|
182
|
+
<template #loading>正在准备链接…</template>
|
|
183
|
+
</ForgeRoute>
|
|
184
|
+
</template>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
两组件共享的契约:
|
|
188
|
+
|
|
189
|
+
- `level` 为**静态字符串**绑定(与 `useForgeRoute` 契约一致,传非 string 运行时抛 `TypeError`);`name` / `params` 支持值与 getter 函数双形态,均保持响应式
|
|
190
|
+
- `loaded` = `href !== ''`——`default` 插槽内恒为 `true`(该插槽只在加载完成后渲染),存在是为与 React render-prop API 对称
|
|
191
|
+
- 控制台行为:`level` 未加载时每实例 `console.warn` **一次**(正常瞬态,不刷屏);路由解析失败每次都 `console.error`(渲染仍不中断)
|
|
192
|
+
- SSR:`level` 缓存就绪前组件只渲染 `loading` 插槽(或不渲染)——可在服务端预加载层级,或让链接在客户端 hydration 后自然出现
|
|
193
|
+
|
|
194
|
+
## 关于 `$forge` 全局属性(不推荐在模板中用)
|
|
195
|
+
|
|
196
|
+
插件安装后会注入 `app.config.globalProperties.$forge`(含 `route(level, name, params?)`)。它是**底层兜底入口**:只有在对应 `level` 已加载完成后才可安全调用(例如 `plugin.ready()` resolve 之后)。渲染期若层级尚未加载,`$forge.route()` 会因路由数据未就绪直接抛错、打断渲染——不可控。模板里生成链接请优先用 `useForgeRoute`,`$forge` 保留给少数已确保时序安全的命令式场景。
|
|
197
|
+
|
|
198
|
+
## 参数智能解析
|
|
199
|
+
|
|
200
|
+
`api()` 的参数支持与 core 一致的智能消解:路径参数平铺传入,`query` / `body` / `headers` 为固定 key;路径参数名与固定 key 冲突时,`string|number` 值自动识别为路径参数,也可用 `params` 显式指定(优先级最高):
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
// 冲突消解:路由 /search/{query} —— query 为 string → 路径参数
|
|
204
|
+
users.api('search.show', { query: 'keyword' })
|
|
205
|
+
|
|
206
|
+
// 显式 params:同时需要路径参数和 query string
|
|
207
|
+
users.api('search.show', {
|
|
208
|
+
params: { query: 'keyword' },
|
|
209
|
+
query: { page: 1 },
|
|
210
|
+
})
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
完整规则与 `timeout` 覆盖见 [core README](../core/README_zh.md#参数智能解析)。
|
|
214
|
+
|
|
215
|
+
## 类型安全(可选但推荐)
|
|
216
|
+
|
|
217
|
+
定义 `ForgeRouteMap`(codegen 或模块增强)后,`useForge` / `useForgeApi` / `useForgeRoute` 的 level、路由名、params 全部自动推断:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
npx route-forge-codegen --endpoint http://localhost/_forge/routes --out src/types/forge-routes.d.ts
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
// 拼错路由名 / 参数名 → 编译期报错;正确调用 → 自动补全
|
|
225
|
+
const users = useForge('admin', 'users')
|
|
226
|
+
await users('show', { user: 1 }) // ✅ params 类型自动校验
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
详见 [core README「类型安全」](../core/README_zh.md#类型安全可选但推荐)。
|
|
230
|
+
|
|
231
|
+
## 与 core / react 的差异
|
|
232
|
+
|
|
233
|
+
| 能力 | @route-forge/core | @route-forge/vue | @route-forge/react |
|
|
234
|
+
|------|-------------------|------------------|--------------------|
|
|
235
|
+
| `levelLoaded` | `Promise<void>` | `Ref<boolean>` | `boolean` |
|
|
236
|
+
| `useForgeApi` 的 `pending` / `error` | —(用 `LoadingTracker`) | `Ref<boolean>` / `Ref<unknown>` | `boolean` / `unknown` |
|
|
237
|
+
| URL 生成返回值 | `string`(同步,未就绪抛错) | `ComputedRef<string>`(未就绪为 `''`) | `string`(未就绪为 `''`) |
|
|
238
|
+
| `useForgeRoute` 的 params | — | getter 函数 | 普通对象(按内容对比依赖) |
|
|
239
|
+
| 绑定签名 | `forge.use(level, prefix?)` | `useForge(level?, prefix?)` | `useForge({ level?, prefix? })` |
|
|
240
|
+
|
|
241
|
+
## 常见问题
|
|
242
|
+
|
|
243
|
+
**模板里 `$forge.route()` 抛错?**
|
|
244
|
+
层级尚未加载。改用 `useForgeRoute`(自动处理加载态并降级为 `''`),或确保在 `plugin.ready()` resolve 后再渲染依赖路由的组件。
|
|
245
|
+
|
|
246
|
+
**`useForge()` 报 "must be used inside an app with createRouteForgePlugin() installed"?**
|
|
247
|
+
composable 在插件安装前或插件未安装的组件树里被调用了;确认 `app.use(plugin)` 已执行。
|
|
248
|
+
|
|
249
|
+
**切换页面后想重新拉取路由表?**
|
|
250
|
+
`forge.invalidate()`(或绑定形态 `users.invalidate()`)后,下次 `api()` / `load()` 会重新拉取。
|
|
251
|
+
|
|
252
|
+
## 文档
|
|
253
|
+
|
|
254
|
+
- 仓库主页: <https://github.com/route-forge/route-forge>
|
|
255
|
+
- 核心包文档: [@route-forge/core](../core/README_zh.md)
|
|
256
|
+
- 设计文档: <https://github.com/route-forge/route-forge/blob/main/.docs/DESIGN.md>
|
|
257
|
+
- 规范: <https://github.com/route-forge/route-forge/blob/main/.docs/SPEC.md>
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
MIT
|