@finesoft/front 0.5.1 → 0.5.2
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 +4 -4
- package/dist/Outlet.svelte +39 -0
- package/dist/Outlet.svelte.d.ts +7 -0
- package/dist/browser-DIU6Sxl3.mjs +1237 -0
- package/dist/browser-kFMjlLGT.d.mts +262 -0
- package/dist/browser.d.mts +7 -2
- package/dist/browser.mjs +9 -1
- package/dist/controller-types-CgmJ6-le.d.mts +16 -0
- package/dist/cookies-Bpf9VayB.d.mts +779 -0
- package/dist/fetch-policy-BHT8RtrL.mjs +82 -0
- package/dist/host-guard-DDWxLpFL.mjs +222 -0
- package/dist/http-B6CJqDyf.d.mts +46 -0
- package/dist/http-CaxrMD1A.d.mts +1 -0
- package/dist/http-D70PL72H.mjs +257 -0
- package/dist/http.d.mts +3 -0
- package/dist/http.mjs +2 -0
- package/dist/index-node.d.mts +15 -0
- package/dist/index-node.mjs +15 -0
- package/dist/index.d.mts +48 -697
- package/dist/index.mjs +44 -261
- package/dist/load-node.d.mts +5 -0
- package/dist/load-node.mjs +10 -0
- package/dist/load-portable.d.mts +5 -0
- package/dist/load-portable.mjs +8 -0
- package/dist/lru-map-BKoUAySU.mjs +50 -0
- package/dist/messages-CAt2QdGr.mjs +140 -0
- package/dist/native-contract-DuR25hYB.d.mts +14 -0
- package/dist/native-contract.d.mts +2 -0
- package/dist/native-contract.mjs +1 -0
- package/dist/node-D9hB4dsz.d.mts +35 -0
- package/dist/node.d.mts +2 -0
- package/dist/node.mjs +59 -0
- package/dist/path-CGFl2w7D.mjs +113 -0
- package/dist/path-CXT6xGPO.d.mts +261 -0
- package/dist/portable-CaxrMD1A.d.mts +1 -0
- package/dist/portable.d.mts +11 -0
- package/dist/portable.mjs +12 -0
- package/dist/proxy-1SphZ7x7.mjs +436 -0
- package/dist/proxy-2dSWO-Xw.d.mts +53 -0
- package/dist/public-types-BcJM-AYc.mjs +835 -0
- package/dist/react-DhwBRw01.d.mts +16 -0
- package/dist/react.d.mts +3 -0
- package/dist/react.mjs +29 -0
- package/dist/rolldown-runtime-B4iAMlE-.mjs +35 -0
- package/dist/secure-fetch-Xlht2jd7.d.mts +30 -0
- package/dist/server-controller-proxy-BkVuVWVD.d.mts +38 -0
- package/dist/session-DnB4ZC3x.d.mts +1279 -0
- package/dist/src-Ftl_0rhu.mjs +28 -0
- package/dist/src-qwx7Vw8g.mjs +3807 -0
- package/dist/ssr-BEUNDvbj.d.mts +210 -0
- package/dist/ssr-C8xnYXoY.mjs +357 -0
- package/dist/ssr.d.mts +3 -0
- package/dist/ssr.mjs +3 -0
- package/dist/svelte-Dr5to3SE.d.mts +16 -0
- package/dist/svelte.d.mts +3 -0
- package/dist/svelte.mjs +13 -0
- package/dist/typegen-C-WeJCtf.d.mts +12 -0
- package/dist/typegen-cli.d.mts +1 -0
- package/dist/typegen-cli.mjs +11 -0
- package/dist/typegen.d.mts +3 -0
- package/dist/typegen.mjs +2 -0
- package/dist/types-BuaZHRG7.mjs +402 -0
- package/dist/undici-CPfL25Hr.mjs +22262 -0
- package/dist/vite-Cj4SPA8D.d.mts +277 -0
- package/dist/vite.d.mts +4 -0
- package/dist/vite.mjs +2354 -0
- package/dist/vue-DGmzuKho.d.mts +32 -0
- package/dist/vue.d.mts +3 -0
- package/dist/vue.mjs +56 -0
- package/dist/web.d.mts +6 -0
- package/dist/web.mjs +8 -0
- package/dist/worker.d.mts +2 -0
- package/dist/worker.mjs +2 -0
- package/docs/01-getting-started.md +67 -199
- package/docs/02-routing-and-controllers.md +163 -241
- package/docs/03-middleware.md +10 -212
- package/docs/04-rendering-and-hydration.md +6 -333
- package/docs/05-i18n.md +7 -237
- package/docs/06-http-client.md +20 -263
- package/docs/07-di-container.md +23 -257
- package/docs/08-observability.md +6 -286
- package/docs/09-server-and-deployment.md +59 -219
- package/docs/10-features-platform-pwa.md +7 -231
- package/docs/11-navigation.md +143 -288
- package/docs/12-session-restoration.md +6 -214
- package/docs/README.md +7 -7
- package/docs/advanced/custom-action-handler.md +29 -229
- package/docs/advanced/custom-adapter.md +7 -259
- package/docs/advanced/custom-event-recorder.md +7 -312
- package/docs/advanced/inline-proxy-codegen.md +6 -185
- package/docs/advanced/multi-tenant-scopes.md +10 -323
- package/docs/engineering/ci-release-flow.md +35 -222
- package/docs/engineering/project-structure.md +34 -277
- package/docs/engineering/testing.md +8 -310
- package/docs/pitfalls/container-scope-leak.md +2 -214
- package/docs/pitfalls/i18n-bundle-size.md +6 -176
- package/docs/pitfalls/proxy-binary-payloads.md +8 -12
- package/docs/pitfalls/ssr-hydration-mismatch.md +4 -160
- package/docs/pitfalls/ssr-vs-csr-globals.md +9 -170
- package/docs/zh/01-getting-started.md +67 -199
- package/docs/zh/02-routing-and-controllers.md +166 -244
- package/docs/zh/03-middleware.md +10 -212
- package/docs/zh/04-rendering-and-hydration.md +6 -333
- package/docs/zh/05-i18n.md +7 -237
- package/docs/zh/06-http-client.md +20 -263
- package/docs/zh/07-di-container.md +23 -257
- package/docs/zh/08-observability.md +6 -283
- package/docs/zh/09-server-and-deployment.md +59 -219
- package/docs/zh/10-features-platform-pwa.md +7 -231
- package/docs/zh/11-navigation.md +130 -290
- package/docs/zh/12-session-restoration.md +6 -214
- package/docs/zh/README.md +4 -4
- package/docs/zh/advanced/custom-action-handler.md +29 -229
- package/docs/zh/advanced/custom-adapter.md +7 -259
- package/docs/zh/advanced/custom-event-recorder.md +7 -312
- package/docs/zh/advanced/inline-proxy-codegen.md +6 -185
- package/docs/zh/advanced/multi-tenant-scopes.md +10 -323
- package/docs/zh/engineering/ci-release-flow.md +35 -222
- package/docs/zh/engineering/project-structure.md +34 -277
- package/docs/zh/engineering/testing.md +8 -310
- package/docs/zh/pitfalls/container-scope-leak.md +2 -214
- package/docs/zh/pitfalls/i18n-bundle-size.md +6 -176
- package/docs/zh/pitfalls/proxy-binary-payloads.md +8 -12
- package/docs/zh/pitfalls/ssr-hydration-mismatch.md +4 -160
- package/docs/zh/pitfalls/ssr-vs-csr-globals.md +9 -170
- package/package.json +118 -20
- package/dist/browser-BHhVWXik.mjs +0 -2
- package/dist/browser-BV2BBXm7.d.mts +0 -2811
package/docs/zh/11-navigation.md
CHANGED
|
@@ -1,355 +1,195 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 结构化导航
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Tabs、Stack、Split 是不可变导航声明。页面引用生成操作目标;分支、列名称仍是明确的布局标识。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 路由定义驱动参数类型
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## 心智模型
|
|
10
|
-
|
|
11
|
-
导航状态是一棵由四种节点构成的树:
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
NavigationNode = LeafNode | StackNode | TabsNode | SplitNode
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
| 节点 | 持有 | 语义 | SwiftUI |
|
|
18
|
-
| ----------- | ------------------------------- | ---------------------------------------------------------------------- | --------------------- |
|
|
19
|
-
| `LeafNode` | `intent` + `params` | 一个目标(一次 intent 派发) | 一个 destination view |
|
|
20
|
-
| `StackNode` | 有序 `entries[]` | 一条路径:`entries[0]` 是根,末尾是可见的栈顶 | `NavigationStack` |
|
|
21
|
-
| `TabsNode` | `active` 键 + `branches` | 并列分支;**仅激活分支可见** | `TabView` |
|
|
22
|
-
| `SplitNode` | `columns[]` + 可选 `visibility` | 多列并存;可见集**默认全部列**,可收窄为 `detailOnly` / `doubleColumn` | `NavigationSplitView` |
|
|
23
|
-
|
|
24
|
-
叶子持有 `intent` + `params`,**不是** `Page`。树是纯粹的、可序列化的数据,描述「要去哪」;「那里是什么」(`Page`)由 controller 在解析时产出,并随快照交回。这正是树能进 URL / history 的原因。
|
|
25
|
-
|
|
26
|
-
内部节点递归嵌套 —— 一个由 `NavigationStack` 组成的 `TabView`、detail 列是 stack 的 split,等等。
|
|
27
|
-
|
|
28
|
-
## 声明一棵树
|
|
29
|
-
|
|
30
|
-
构造器与其它一切一样从 `@finesoft/front` 导入:
|
|
7
|
+
路径参数和 query 都在路由中声明类型,并分别读取为 `params.id`、`query.tab`。既有 `handler` 仍接收路径参数和执行上下文,第三个参数接收 query:
|
|
31
8
|
|
|
32
9
|
```ts
|
|
33
|
-
import {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
search: stack(leaf("search")),
|
|
49
|
-
me: stack(leaf("me")),
|
|
10
|
+
import { definePage, defineWebApp, int, optional, oneOf } from "@finesoft/front";
|
|
11
|
+
|
|
12
|
+
export const product = definePage({
|
|
13
|
+
id: "product",
|
|
14
|
+
routes: [
|
|
15
|
+
{
|
|
16
|
+
path: "/products/:id",
|
|
17
|
+
params: { id: int() },
|
|
18
|
+
query: { tab: optional(oneOf(["details", "reviews"])) },
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
handler(params, context, query) {
|
|
22
|
+
context.signal.throwIfAborted();
|
|
23
|
+
// params.id: number;query.tab?: "details" | "reviews"
|
|
24
|
+
return { id: params.id.toFixed(), pageType: "product" as const, title: "Product" };
|
|
50
25
|
},
|
|
51
26
|
});
|
|
27
|
+
export const definition = defineWebApp({
|
|
28
|
+
id: "shop",
|
|
29
|
+
pages: [product],
|
|
30
|
+
getErrorPage: (status, title) => ({ id: String(status), pageType: "error", title }),
|
|
31
|
+
});
|
|
52
32
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
{ id: "sidebar", content: leaf("folders") },
|
|
56
|
-
{ id: "detail", content: stack(leaf("folder", { id: "inbox" })) },
|
|
57
|
-
]);
|
|
33
|
+
product.leaf({ id: 42 });
|
|
34
|
+
// product.leaf({ id: "42" }); // 编译错误
|
|
58
35
|
```
|
|
59
36
|
|
|
60
|
-
`
|
|
37
|
+
需要错误恢复时使用独立的 `BaseController` 子类;`execute` 可同步或异步返回,`fallback` 可省略,取消执行不会进入 `fallback`。已有 `perform` 工厂继续支持。`create` 不接受只有 `execute/fallback` 的对象。
|
|
61
38
|
|
|
62
|
-
|
|
39
|
+
### 独立 Controller 类
|
|
63
40
|
|
|
64
|
-
|
|
41
|
+
路由与类可以分文件。只在路由中维护输入类型,类统一使用 `execute({ params, query, context })` / `fallback({ params, query, context, error })`:
|
|
65
42
|
|
|
66
43
|
```ts
|
|
67
|
-
//
|
|
68
|
-
import {
|
|
69
|
-
import {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
defineRoutes(framework, [
|
|
76
|
-
{ path: "/", intentId: "home", controller: new HomeController() },
|
|
77
|
-
{ path: "/search", intentId: "search", controller: new SearchController() },
|
|
78
|
-
{ path: "/me", intentId: "me", controller: new ProfileController() },
|
|
79
|
-
{ path: "/posts/:id", intentId: "post", controller: new PostController() },
|
|
80
|
-
]);
|
|
44
|
+
// controllers/product.ts — 编写时不用重复参数类型
|
|
45
|
+
import { BaseController } from "@finesoft/front";
|
|
46
|
+
import type { ProductPage } from "../models/product";
|
|
47
|
+
|
|
48
|
+
export class ProductController extends BaseController {
|
|
49
|
+
execute({ params, query, context }): ProductPage {
|
|
50
|
+
return { id: params.id.toFixed(), pageType: "product", title: "Product" };
|
|
51
|
+
}
|
|
81
52
|
}
|
|
82
|
-
|
|
83
|
-
// 导航结构,只声明一次
|
|
84
|
-
export const navigation = defineNavigation({
|
|
85
|
-
initial: tabs({
|
|
86
|
-
active: "home",
|
|
87
|
-
branches: {
|
|
88
|
-
home: stack(leaf("home")),
|
|
89
|
-
search: stack(leaf("search")),
|
|
90
|
-
me: stack(leaf("me")),
|
|
91
|
-
},
|
|
92
|
-
}),
|
|
93
|
-
});
|
|
94
53
|
```
|
|
95
54
|
|
|
96
|
-
`defineNavigation` 返回一个规范化的定义,附带两个适配器 —— `toBrowserConfig()` 给 CSR、`toSSRDefinition()` 给 SSR —— 因此你只声明**一次**树,就能把各自需要的形态交给对应 runner。
|
|
97
|
-
|
|
98
|
-
### 接入浏览器
|
|
99
|
-
|
|
100
|
-
`startBrowserApp` 新增一个可选 `navigation` 字段;存在时,`NavigationHandle`(以及统一的 `app` 句柄)会在 `mount` 回调的 context 里交给你,挂载时即可直接使用:
|
|
101
|
-
|
|
102
55
|
```ts
|
|
103
|
-
//
|
|
104
|
-
import {
|
|
105
|
-
import {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
mount(target, { navigation: nav, app }) {
|
|
112
|
-
// nav/app 在 mount 时已就绪,无需等待回调。
|
|
113
|
-
// 快照变更时重渲染:
|
|
114
|
-
nav?.subscribe((snapshot) => mountNavigation(snapshot));
|
|
115
|
-
if (nav) mountNavigation(nav.getSnapshot());
|
|
116
|
-
// ... 把 UI 挂载到 target,将 app 传给组件 ...
|
|
117
|
-
return () => undefined;
|
|
118
|
-
},
|
|
56
|
+
// app-definition.ts
|
|
57
|
+
import { definePage, int } from "@finesoft/front";
|
|
58
|
+
import { ProductController } from "./controllers/product";
|
|
59
|
+
|
|
60
|
+
export const product = definePage({
|
|
61
|
+
id: "product",
|
|
62
|
+
routes: [{ path: "/products/:id", params: { id: int() } }],
|
|
63
|
+
create: () => new ProductController(),
|
|
119
64
|
});
|
|
120
65
|
```
|
|
121
66
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
## 驱动导航
|
|
67
|
+
框架根据 `create` 返回的类定位 `BaseController` 子类,只维护 `import type`、参数注解与基类泛型。`context` 保留 DI、取消信号等执行服务;query 不混入 params。类型声明集中在 `.finesoft/controller-types.d.ts`,不会在 Controller 文件末尾追加声明区块。保存的类源码会出现这些引用,业务无需手写或同步。手写参数类型保留;移除参数注解与基类输入泛型即可交给生成器管理。建议保留方法的返回页面类型。
|
|
125
68
|
|
|
126
|
-
|
|
69
|
+
生成后使用短类型名,基类直接接收完整输入类型:
|
|
127
70
|
|
|
128
71
|
```ts
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
72
|
+
import type {
|
|
73
|
+
ProductControllerInput as Input,
|
|
74
|
+
ProductControllerFailure as Failure,
|
|
75
|
+
} from "../../../.finesoft/controller-types";
|
|
76
|
+
|
|
77
|
+
export class ProductController extends BaseController<Input, ProductPage> {
|
|
78
|
+
async execute({ params, query, context }: Input): Promise<ProductPage> {
|
|
79
|
+
/* 业务逻辑 */
|
|
80
|
+
}
|
|
81
|
+
fallback({ params, error }: Failure): ProductPage {
|
|
82
|
+
/* 回退逻辑 */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
142
85
|
```
|
|
143
86
|
|
|
144
|
-
`
|
|
145
|
-
|
|
146
|
-
### 读取结果
|
|
87
|
+
`Input` 已包含 `params`、`query` 和 `context`,无需在基类中再次拆开;`Failure` 额外包含 `error`。同文件有多个控制器或名称冲突时使用带类名的别名。基类统一使用 `BaseController<Input, Result>`,手写输入可使用 `ControllerInput<Params, Query>`。直接调用 `perform` 的参数与结果保持类型检查。
|
|
147
88
|
|
|
148
|
-
`
|
|
89
|
+
旧的多个位置参数写法需要先迁移为单对象参数。生成器只更新类型引用,不改写方法业务逻辑;遇到旧的多参数 `execute` / `fallback` 签名时会报告迁移提示。
|
|
149
90
|
|
|
150
|
-
|
|
151
|
-
const snapshot = handle.getSnapshot();
|
|
152
|
-
snapshot.tree; // 当前 NavigationNode 树
|
|
153
|
-
snapshot.destinations; // ResolvedDestination[]:{ intent, params, page, status? }
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
`destinations` 的顺序与 `collectVisibleDestinations(tree)` 一致:tabs 节点**只**贡献激活分支,split **每个**非空列都贡献。这个顺序也正是服务端预取的内容。
|
|
91
|
+
这使用标准 TypeScript 声明,因此编辑器补全与命令行检查一致。它不会执行路由模块、实例化 Controller,也不会增加运行时代码。同一类用于多个路由时,params/query 分别保留对应路由的输入类型,需要正常收窄。
|
|
157
92
|
|
|
158
|
-
|
|
93
|
+
取消路由注册但保留类时,框架保留该类最后一次生成的输入契约,避免破坏它的独立使用;重新注册后按新路由更新。引用式 `tsconfig.json` 会自动选择包含 `src` 的应用项目;有多个候选项目时,通过 `controllerTypes.tsconfig` 指定应用配置。
|
|
159
94
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
split 视图同时展示多列 —— 经典的 sidebar + detail(+ sub-detail)布局。一列的选择驱动下一列。
|
|
95
|
+
模板已配置自动生成。现有项目安装开发依赖 `typescript`,并把框架插件的创建放在 `lazyPlugins` 外,确保 `vp check` 读取配置时也会生成类型:
|
|
163
96
|
|
|
164
97
|
```ts
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
]),
|
|
98
|
+
const front = finesoftFrontViteConfig({
|
|
99
|
+
controllerTypes: { root: import.meta.dirname },
|
|
100
|
+
});
|
|
101
|
+
export default defineConfig({
|
|
102
|
+
lint: { options: { typeAware: true, typeCheck: true } },
|
|
103
|
+
plugins: lazyPlugins(() => [front, react()]),
|
|
171
104
|
});
|
|
172
105
|
```
|
|
173
106
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
```ts
|
|
177
|
-
// 选一个邮箱 → 填充 "list" 列
|
|
178
|
-
await handle.selectColumn("list", "messages", { mailbox: "inbox" });
|
|
179
|
-
|
|
180
|
-
// 选一封邮件 → 填充 "detail" 列
|
|
181
|
-
await handle.selectColumn("detail", "message", { id: 1024 });
|
|
182
|
-
|
|
183
|
-
// 重选邮箱 → 清空 "list" 与 "detail"(它之后的所有列)
|
|
184
|
-
await handle.selectColumn("list", "messages", { mailbox: "archive" });
|
|
185
|
-
|
|
186
|
-
// 给 intent 传 undefined 显式清空某列
|
|
187
|
-
await handle.selectColumn("detail", undefined);
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
设置某列会**清空它之后的所有列**。重选 sidebar 会正确作废已打开的 detail,于是你绝不会渲染出「旧 detail 配新 sidebar」的错配。
|
|
191
|
-
|
|
192
|
-
默认所有列都可见,快照的 `destinations` 里**每个非空列**各一条 —— 框架会派发(服务端则预取)它们每一个。
|
|
193
|
-
|
|
194
|
-
### 列可见性
|
|
195
|
-
|
|
196
|
-
对标 SwiftUI 的 `NavigationSplitViewVisibility`,split 带一个可选的 **visibility** —— 这是**可绑定、可序列化的导航状态**(不是样式),它决定哪些列算可见,进而决定服务端预取什么:
|
|
107
|
+
`vp dev` 启动后监听源码变更;`vp check` 与构建也会生成。初次打开项目先运行其中一个命令。将 `.finesoft/` 加入 `.gitignore`,提交类中的框架管理引用。直接使用 `tsc` 或自定义工具链时,先调用 `@finesoft/front` 导出的 `generateControllerTypes({ root })`。`controllerTypes: false` 关闭自动维护。
|
|
197
108
|
|
|
198
|
-
|
|
199
|
-
| -------------------------- | ------------------------- |
|
|
200
|
-
| `automatic`(缺省)/ `all` | 全部列 |
|
|
201
|
-
| `doubleColumn` | 首列 + 末列(隐藏中间列) |
|
|
202
|
-
| `detailOnly` | 仅末列(detail) |
|
|
109
|
+
`createBrowserApp({ definition, target })` 返回的 `app`、`createWebSession` 和 SSR 的 `render(app)` 都保留该定义的参数关联:
|
|
203
110
|
|
|
204
111
|
```ts
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
// 声明时即指定(例如深链直达 detail)
|
|
208
|
-
split(
|
|
209
|
-
[
|
|
210
|
-
{ id: "sidebar", content: leaf("mailboxes") },
|
|
211
|
-
{ id: "detail", content: leaf("message", { id: 7 }) },
|
|
212
|
-
],
|
|
213
|
-
SPLIT_VISIBILITIES.DETAIL_ONLY,
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
// 或运行时切换 —— 新变可见的列会被派发,隐藏的列从快照中移除
|
|
217
|
-
await handle.setVisibility(SPLIT_VISIBILITIES.DETAIL_ONLY); // 只剩 detail 目标
|
|
218
|
-
await handle.setVisibility(SPLIT_VISIBILITIES.ALL); // 重新预取 sidebar + list
|
|
219
|
-
|
|
220
|
-
// 无需自己重实现映射,直接拿可见列渲染
|
|
221
|
-
for (const col of visibleSplitColumns(splitNode)) renderColumn(col);
|
|
112
|
+
await app.perform({ kind: "push", intent: "product", params: { id: 42 } });
|
|
113
|
+
// id: "42"、缺少 id 或未知 intent 都会产生编译错误。
|
|
222
114
|
```
|
|
223
115
|
|
|
224
|
-
`
|
|
225
|
-
|
|
226
|
-
## 定位嵌套容器
|
|
116
|
+
`route()` 和页面引用的 `.route()` 返回值也保留已声明的 codec 类型。保留工厂返回值的推导;不要用宽类型 `WebAppDefinition` 注解覆盖 `definition`。显式标为 `PageRoute` 的变量允许省略 codec,接收方类型也必须考虑未配置 codec 的情况;需要校验声明形状时可用 `satisfies PageRoute` 保留具体类型。组件需要显式声明应用类型时使用 `WebAppView<typeof definition>`(或 `ViewProps<ProductPage, typeof definition>`),无需再写参数接口。通用 `WebAppView` 仍用于与任意应用兼容的布局和 Outlet。
|
|
227
117
|
|
|
228
|
-
|
|
118
|
+
字符串数组如 `routes: ["/items/:id", "/products/:id"]` 继续支持多个别名,未声明 codec 的路径参数为 `string`,`:tab?` 为可选字符串。不同别名的参数结构不同时,控制器接收联合类型,需要先判断对应属性。相同参数匹配多个别名时,结构化目标需提供 `url` 指明路径,例如 `product.leaf({ id: 42 }, { url: "/products/42" })`。
|
|
229
119
|
|
|
230
|
-
|
|
231
|
-
import type { NavigationPath } from "@finesoft/front";
|
|
232
|
-
|
|
233
|
-
// split 的 detail 列里那个栈
|
|
234
|
-
const detailStack: NavigationPath = [
|
|
235
|
-
{ kind: "column", id: "detail" },
|
|
236
|
-
{ kind: "stack-entry", index: 0 },
|
|
237
|
-
];
|
|
238
|
-
|
|
239
|
-
await handle.push("attachment", { id: 3 }, { target: detailStack });
|
|
240
|
-
await handle.selectTab("photos", someTabsPath);
|
|
241
|
-
```
|
|
120
|
+
`optional`、`withDefault`、`list` 与第三方 Standard Schema 的输出决定接收方类型。`leaf` 和结构化 Action 使用同一输出形状;默认值字段在该形状中为必填,URL 导航省略它时仍由 Router 填入默认值。运行时仍由原有 Router 校验和转换参数,不增加第二次 Schema 校验。
|
|
242
121
|
|
|
243
|
-
|
|
122
|
+
### Query 与路径参数
|
|
244
123
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
上面这一切都由纯粹、不可变的树函数支撑,你可以直接用 —— 写测试、做乐观计算、或自建 controller:
|
|
124
|
+
两者共用 codec 与推导规则,在 Controller 的对象入参中分别提供,不需要手动解析 URL:
|
|
248
125
|
|
|
249
126
|
```ts
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
SSR 预取**所有**可见目标并把它们 —— 连同树本身 —— 序列化进 HTML,于是浏览器首屏直接复用服务端结果、不再取数。多列 split 视图天然预取多个 intent。
|
|
268
|
-
|
|
269
|
-
用 `createSSRNavigationRender` 配合 SSR 适配器:
|
|
270
|
-
|
|
271
|
-
```ts
|
|
272
|
-
// src/ssr.ts
|
|
273
|
-
import { createSSRNavigationRender } from "@finesoft/front";
|
|
274
|
-
import { bootstrap, navigation } from "./bootstrap";
|
|
275
|
-
import { renderApp } from "./lib/render";
|
|
276
|
-
|
|
277
|
-
export const render = createSSRNavigationRender({
|
|
278
|
-
bootstrap,
|
|
279
|
-
getErrorPage: (status, message) => ({
|
|
280
|
-
id: `error-${status}`,
|
|
281
|
-
pageType: "error",
|
|
282
|
-
title: message,
|
|
283
|
-
}),
|
|
284
|
-
renderApp, // (page, framework, snapshot) => { html, head, css }
|
|
285
|
-
navigation: navigation.toSSRDefinition(),
|
|
127
|
+
routes: [
|
|
128
|
+
{
|
|
129
|
+
path: "/products/:id",
|
|
130
|
+
params: { id: int() },
|
|
131
|
+
query: {
|
|
132
|
+
q: withDefault(str(), ""),
|
|
133
|
+
tags: optional(list(str())),
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
// execute({ params, query, context }) 内:
|
|
138
|
+
// params.id 是 number,query.q 是 string,query.tags 是 string[] | undefined。
|
|
139
|
+
await app.perform({
|
|
140
|
+
kind: "push",
|
|
141
|
+
intent: "product",
|
|
142
|
+
params: { id: 42 },
|
|
143
|
+
query: { q: "a & b", tags: ["new", "sale"] },
|
|
286
144
|
});
|
|
287
145
|
```
|
|
288
146
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
```ts
|
|
292
|
-
function renderApp(page, framework, snapshot) {
|
|
293
|
-
// page → 聚焦目标(如用于 <title>、status)
|
|
294
|
-
// snapshot.tree → 要画哪些 tab / 列
|
|
295
|
-
// snapshot.destinations → 每个可见区域的 Page
|
|
296
|
-
return renderYourFramework(snapshot);
|
|
297
|
-
}
|
|
298
|
-
```
|
|
147
|
+
框架把 params 编入路径、query 编入查询串;数组保留顺序并编码成重复键。普通 URL 导航仍可直接使用 `/products/42?q=a%20%26%20b&tags=new&tags=sale`。
|
|
299
148
|
|
|
300
|
-
`
|
|
149
|
+
Query 的字段名不限于路径占位符;`optional` 允许缺失,`withDefault` 仅在缺失时补值,显式空字符串仍参与校验。`list` 收集同名键,缺失时为 `[]`;`optional(list(...))` 缺失时为 `undefined`,`withDefault(list(...), [...])` 使用默认数组。普通单值字段遇到重复键仍取最后一个值。未声明的 query 保持字符串兼容行为,不会获得声明字段的类型保证;同名字段各自保留,例如 `/products/42?id=other` 的 params.id 为 42,query.id 为 "other"(声明为 str 时)。
|
|
301
150
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
若某请求没有结构化深链、应用也没提供骨架,SSR 回退到 `Router.resolve(url)` → 单个叶子 —— 即今天的扁平单页(含其 `renderMode`)。404 路径不变。
|
|
305
|
-
|
|
306
|
-
## 用 `createFullStateCodec` 做深链
|
|
307
|
-
|
|
308
|
-
默认情况下,**激活叶子**驱动 URL(`/posts/7`),完整的树通过 history state 旁路传输 —— 聚焦目标拥有干净、可分享的 URL。若要把**整棵**树编码进 URL 以支持完整深链(分享一个能还原 tab、栈深、split 选择的链接),改用 `createFullStateCodec`:
|
|
151
|
+
## Tree / 导航树
|
|
309
152
|
|
|
310
153
|
```ts
|
|
311
|
-
import {
|
|
312
|
-
|
|
313
|
-
export const navigation =
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
154
|
+
import { stack, tabs, split } from "@finesoft/front";
|
|
155
|
+
import { home, product } from "./pages";
|
|
156
|
+
export const navigation = tabs({
|
|
157
|
+
active: "catalog",
|
|
158
|
+
branches: {
|
|
159
|
+
catalog: stack([home.leaf(), product.leaf({ id: 42 })]),
|
|
160
|
+
compare: split([{ id: "left", content: product.leaf({ id: 42 }) }, { id: "right" }]),
|
|
161
|
+
},
|
|
319
162
|
});
|
|
163
|
+
// defineWebApp({ ..., navigation })
|
|
320
164
|
```
|
|
321
165
|
|
|
322
|
-
|
|
166
|
+
相同目标仍有独立 EntryId 与草稿;ResourceKey 可共享显式缓存的查询数据,但不会共享视图状态。标准浏览器启动器拥有 URL 动作、重定向、popstate,过期 URL 结果不能覆盖后来的操作。显式树操作通过单个串行队列执行。Tabs 保留分支,Stack 保留在树条目,Split 检查各个目标。原生视图生命周期需跟随树时选择 `entries`。
|
|
323
167
|
|
|
324
|
-
|
|
168
|
+
## Action 导航
|
|
325
169
|
|
|
326
|
-
|
|
170
|
+
所有导航通过 `app.perform(action)` 执行。URL 使用 `{ kind: "flow", url }`;结构化导航使用下列 Action:
|
|
327
171
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
172
|
+
| kind | Fields |
|
|
173
|
+
| ----------------- | ------------------------------------------------------------- |
|
|
174
|
+
| push / replaceTop | intent, params?, query?, target?, url? |
|
|
175
|
+
| pop | count?, target? |
|
|
176
|
+
| popToRoot | target? |
|
|
177
|
+
| popTo | index, target? |
|
|
178
|
+
| selectTab | key, target? |
|
|
179
|
+
| selectColumn | columnId, intent (undefined clears), params?, query?, target? |
|
|
180
|
+
| setVisibility | visibility, target? |
|
|
181
|
+
| reuseEntry | entryId |
|
|
182
|
+
| refresh | — |
|
|
183
|
+
| hydrate | tree |
|
|
339
184
|
|
|
340
|
-
|
|
341
|
-
- `rewrite` → 用新 URL 重新解析出该目标的 intent/params。
|
|
342
|
-
- `deny` → 给目标打上 deny status,不派发其 intent。
|
|
185
|
+
## 导航事务策略
|
|
343
186
|
|
|
344
|
-
|
|
187
|
+
`defineWebApp({ beforeNavigate, beforeCommit })` 可选地声明整棵树的策略。数组中的每个策略每次事务只运行一次,空树退出也会执行;先执行应用定义的策略,再执行控制器附加策略。`beforeLoad` / `afterLoad` 仍按每个可见页面运行。
|
|
345
188
|
|
|
346
|
-
|
|
189
|
+
`beforeNavigate` 接收原快照 `from`、候选树 `tree`、稳定的 `transitionId`、当前 `execution` 及其 `signal`、`isServer`,返回 `next`、`deny` 或 `redirect`。页面重定向沿用同一事务标识,不重复运行准入策略;各跳转仍由原所有者释放执行资源。`beforeCommit` 另接收加载完成的 `candidate`,仅允许 `next` 或 `deny`,在消费预取缓存、写入快照、历史、事件和视图之前执行。普通应用无需配置策略或手动管理作用域。
|
|
347
190
|
|
|
348
|
-
|
|
349
|
-
- 单叶子树等价于扁平单页:一个可见目标、一次 resolve/dispatch、一对 before/after。SSR 仅在 `serverData` 多挂一条树哨兵(在抵达 `PrefetchedIntents` 前被剔除)。
|
|
350
|
-
- `Page` 保持内容无关。导航在你的页面**周围**加结构,从不规定页面形状或你怎么渲染它。
|
|
191
|
+
拒绝返回带 `rejection` 的未提交快照,空树也能表达拒绝。会话恢复会在替换 scope 和业务 slice 前检查是否提交。浏览器首屏拒绝显示错误页,不提交导航或记录页面访问;后续拒绝保留当前草稿。扁平和导航 SSR 都执行相同策略,不输出被拒绝页面的数据或公开缓存许可。CSR 空壳继续由浏览器执行导航。
|
|
351
192
|
|
|
352
|
-
|
|
193
|
+
自有历史条目的后退或前进被拒绝时,History 补偿回已提交条目并保持滚动身份;附加元数据让刷新后仍可识别所有权与位置。缺少兼容元数据的外部条目会给出诊断,不猜测应跨越几个历史位置。异步策略应把 `signal` 传给自身 I/O;取消不会回滚已经完成的业务写入。
|
|
353
194
|
|
|
354
|
-
|
|
355
|
-
- [渲染与 Hydration](./04-rendering-and-hydration.md) —— 渲染模式 × 架构矩阵、islands SSR 外壳,以及预取结果如何跨越 SSR → CSR 边界
|
|
195
|
+
底层宿主配置 `createWebSession({ createContext })` 时,回调直接返回 `NavigationContext`。执行作用域提供 DI 容器和取消信号,请求 cookie 与 header 由宿主上下文提供。会话默认使用应用的 `getErrorPage`,也可显式覆盖。
|