@route-forge/vue 2.1.0 → 2.2.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,15 +1,20 @@
1
1
  # @route-forge/vue
2
2
 
3
- Route Forge 的 Vue 3 集成:插件 + composable(`useForge` / `useForgeApi` /
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
- // 推荐:ready()(摘要 + eager 层级全部完成)后再挂载应用
27
- // (onSummaryReady 回调已移除,统一走 ready——完整成功/失败语义链)
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
- // 失败兜底:摘要端点不可达(网络错误/非 2xx/超时)时接住 reject,避免静默白屏
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
- ## useForge 核心 composable
42
+ > Plugin options are exactly `createRouteForge(options)` (`levels` / `eager` / `adapter` / `cache` / `interceptors` / `timeout` / `baseURL`); full options table in the [core README](../core/README.md#options-createrouteforgeoptions).
37
43
 
38
- `useForge()` 返回 forge 实例。无 level 时返回完整 `RouteForge` 实例;传入 `level` 时内部调用 `forge.use(level, prefix?)`,自动触发 load:
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
- // 不绑定层级返回完整 RouteForge 实例
51
+ // Unboundfull RouteForge instance
44
52
  const forge = useForge()
45
- forge.api('admin', 'users.show', { user: 1 }) // 通过层级 + 路由名调用
46
- forge.ready().then(f => f.use('admin')) // 等待就绪后绑定层级
47
- forge.use('admin') // 绑定层级,返回 BoundForge
48
-
49
- // 绑定层级 自动触发 load,提供同步方法 + levelLoaded 状态
50
- const forge = useForge('admin')
51
- forge.level // 'admin'
52
- forge.levelLoaded // Ref<boolean>,加载完成后变为 true
53
- forge('users.show', { user: 1 }) // 可直接调用(= forge.api() 快捷方式)
54
- forge.api('users.show', { user: 1 }) // 同上
55
- forge.route('users.show', { user: 1 }) // 生成 URL
56
- forge.url('users.show', { user: 1 }) // route() 语义别名
57
- forge.onLevelLoaded() // 等待 level 加载完成
58
- forge.useRoutePrefix('users') // 追加路由名前缀
59
-
60
- // 绑定层级 + 前缀 — 路由名自动拼接
61
- const forge = useForge('admin', 'users')
62
- forge.level // → 'admin'
63
- forge.prefix // → 'users'
64
- forge('show', { user: 1 }) // → forge.api('admin', 'users.show', ...)
65
- forge.route('show', { user: 1 }) // forge.route('admin', 'users.show', ...)
66
-
67
- // 通用方法(无论是否绑定 level 均可用)
68
- forge.load('admin') // 加载层级
69
- forge.isLoaded('admin') // 检查缓存
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
- > **注意**:`useForge()` level 时返回完整 `RouteForge` 实例,包含所有方法(`route`/`url`/`hasRoute`/`getRoutes` 等)。
74
- > 但 auto-discovery 可能未完成,同步方法可能抛守卫错误。建议使用 `await forge.ready()` 或 `useForgeRoute`。
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
- `api()` 的参数支持智能消解:路径参数平铺传入,`query`/`body`/`headers` 为固定 key。当路径参数名与固定
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
- // 冲突消解:query string 路径参数
84
- forge.api('search.show', { query: 'keyword' })
89
+ import { useForgeApi } from '@route-forge/vue'
85
90
 
86
- // 显式 params:同时需要路径参数和 query string
87
- forge.api('search.show', {
88
- params: { query: 'keyword' },
89
- query: { page: 1 },
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
- ## 其他 composable
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
- ```ts
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
- const { data, error } = await admin.call('users.show', { user: 1 })
109
- // pending: Ref<boolean>,并发多个 call 时全部完成才置 false(引用计数)
110
- // error: Ref<unknown>,最近一次失败信息
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
- // useForgeRoute — 响应式 URL 生成器(模板层场景)
113
- // ComputedRef<string>,内部处理 level 加载状态:
114
- // level 未加载时返回 '',加载后自动更新;参数(getter)变化自动重算
115
- const url = useForgeRoute('public', 'login.show')
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
- <template>
124
- <!-- useForgeRoute 内部处理加载状态,可直接在模板中使用 -->
125
- <a :href="url">登录</a>
148
+ <script setup>
149
+ import { ForgeLink } from '@route-forge/vue'
150
+ </script>
126
151
 
127
- <!-- 或通过 $forge 全局属性(需确保 level 已加载) -->
128
- <a :href="$forge.route('public', 'login.show')">登录</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 { useForgeRoute } from '@route-forge/vue'
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
- - 仓库主页: https://github.com/route-forge/route-forge
140
- - 设计文档: https://github.com/route-forge/route-forge/blob/main/.docs/DESIGN.md
141
- - 规范: https://github.com/route-forge/route-forge/blob/main/.docs/SPEC.md
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)` 完全一致(`levels` / `eager` / `adapter` / `cache` / `interceptors` / `timeout` / `baseURL`),完整选项表见 [core README](../core/README_zh.md#配置选项createruteforgeoptions)。
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