@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
|
@@ -1,314 +1,236 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 路由、控制器与类型化页面
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
六个模板均使用 `BaseController` 加载页面。它继续作为公开 API 提供;`definePage` 将控制器工厂或函数处理器接入 Web 应用,两种写法共用运行时、执行作用域和策略。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 各层职责
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
▼
|
|
14
|
-
Controller.execute() → Page
|
|
15
|
-
```
|
|
7
|
+
| API | 职责 |
|
|
8
|
+
| --------------------------------- | ----------------------------------------------------------------- |
|
|
9
|
+
| `BaseController<TInput, TResult>` | 实现 `execute`,通过继承的 `perform` 处理参数、取消和 `fallback` |
|
|
10
|
+
| `definePage({ id, create })` | 声明页面加载工厂,生成可复用的 `route`、`leaf` 和 `bindView` 引用 |
|
|
11
|
+
| `defineWebApp` | 组装页面声明、路由、守卫及可选导航结构 |
|
|
12
|
+
| 原生页面组件 | 接收 `page` 数据,渲染 UI 和处理交互 |
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
控制器、页面、路由与公开数据声明统一从 `@finesoft/front` 导入。
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
- **intent id**(操作的逻辑名;一个 intent 可以对应多条路由)
|
|
21
|
-
- **Controller 实例**(产出页面数据)
|
|
22
|
-
- 可选的**参数 codec**(`params` / `query`)—— 校验并类型化 URL 参数
|
|
23
|
-
- 可选的**渲染模式**(`ssr` / `csr` / `prerender`)
|
|
24
|
-
- 可选的**守卫**(`beforeLoad` / `afterLoad`)
|
|
16
|
+
## 用 BaseController 加载页面
|
|
25
17
|
|
|
26
|
-
|
|
18
|
+
`src/lib/controllers/product.ts`:
|
|
27
19
|
|
|
28
20
|
```ts
|
|
29
|
-
|
|
30
|
-
import { type
|
|
31
|
-
import { HomeController } from "./lib/controllers/home";
|
|
32
|
-
import { ProductController } from "./lib/controllers/product";
|
|
33
|
-
import { authGuard } from "./lib/guards/auth";
|
|
34
|
-
|
|
35
|
-
export function bootstrap(framework: Framework): void {
|
|
36
|
-
defineRoutes(framework, [
|
|
37
|
-
// 普通 SSR 路由
|
|
38
|
-
{ path: "/", intentId: "home", controller: new HomeController() },
|
|
39
|
-
|
|
40
|
-
// 动态参数段
|
|
41
|
-
{ path: "/products/:id", intentId: "product", controller: new ProductController() },
|
|
42
|
-
|
|
43
|
-
// 仅 CSR(服务端只返回空壳)
|
|
44
|
-
{
|
|
45
|
-
path: "/dashboard",
|
|
46
|
-
intentId: "dashboard",
|
|
47
|
-
controller: new DashboardController(),
|
|
48
|
-
renderMode: "csr",
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
// 构建期静态化
|
|
52
|
-
{
|
|
53
|
-
path: "/about",
|
|
54
|
-
intentId: "about",
|
|
55
|
-
controller: new AboutController(),
|
|
56
|
-
renderMode: "prerender",
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
// 受保护路由 —— 复用 home intent,但加守卫
|
|
60
|
-
{
|
|
61
|
-
path: "/admin",
|
|
62
|
-
intentId: "home",
|
|
63
|
-
controller: new HomeController(),
|
|
64
|
-
beforeLoad: [authGuard],
|
|
65
|
-
},
|
|
66
|
-
]);
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### 路由选项
|
|
71
|
-
|
|
72
|
-
| 字段 | 类型 | 说明 |
|
|
73
|
-
| ------------ | -------------------------------- | -------------------------------------------------------------- |
|
|
74
|
-
| `path` | `string` | 路径模式,含 `:param` 占位。结尾的 `/` 会被归一化。 |
|
|
75
|
-
| `intentId` | `string` | 操作的逻辑名。用于注册 Controller。 |
|
|
76
|
-
| `controller` | `BaseController<TParams, TPage>` | 若 intent 已注册可省略。 |
|
|
77
|
-
| `params` | codec map | 校验/转换 **path** 参数。key 必须出现在 `path` 中。见下文。 |
|
|
78
|
-
| `query` | codec map | 校验/转换 **query** 参数。key 开放。见下文。 |
|
|
79
|
-
| `renderMode` | `"ssr" \| "csr" \| "prerender"` | 默认 `"ssr"`。详见[第 4 章](./04-rendering-and-hydration.md)。 |
|
|
80
|
-
| `beforeLoad` | `BeforeLoadGuard[]` | Controller 之前执行。详见[第 3 章](./03-middleware.md)。 |
|
|
81
|
-
| `afterLoad` | `AfterLoadGuard[]` | Page 产出之后执行。 |
|
|
82
|
-
|
|
83
|
-
### 路径模式
|
|
21
|
+
import { BaseController, DEP_KEYS } from "@finesoft/front";
|
|
22
|
+
import { markPublic, type BasePage } from "@finesoft/front";
|
|
84
23
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
默认情况下,path 和 query 参数以**字符串键对象**的形式进入 `controller.execute(params, container)`。挂上 codec(下一节)即可校验并转换它们。
|
|
90
|
-
|
|
91
|
-
## 路由参数类型化
|
|
24
|
+
export interface ProductPage extends BasePage {
|
|
25
|
+
pageType: "product";
|
|
26
|
+
product: { id: number; name: string };
|
|
27
|
+
}
|
|
92
28
|
|
|
93
|
-
|
|
29
|
+
export class ProductController extends BaseController {
|
|
30
|
+
async execute({ params, context }): Promise<ProductPage> {
|
|
31
|
+
const logger = await context.get(DEP_KEYS.LOGGER_FACTORY);
|
|
32
|
+
logger.loggerFor("ProductController").info(`Loading product ${params.id}`);
|
|
33
|
+
context.record("product.load", { productId: params.id });
|
|
34
|
+
|
|
35
|
+
return markPublic(
|
|
36
|
+
{
|
|
37
|
+
id: String(params.id),
|
|
38
|
+
pageType: "product",
|
|
39
|
+
title: `Product ${params.id}`,
|
|
40
|
+
product: { id: params.id, name: `Product ${params.id}` },
|
|
41
|
+
},
|
|
42
|
+
{ product: { id: true, name: true } },
|
|
43
|
+
);
|
|
44
|
+
}
|
|
94
45
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
tags: list(str()), // ?tags=a&tags=b → string[]
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
]);
|
|
46
|
+
override fallback({ params }): ProductPage {
|
|
47
|
+
return markPublic(
|
|
48
|
+
{
|
|
49
|
+
id: String(params.id),
|
|
50
|
+
pageType: "product",
|
|
51
|
+
title: "Product unavailable",
|
|
52
|
+
product: { id: params.id, name: "Unavailable" },
|
|
53
|
+
},
|
|
54
|
+
{ product: { id: true, name: true } },
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
111
58
|
```
|
|
112
59
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
| codec | 输出 | 校验 |
|
|
116
|
-
| ----------------------- | ---------- | ------------------------------------------------- |
|
|
117
|
-
| `str(opts?)` | `string` | `minLength` / `maxLength` / `pattern`(`RegExp`) |
|
|
118
|
-
| `int(opts?)` | `number` | 整数 + `min` / `max` |
|
|
119
|
-
| `num(opts?)` | `number` | 有限数 + `min` / `max` |
|
|
120
|
-
| `bool()` | `boolean` | `"true" \| "1" \| "false" \| "0"` |
|
|
121
|
-
| `oneOf([...] as const)` | 字面量联合 | 成员检查 |
|
|
122
|
-
| `uuid()` | `string` | UUID v1–v5 |
|
|
123
|
-
| `list(item, opts?)` | `T[]` | 多值 query;每项过 `item` + `min`/`max` 个数 |
|
|
124
|
-
|
|
125
|
-
修饰器包裹 codec(codec 保持可序列化纯数据,不用链式 `.optional()`):
|
|
60
|
+
框架根据下方路由声明维护类型导入、方法注解和基类泛型;这里展示首次编写时的源码,省略自动生成的引用。运行 `vp dev`、`vp check` 或构建即可生成。类型集中在 `.finesoft/controller-types.d.ts`,Controller 文件末尾不再追加声明区块。配置方式见[控制器类型自动关联](./11-navigation.md)。
|
|
126
61
|
|
|
127
|
-
|
|
128
|
-
- `withDefault(codec, fallback)` —— 输入缺失 → `fallback`;key 保持必选。
|
|
62
|
+
两个方法统一接收一个对象:`execute({ params, query, context })` 和 `fallback({ params, query, context, error })`,只解构需要的字段即可。Query 与路径参数分开,根据路由中的 `query` 声明推断类型。
|
|
129
63
|
|
|
130
|
-
|
|
64
|
+
这个示例使用本地数据;实际业务可以在 `execute` 中调用服务。`context` 提供当前执行的 `signal`、`fetch`、`get(token)` 和 `execute(operation, input)`。执行异步请求时,将 `context.signal` 传给请求 API,取消才能传递到实际工作。
|
|
131
65
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
```ts
|
|
135
|
-
defineRoutes(framework, [
|
|
136
|
-
{ path: "/item/:id", intentId: "item-by-id", controller, params: { id: int() } },
|
|
137
|
-
{ path: "/item/:slug", intentId: "item-by-slug", controller, params: { slug: str() } },
|
|
138
|
-
]);
|
|
139
|
-
// /item/42 → item-by-id (int 匹配)
|
|
140
|
-
// /item/hello → item-by-slug (int 拒绝 → fall-through 到 str)
|
|
141
|
-
```
|
|
66
|
+
`fallback` 仅在 `execute` 发生普通异常时调用,返回类型仍是 `ProductPage`;示例选择展示不可用状态。未覆写时默认重新抛出错误,交给页面加载流程的错误处理。取消、`AbortError` 和 `ExecutionError("cancelled")` 会继续抛出,不进入 `fallback`。运行时策略在控制器外执行,策略拒绝也不会由控制器回退处理。
|
|
142
67
|
|
|
143
|
-
|
|
68
|
+
## 注册工厂、路由与导航目标
|
|
144
69
|
|
|
145
|
-
`
|
|
70
|
+
`src/app-definition.ts`:
|
|
146
71
|
|
|
147
72
|
```ts
|
|
148
|
-
import {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
type InferQuery,
|
|
152
|
-
int,
|
|
153
|
-
oneOf,
|
|
154
|
-
optional,
|
|
155
|
-
} from "@finesoft/front";
|
|
73
|
+
import { int } from "@finesoft/front";
|
|
74
|
+
import { definePage, defineWebApp } from "@finesoft/front";
|
|
75
|
+
import { ProductController } from "./lib/controllers/product";
|
|
156
76
|
|
|
157
|
-
const
|
|
158
|
-
|
|
77
|
+
export const product = definePage({
|
|
78
|
+
id: "load-product",
|
|
79
|
+
create: () => new ProductController(),
|
|
80
|
+
routes: [{ path: "/products/:id", params: { id: int() } }],
|
|
81
|
+
});
|
|
82
|
+
export const target = product.leaf({ id: 42 });
|
|
159
83
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
execute(params) {
|
|
166
|
-
// params.id: number, params.sort: "asc" | "desc" | undefined
|
|
167
|
-
}
|
|
168
|
-
}
|
|
84
|
+
export const app = defineWebApp({
|
|
85
|
+
id: "example",
|
|
86
|
+
pages: [product],
|
|
87
|
+
getErrorPage: (status, message) => ({ id: String(status), pageType: "error", title: message }),
|
|
88
|
+
});
|
|
169
89
|
```
|
|
170
90
|
|
|
171
|
-
|
|
91
|
+
页面声明拥有唯一操作标识,控制器不再重复声明 intentId。URL `/products/42` 经 `int()` 解码后,`execute` 收到 `params: { id: 42 }`;代码内的 `product.leaf({ id: 42 })` 保留同样的参数类型。
|
|
172
92
|
|
|
173
|
-
|
|
93
|
+
`create` 每次实际执行时返回新控制器。声明、路由发现及引用辅助方法只读取定义;命中已预取或保留的页面结果时也无需创建控制器。请求身份放在执行上下文或有作用域的 provider 中;页面草稿放在页面实例中。
|
|
174
94
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
```
|
|
95
|
+
`id` 是操作标识,`pageType` 选择视图,每个叶子节点生成独立 EntryId。`product.bindView("product", ProductView)` 检查结果中的字面量页面类型,不从操作 id 推断类型。显式保留 `ProductPage.pageType: "product"` 才能获得该检查;宽泛的 `BasePage.pageType: string` 无法约束视图名称。
|
|
96
|
+
|
|
97
|
+
类型化叶子参数保留控制器类型;外部 URL 仍需显式 codec 解码。泛型提供编译期类型约束,不替代运行期输入校验,也不会自动证明任意 codec 与业务校验规则等价。
|
|
179
98
|
|
|
180
|
-
|
|
99
|
+
## 简单页面的函数写法
|
|
181
100
|
|
|
182
|
-
|
|
101
|
+
只需直接返回页面数据时,可以使用 `handler`。它也能通过执行上下文使用依赖、取消和嵌套操作;需要回退逻辑时,在函数内自行处理。`create` 和 `handler` 二选一。
|
|
183
102
|
|
|
184
103
|
```ts
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
handler: (
|
|
190
|
-
// params: { id: number; page: number } —— 从 codec 推导
|
|
191
|
-
return loadProduct(params.id, params.page);
|
|
192
|
-
},
|
|
193
|
-
fallback: (params, error) => errorPage(error), // 可选
|
|
104
|
+
import { definePage, markPublic } from "@finesoft/front";
|
|
105
|
+
|
|
106
|
+
export const home = definePage({
|
|
107
|
+
id: "load-home",
|
|
108
|
+
handler: () => markPublic({ id: "home", pageType: "home" as const, title: "Home" }, []),
|
|
194
109
|
});
|
|
195
110
|
```
|
|
196
111
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
## 写一个 Controller
|
|
112
|
+
模板采用控制器类来保持组织方式一致;函数形式适用于简短加载逻辑。既有函数处理器接收 `(params, context, query)`。继承基类是可选的,实现 `perform(params, context, query)` 契约也能由 `create` 工厂接入。
|
|
200
113
|
|
|
201
|
-
|
|
202
|
-
// src/lib/controllers/product.ts
|
|
203
|
-
import { BaseController, type Container, type HttpClient } from "@finesoft/front";
|
|
204
|
-
|
|
205
|
-
interface ProductPage {
|
|
206
|
-
kind: "product";
|
|
207
|
-
id: string;
|
|
208
|
-
name: string;
|
|
209
|
-
price: number;
|
|
210
|
-
}
|
|
114
|
+
## 服务端 Controller 与两种上下文
|
|
211
115
|
|
|
212
|
-
|
|
213
|
-
readonly intentId = "product";
|
|
116
|
+
需要机密、HttpOnly Cookie 或响应设置的页面使用 `@finesoft/front` 的 `BaseServerController`。方法仍然是 `execute({ params, query, context })` / `fallback({ params, query, context, error })`,路由仍通过 `definePage({ create, routes })` 注册。类型生成器识别基类并维护同样简短的 `Input` / `Failure` 引用,不需要第二套路由声明。
|
|
214
117
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
118
|
+
```ts
|
|
119
|
+
import { BaseServerController } from "@finesoft/front";
|
|
120
|
+
import { markPublic } from "@finesoft/front";
|
|
121
|
+
import { loadAccount } from "./account-service";
|
|
122
|
+
|
|
123
|
+
export class AccountController extends BaseServerController {
|
|
124
|
+
async execute({ params, context }) {
|
|
125
|
+
const account = await loadAccount(params.id, context.getCookie("session"));
|
|
126
|
+
context.responseHeaders.set("Cache-Control", "private, no-store");
|
|
127
|
+
return markPublic(
|
|
128
|
+
{ id: String(params.id), pageType: "account", title: account.name, account },
|
|
129
|
+
{ account: { id: true, name: true } },
|
|
219
130
|
);
|
|
220
|
-
return {
|
|
221
|
-
kind: "product",
|
|
222
|
-
id: params.id,
|
|
223
|
-
name: product.name,
|
|
224
|
-
price: product.price,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
fallback(params: { id: string }, _error: unknown): ProductPage {
|
|
229
|
-
return { kind: "product", id: params.id, name: "Not available", price: 0 };
|
|
230
131
|
}
|
|
231
132
|
}
|
|
232
133
|
```
|
|
233
134
|
|
|
234
|
-
|
|
135
|
+
示例省略框架自动维护的类型注解。`account-service` 是业务实现;Controller 可以放在任意业务目录。Vite 根据继承关系替换整个服务端 Controller 模块,浏览器获得调用代理;SSR 保留原实现并直接执行。客户端导航通过同站 `POST /__finesoft/controller` 加载公开页面数据,服务端重新解析路由、执行策略和守卫。首次水合使用已有 SSR 数据,不重复请求。
|
|
136
|
+
|
|
137
|
+
| 能力 | 共享 `BaseController` 的页面上下文 | `BaseServerController` |
|
|
138
|
+
| ------------------------------------------------------ | -------------------------------------- | ----------------------------------- |
|
|
139
|
+
| `params`、`query` | 独立输入,按路由推断 | 相同 |
|
|
140
|
+
| `context.url`、`path`、`intent`、`isServer` | 当前页面与执行位置 | 相同,`isServer` 为 `true` |
|
|
141
|
+
| `getCookie(name)` | 浏览器可读 Cookie;SSR 当前请求 Cookie | 读取原始请求 Cookie,包含 HttpOnly |
|
|
142
|
+
| `getHeader(name)` | SSR 请求头;浏览器无服务端请求头 | 当前服务端请求头 |
|
|
143
|
+
| `get(token)`、`fetch`、`signal`、`execute`、日志和追踪 | 现有执行上下文 | 相同 |
|
|
144
|
+
| `request`、`responseHeaders` | 无专用接口 | 原始 `Request` 与最终响应头 |
|
|
145
|
+
| `setCookie`、`deleteCookie` | 无专用接口 | 写入最终响应,不改变原始请求 Cookie |
|
|
235
146
|
|
|
236
|
-
|
|
237
|
-
| ---------- | ---- | --------------------------------------------------------------------------- |
|
|
238
|
-
| `intentId` | 是 | 必须与路由的 `intentId` 一致(或与 `IntentDispatcher.register` 调用一致)。 |
|
|
239
|
-
| `execute` | 是 | 产出页面。接受解析后的路径参数和请求级 DI 容器。 |
|
|
240
|
-
| `fallback` | 是 | `execute()` 抛错时返回降级页面。必须同步且总能返回。 |
|
|
147
|
+
自动生成的页面 `Input` 使用 `ControllerContext` 或 `ServerControllerContext`。独立数据操作仍使用可移植的 `ExecutionContext`;直接执行 Page operation、没有导航时,`url` / `path` 为空。远程调用的 `request.url` 是传输端点地址,页面地址请使用 `context.url`。
|
|
241
148
|
|
|
242
|
-
|
|
149
|
+
### 构建边界
|
|
243
150
|
|
|
244
|
-
|
|
151
|
+
服务端 Controller 模块只导出 Controller 类和类型。该模块的本地运行时依赖也属于服务端边界;客户端直接导入这些依赖会报错。共享类型请使用 `import type`,共享的客户端实现放在独立模块。构造函数内的依赖创建属于受保护模块;不要在共享的 `create` 工厂参数、共享配置或页面组件中写入机密。
|
|
245
152
|
|
|
246
|
-
|
|
153
|
+
生产客户端 JS / source map 不包含被替换的实现。开发服务拒绝 Controller 的原始资源导入和受保护依赖的源码请求。模块与依赖索引在启动时建立,后续缓存 AST,只重新分析变更文件;开发期间已识别的私有依赖保持保护,若要将它迁回共享模块,应重启开发服务。动态计算的文件路径、`public/` 资源和另行复制的源码不属于静态导入边界。开发服务器仍只用于受信任的开发环境。
|
|
247
154
|
|
|
248
|
-
|
|
155
|
+
使用框架 Vite 插件及具有 SSR 请求处理能力的 Node / Worker 等宿主。纯静态托管没有远程执行端点。此入口用于注册的服务端页面 Controller,不会自动发布任意方法或代替登录、注册等命令端点。
|
|
249
156
|
|
|
250
|
-
|
|
251
|
-
| ------------- | -------------------------------- | ------------------------------------------- |
|
|
252
|
-
| `"ssr"` | 完整渲染的 HTML + 序列化数据 | 默认。SEO 和 TTFB 敏感的页面。 |
|
|
253
|
-
| `"csr"` | 空壳 HTML;Controller 在浏览器跑 | 鉴权后的 dashboard、高度个性化的页面。 |
|
|
254
|
-
| `"prerender"` | 部署期生成的静态 HTML | 营销页、文档、博客。结合 ISR(见第 4 章)。 |
|
|
157
|
+
### 显式传递请求与响应信息
|
|
255
158
|
|
|
256
|
-
|
|
159
|
+
框架提供 `request`、`getHeader`、`getCookie`、`fetch`、`responseHeaders`、`setCookie` 和 `deleteCookie` 等通用工具。登录状态、鉴权、令牌存储与刷新,以及凭据是否传递,均由业务代码决定。
|
|
257
160
|
|
|
258
|
-
|
|
161
|
+
服务端 `context.fetch` 只使用调用者提供的请求头,不会从外层请求自动继承 Cookie / Authorization,也不会把内部响应的 `Set-Cookie` 自动复制到页面响应。`credentials` 选项不会在服务端创建或启用 Cookie 存储。浏览器发起请求时,Cookie 仍遵循浏览器原生的同站请求规则。
|
|
259
162
|
|
|
260
|
-
|
|
163
|
+
例如,业务选择给一个内部接口传递 Authorization,并将该接口返回的 Cookie 写入最终响应:
|
|
261
164
|
|
|
262
165
|
```ts
|
|
263
|
-
|
|
166
|
+
const headers = new Headers();
|
|
167
|
+
const authorization = context.getHeader("authorization");
|
|
168
|
+
if (authorization) headers.set("authorization", authorization);
|
|
264
169
|
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
});
|
|
170
|
+
const response = await context.fetch("/api/account", { headers });
|
|
171
|
+
for (const cookie of response.headers.getSetCookie()) {
|
|
172
|
+
context.responseHeaders.append("set-cookie", cookie);
|
|
173
|
+
}
|
|
270
174
|
```
|
|
271
175
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
## 一个 intent 多个路由
|
|
176
|
+
选择哪些请求头、哪些响应 Cookie、发给哪个接口,都是上面业务代码的决定。需要多处复用时,可放进业务 `HttpClient` 拦截器或请求作用域服务,通过 `context.get` 使用。
|
|
275
177
|
|
|
276
|
-
|
|
178
|
+
业务也可以直接构造响应 Cookie:
|
|
277
179
|
|
|
278
180
|
```ts
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
{ path: "/welcome", intentId: "home" }, // 复用已注册的 HomeController
|
|
282
|
-
{ path: "/landing/:slug", intentId: "home" }, // 同 intent,参数不同
|
|
283
|
-
]);
|
|
181
|
+
context.setCookie("theme", "dark", { path: "/", sameSite: "Lax" });
|
|
182
|
+
context.deleteCookie("theme", { path: "/" });
|
|
284
183
|
```
|
|
285
184
|
|
|
286
|
-
|
|
185
|
+
`getCookie` 始终读取本次收到的请求;写响应 Cookie 不会修改它,也不会自动更新任何用户身份。后续业务逻辑需要新值时,使用业务自身保存的结果。`responseHeaders` 是显式响应写入工具;框架负责组装最终响应,包括错误响应中的业务响应头,同时保留框架错误正文类型及禁用缓存的约束。
|
|
287
186
|
|
|
288
|
-
|
|
187
|
+
远程请求提供的 `context` / `bindings` 不会成为可信身份。`markPublic` 继续控制发给浏览器的数据字段,机密也不能直接渲染进 HTML。
|
|
289
188
|
|
|
290
|
-
|
|
189
|
+
## 独立业务操作也可以用控制器
|
|
190
|
+
|
|
191
|
+
`BaseController` 的结果泛型不要求是页面。通过 `implementController` 将类绑定到操作后,可以在独立数据运行时执行;HTTP / Worker 入口也可以调用该操作。
|
|
291
192
|
|
|
292
193
|
```ts
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
```
|
|
194
|
+
import {
|
|
195
|
+
BaseController,
|
|
196
|
+
createRuntime,
|
|
197
|
+
defineApp,
|
|
198
|
+
defineOperation,
|
|
199
|
+
implementController,
|
|
200
|
+
type ControllerInput,
|
|
201
|
+
} from "@finesoft/front";
|
|
302
202
|
|
|
303
|
-
|
|
203
|
+
type TotalInput = { unitPrice: number; quantity: number };
|
|
204
|
+
interface TotalResult {
|
|
205
|
+
total: number;
|
|
206
|
+
}
|
|
304
207
|
|
|
305
|
-
|
|
208
|
+
class TotalController extends BaseController<ControllerInput<TotalInput>, TotalResult> {
|
|
209
|
+
execute({ params }: ControllerInput<TotalInput>): TotalResult {
|
|
210
|
+
return { total: params.unitPrice * params.quantity };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
306
213
|
|
|
307
|
-
|
|
214
|
+
const calculateTotal = defineOperation<TotalInput, TotalResult>({
|
|
215
|
+
id: "calculate-total",
|
|
216
|
+
kind: "query",
|
|
217
|
+
});
|
|
218
|
+
const runtime = createRuntime({
|
|
219
|
+
app: defineApp({
|
|
220
|
+
id: "data-example",
|
|
221
|
+
operations: [calculateTotal],
|
|
222
|
+
implementations: [implementController(calculateTotal, () => new TotalController())],
|
|
223
|
+
}),
|
|
224
|
+
});
|
|
308
225
|
|
|
309
|
-
|
|
226
|
+
try {
|
|
227
|
+
const result = await runtime.execute(calculateTotal, { unitPrice: 29, quantity: 2 });
|
|
228
|
+
console.log(result); // { total: 58 }
|
|
229
|
+
} finally {
|
|
230
|
+
await runtime.dispose();
|
|
231
|
+
}
|
|
232
|
+
```
|
|
310
233
|
|
|
311
|
-
|
|
234
|
+
操作声明定义输入输出契约,控制器负责实现。将操作暴露为 HTTP 接口仍需显式配置端点,注册控制器本身不会公开接口。
|
|
312
235
|
|
|
313
|
-
|
|
314
|
-
- [渲染与 Hydration](./04-rendering-and-hydration.md) —— Controller 产出 Page 之后发生什么
|
|
236
|
+
<Ch02RouteResolver />
|