@finesoft/front 0.1.76 → 0.1.77

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.
Files changed (51) hide show
  1. package/docs/01-getting-started.md +230 -0
  2. package/docs/02-routing-and-controllers.md +197 -0
  3. package/docs/03-middleware.md +214 -0
  4. package/docs/04-rendering-and-hydration.md +271 -0
  5. package/docs/05-i18n.md +243 -0
  6. package/docs/06-http-client.md +286 -0
  7. package/docs/07-di-container.md +264 -0
  8. package/docs/08-observability.md +290 -0
  9. package/docs/09-server-and-deployment.md +242 -0
  10. package/docs/10-features-platform-pwa.md +238 -0
  11. package/docs/README.md +72 -0
  12. package/docs/advanced/custom-action-handler.md +248 -0
  13. package/docs/advanced/custom-adapter.md +264 -0
  14. package/docs/advanced/custom-event-recorder.md +318 -0
  15. package/docs/advanced/inline-proxy-codegen.md +200 -0
  16. package/docs/advanced/multi-tenant-scopes.md +330 -0
  17. package/docs/engineering/ci-release-flow.md +244 -0
  18. package/docs/engineering/project-structure.md +296 -0
  19. package/docs/engineering/testing.md +317 -0
  20. package/docs/pitfalls/container-scope-leak.md +215 -0
  21. package/docs/pitfalls/i18n-bundle-size.md +182 -0
  22. package/docs/pitfalls/proxy-binary-payloads.md +133 -0
  23. package/docs/pitfalls/redirect-vs-rewrite.md +147 -0
  24. package/docs/pitfalls/ssr-hydration-mismatch.md +163 -0
  25. package/docs/pitfalls/ssr-vs-csr-globals.md +176 -0
  26. package/docs/zh/01-getting-started.md +230 -0
  27. package/docs/zh/02-routing-and-controllers.md +197 -0
  28. package/docs/zh/03-middleware.md +214 -0
  29. package/docs/zh/04-rendering-and-hydration.md +271 -0
  30. package/docs/zh/05-i18n.md +243 -0
  31. package/docs/zh/06-http-client.md +286 -0
  32. package/docs/zh/07-di-container.md +264 -0
  33. package/docs/zh/08-observability.md +287 -0
  34. package/docs/zh/09-server-and-deployment.md +242 -0
  35. package/docs/zh/10-features-platform-pwa.md +238 -0
  36. package/docs/zh/README.md +72 -0
  37. package/docs/zh/advanced/custom-action-handler.md +248 -0
  38. package/docs/zh/advanced/custom-adapter.md +264 -0
  39. package/docs/zh/advanced/custom-event-recorder.md +318 -0
  40. package/docs/zh/advanced/inline-proxy-codegen.md +200 -0
  41. package/docs/zh/advanced/multi-tenant-scopes.md +330 -0
  42. package/docs/zh/engineering/ci-release-flow.md +244 -0
  43. package/docs/zh/engineering/project-structure.md +296 -0
  44. package/docs/zh/engineering/testing.md +317 -0
  45. package/docs/zh/pitfalls/container-scope-leak.md +215 -0
  46. package/docs/zh/pitfalls/i18n-bundle-size.md +182 -0
  47. package/docs/zh/pitfalls/proxy-binary-payloads.md +133 -0
  48. package/docs/zh/pitfalls/redirect-vs-rewrite.md +147 -0
  49. package/docs/zh/pitfalls/ssr-hydration-mismatch.md +163 -0
  50. package/docs/zh/pitfalls/ssr-vs-csr-globals.md +176 -0
  51. package/package.json +2 -1
@@ -0,0 +1,248 @@
1
+ # 高阶:自定义 Action handler
2
+
3
+ 框架内置三种 action:`flow`(应用内导航)、`external-url`(整页浏览器跳转)、`compound`(按顺序执行的 action 元组)。对大多数应用够用。
4
+
5
+ 本配方展示怎么加自己的 —— 适用于有一类操作需要横切处理(分析、确认、遥测)又不想污染每个调用点。
6
+
7
+ ## 用例:带确认的 action
8
+
9
+ 加 `"confirm"` action kind:dispatch `{ kind: "confirm", message, then }`,框架在 dispatch `then` 之前(`then` 本身也是个 action)显示确认对话框。
10
+
11
+ ```ts
12
+ dispatch({
13
+ kind: "confirm",
14
+ message: "Delete this item permanently?",
15
+ then: { kind: "flow", url: "/items/42/deleted" },
16
+ });
17
+ ```
18
+
19
+ 用户点 Cancel → 不导航。点 OK → 内层 flow action 触发。
20
+
21
+ ## 步骤 1:定义 action 类型
22
+
23
+ ```ts
24
+ // src/lib/actions/confirm.ts
25
+ import { type Action } from "@finesoft/front";
26
+
27
+ export interface ConfirmAction {
28
+ kind: "confirm";
29
+ message: string;
30
+ then: Action;
31
+ }
32
+
33
+ export function makeConfirmAction(message: string, then: Action): ConfirmAction {
34
+ return { kind: "confirm", message, then };
35
+ }
36
+
37
+ export function isConfirmAction(action: Action): action is ConfirmAction {
38
+ return (action as any).kind === "confirm";
39
+ }
40
+ ```
41
+
42
+ 形状你定 —— `kind` 只要在已注册 handler 中唯一即可。
43
+
44
+ ## 步骤 2:扩展 `Action` 类型 union
45
+
46
+ TypeScript 不会自动扩展框架的 `Action` 类型。声明模块增强:
47
+
48
+ ```ts
49
+ // src/lib/actions/confirm.ts
50
+ declare module "@finesoft/front" {
51
+ interface ActionRegistry {
52
+ confirm: ConfirmAction;
53
+ }
54
+ }
55
+ ```
56
+
57
+ 框架暴露 `ActionRegistry` 的话(大多数可插拔框架会),TypeScript 就知道你的新 kind。没暴露的话,注册时 cast:
58
+
59
+ ```ts
60
+ framework.actionDispatcher.register("confirm" as any, handleConfirm as any);
61
+ ```
62
+
63
+ 运行时不在乎 —— `kind` dispatch 时就是普通字符串。
64
+
65
+ ## 步骤 3:写 handler
66
+
67
+ ```ts
68
+ // src/lib/actions/confirm.ts
69
+ import type { Framework } from "@finesoft/front";
70
+
71
+ export function registerConfirmHandler(framework: Framework): void {
72
+ framework.actionDispatcher.register("confirm", async (action: ConfirmAction) => {
73
+ if (typeof window === "undefined") {
74
+ // SSR:没法弹确认 —— 直接 dispatch 内层 action
75
+ await framework.actionDispatcher.dispatch(action.then);
76
+ return;
77
+ }
78
+
79
+ const confirmed = window.confirm(action.message);
80
+ if (!confirmed) return;
81
+
82
+ await framework.actionDispatcher.dispatch(action.then);
83
+ });
84
+ }
85
+ ```
86
+
87
+ 要点:
88
+
89
+ - handler 服务端和客户端都跑。服务端没有 `window` —— 决定「没 UI」对你的 action 意味着什么。
90
+ - 递归 dispatch(`actionDispatcher.dispatch(action.then)`)走普通管线,包含任何其他自定义 handler。
91
+ - 框架已经用递归深度限制(默认 4)保护 compound action。你的 handler 通过 dispatch 到达,继承了这个限制。
92
+
93
+ ## 步骤 4:应用启动时注册
94
+
95
+ ```ts
96
+ // src/main.ts
97
+ import { startBrowserApp } from "@finesoft/front/browser";
98
+ import { bootstrap } from "./bootstrap";
99
+ import { registerConfirmHandler } from "./lib/actions/confirm";
100
+
101
+ startBrowserApp({
102
+ bootstrap,
103
+ onBeforeStart(framework) {
104
+ registerConfirmHandler(framework);
105
+ },
106
+ mount: /* ... */,
107
+ });
108
+ ```
109
+
110
+ SSR 端镜像一份:
111
+
112
+ ```ts
113
+ // src/ssr.ts
114
+ export const render = createSSRRender({
115
+ bootstrap,
116
+ onBeforeStart(framework) {
117
+ registerConfirmHandler(framework);
118
+ },
119
+ async renderApp(page) {
120
+ /* ... */
121
+ },
122
+ });
123
+ ```
124
+
125
+ 或者更简单:在 `bootstrap()` 内注册,两端都自动拿到。
126
+
127
+ ## 步骤 5:使用
128
+
129
+ ```ts
130
+ // 在 view 组件里
131
+ import { makeConfirmAction, makeFlowAction } from "@finesoft/front";
132
+
133
+ function onDelete(id: string) {
134
+ framework.actionDispatcher.dispatch(
135
+ makeConfirmAction(`Delete item ${id}?`, makeFlowAction(`/items/${id}/deleted`)),
136
+ );
137
+ }
138
+ ```
139
+
140
+ ## 替换已有 handler
141
+
142
+ 每个 `kind` 只能注册一次。dispatcher 对重复注册打 warning 并跳过:
143
+
144
+ ```ts
145
+ framework.actionDispatcher.register("flow", myFlowHandler);
146
+ // [ActionDispatcher] kind="flow" already registered, skipping
147
+ ```
148
+
149
+ 要替换先 unregister:
150
+
151
+ ```ts
152
+ framework.actionDispatcher.removeAction("flow");
153
+ framework.actionDispatcher.register("flow", myFlowHandler);
154
+ ```
155
+
156
+ 适用于想用日志或分析包默认 flow handler:
157
+
158
+ ```ts
159
+ import { registerFlowActionHandler, type FlowActionDependencies } from "@finesoft/front";
160
+
161
+ const baseHandler = framework.actionDispatcher.getHandler("flow"); // 假设暴露
162
+ framework.actionDispatcher.removeAction("flow");
163
+ framework.actionDispatcher.register("flow", async (action) => {
164
+ console.log("[nav]", action.url);
165
+ await baseHandler(action);
166
+ });
167
+ ```
168
+
169
+ 实践中,导航的横切关注点优先用中间件(`beforeLoad`)—— 替换 flow handler 太侵入。
170
+
171
+ ## 自定义 kind 的 compound action
172
+
173
+ `CompoundAction` 配任何已注册 kind 都行:
174
+
175
+ ```ts
176
+ framework.actionDispatcher.dispatch({
177
+ kind: "compound",
178
+ actions: [
179
+ makeFlowAction("/checkout/complete"),
180
+ makeConfirmAction("Add to email list?", { kind: "subscribe", email: user.email }),
181
+ ],
182
+ });
183
+ ```
184
+
185
+ 每个内层 action 顺序跑。一个 handler 抛错短路 compound 剩余 action —— 想要 best-effort 语义就 `try/catch` 包起来。
186
+
187
+ ## 服务端考虑
188
+
189
+ Controller dispatch action 时 action handler 在 SSR 期间在服务端跑。常见模式:
190
+
191
+ - **外部 URL**:服务端没法让用户导航 —— 大多数应用提前返回。框架内置 `external-url` handler 在 SSR 上正是这样做的。
192
+ - **Confirm 类**:没用户能问。要么自动接受(用内层 action)要么自动拒绝(丢弃)。
193
+ - **仅遥测**:两端工作一样。记录就行。
194
+
195
+ handler 依赖服务端没有的浏览器 API,用 `typeof window === "undefined"` 守卫。
196
+
197
+ ## 测试
198
+
199
+ ```ts
200
+ import { afterEach, describe, expect, test, vi } from "vite-plus/test";
201
+ import { Framework } from "@finesoft/front";
202
+ import { registerConfirmHandler, makeConfirmAction } from "./confirm";
203
+
204
+ describe("confirm action", () => {
205
+ afterEach(() => vi.restoreAllMocks());
206
+
207
+ test("dispatches inner action when confirmed", async () => {
208
+ const framework = Framework.create({});
209
+ registerConfirmHandler(framework);
210
+ vi.stubGlobal("window", { confirm: () => true });
211
+
212
+ const innerHandler = vi.fn();
213
+ framework.actionDispatcher.register("test", innerHandler);
214
+
215
+ await framework.actionDispatcher.dispatch(
216
+ makeConfirmAction("ok?", { kind: "test" } as any),
217
+ );
218
+
219
+ expect(innerHandler).toHaveBeenCalled();
220
+ });
221
+
222
+ test("skips inner action when cancelled", async () => {
223
+ const framework = Framework.create({});
224
+ registerConfirmHandler(framework);
225
+ vi.stubGlobal("window", { confirm: () => false });
226
+
227
+ const innerHandler = vi.fn();
228
+ framework.actionDispatcher.register("test", innerHandler);
229
+
230
+ await framework.actionDispatcher.dispatch(
231
+ makeConfirmAction("ok?", { kind: "test" } as any),
232
+ );
233
+
234
+ expect(innerHandler).not.toHaveBeenCalled();
235
+ });
236
+ });
237
+ ```
238
+
239
+ ## 自定义 action vs 中间件 怎么选
240
+
241
+ | 关注点 | 自定义 action | 中间件(`beforeLoad`) |
242
+ | ------------------------------- | ------------- | ---------------------- |
243
+ | 导航到特定 URL 前确认 | ✅ | ❌(每个导航都跑) |
244
+ | 每次导航的审计日志 | ❌ | ✅ |
245
+ | 引入新的操作机制 | ✅ | ❌ |
246
+ | 把守所有导航到 admin 路由的访问 | ❌ | ✅ |
247
+
248
+ 自定义 action 是**新种类的操作**。中间件是**已有操作的横切关注点**。
@@ -0,0 +1,264 @@
1
+ # 高阶:自定义 adapter
2
+
3
+ 适配框架未内置的平台。内置 adapter 有 Node、Vercel、Cloudflare、Netlify、Static。其他 —— Deno Deploy、Bun、AWS Lambda、自建本地 —— 就是自定义 adapter。
4
+
5
+ 本配方端到端写一个。模式:构建期发出平台专属的入口文件,让该入口指向框架的 SSR + proxy 管线。
6
+
7
+ ## adapter 做什么
8
+
9
+ 构建期:
10
+
11
+ 1. 把 SSR 入口(`src/ssr.ts`)打包成单 JS 文件,依赖内联。
12
+ 2. 把客户端入口打成平台预期的形状(多数是 `dist/client/`)。
13
+ 3. 发出**平台专属入口**,做以下事:
14
+ - import SSR bundle
15
+ - 以平台原生形状(Request、Lambda event 等)接收请求
16
+ - 调 `createServer({ ssrEntry, proxies })` 并 serve 响应
17
+
18
+ 框架在 `packages/server/src/adapters/shared.ts` 提供 `buildBundle`、`generateSSREntry`、`copyStaticAssets`、`prerenderRoutes`。用它们 —— 它们一致地处理了所有 adapter 的重活。
19
+
20
+ ## 示例:Deno Deploy adapter
21
+
22
+ Deno Deploy 跑 ES 模块,Web 标准 Request/Response。工作流类似 Cloudflare Workers 但带原生 Deno API。
23
+
24
+ ### adapter 接口
25
+
26
+ ```ts
27
+ // src/lib/adapters/deno-deploy.ts
28
+ import type { AdapterDefinition, AdapterContext } from "@finesoft/front";
29
+ import { buildBundle, copyStaticAssets, generateSSREntry, prerenderRoutes } from "@finesoft/front";
30
+
31
+ export const denoDeployAdapter: AdapterDefinition = {
32
+ name: "deno-deploy",
33
+
34
+ async build(ctx: AdapterContext): Promise<void> {
35
+ // 1. 打包 SSR
36
+ const ssrEntry = generateSSREntry(ctx, {
37
+ // Deno 支持原生 fetch / URL / Response,无需 shim
38
+ external: [],
39
+ });
40
+ await buildBundle(ctx, {
41
+ entry: ssrEntry,
42
+ outFile: "dist/server.js",
43
+ format: "esm",
44
+ });
45
+
46
+ // 2. 拷静态资源
47
+ copyStaticAssets(ctx, "dist/client", "dist/static");
48
+
49
+ // 3. 预渲染 prerender 路由
50
+ await prerenderRoutes(ctx);
51
+
52
+ // 4. 发出 Deno 入口
53
+ writeEntryFile(
54
+ ctx,
55
+ "dist/main.ts",
56
+ `
57
+ import { createServer } from "./server.js";
58
+ const app = createServer({
59
+ ssrEntry: "./server.js",
60
+ staticDir: "./static",
61
+ });
62
+ Deno.serve(app.fetch);
63
+ `,
64
+ );
65
+ },
66
+ };
67
+ ```
68
+
69
+ ### 注册
70
+
71
+ ```ts
72
+ // vite.config.ts
73
+ import { finesoftFrontViteConfig } from "@finesoft/front";
74
+ import { denoDeployAdapter } from "./src/lib/adapters/deno-deploy";
75
+
76
+ export default {
77
+ plugins: [
78
+ finesoftFrontViteConfig({
79
+ ssr: { entry: "src/ssr.ts" },
80
+ adapter: denoDeployAdapter,
81
+ }),
82
+ ],
83
+ };
84
+ ```
85
+
86
+ `adapter` 选项接受字符串(内置)或 `AdapterDefinition`(自定义)。
87
+
88
+ ## adapter context
89
+
90
+ `AdapterContext` 传给 `build()`,暴露:
91
+
92
+ ```ts
93
+ interface AdapterContext {
94
+ root: string; // 项目根的绝对路径
95
+ outDir: string; // dist 目录的绝对路径
96
+ ssrEntryPath: string; // src/ssr.ts 的解析后路径
97
+ routes: RouteDefinition[]; // 来自 bootstrap 的路由(用于 prerender)
98
+ proxies: ProxyRouteConfig[]; // 来自 finesoftFrontViteConfig 的 proxy 配置
99
+ isr: IsrConfig | null; // 启用时的 ISR 配置
100
+ env: Record<string, string>; // 构建期环境变量
101
+ }
102
+ ```
103
+
104
+ 通常不会用全部 —— `buildBundle` 和 `generateSSREntry` 拿它们需要的。
105
+
106
+ ## 常见模式
107
+
108
+ ### Edge 运行时(Workers / Deno / Bun)
109
+
110
+ 标准 Web API(Request、Response、fetch)。打 ESM,target `webworker`。大多数 edge 运行时接受 default-exported handler:
111
+
112
+ ```ts
113
+ export default {
114
+ async fetch(request, env) {
115
+ return app.fetch(request, env);
116
+ },
117
+ };
118
+ ```
119
+
120
+ `packages/server/src/adapters/cloudflare.ts` 是 Cloudflare 的标准参考。
121
+
122
+ ### Lambda 风格(AWS Lambda、GCF、Azure Functions)
123
+
124
+ 平台专属 event 形状。在 `Request` 之间转换:
125
+
126
+ ```ts
127
+ import { app } from "./server.js";
128
+
129
+ export const handler = async (event: APIGatewayProxyEventV2) => {
130
+ const request = lambdaEventToRequest(event);
131
+ const response = await app.fetch(request);
132
+ return responseToLambdaResult(response);
133
+ };
134
+ ```
135
+
136
+ 每个云的 SDK 都自带 event-to-request 转换的类型和 helper。直接复用,别重造。
137
+
138
+ ### 多进程服务器(Bun cluster、PM2)
139
+
140
+ Bun 和现代 Node 支持 `cluster` 风格多进程 serve 利用 CPU 并行:
141
+
142
+ ```ts
143
+ import { app } from "./server.js";
144
+ import { serve } from "@hono/node-server";
145
+
146
+ const port = parseInt(process.env.PORT ?? "3000", 10);
147
+ serve({ fetch: app.fetch, port });
148
+ ```
149
+
150
+ 每个进程独立。ISR 缓存按进程 —— 真正共享缓存要前置 CDN。
151
+
152
+ ## 静态(无服务器)
153
+
154
+ `adapter: "static"` 是最简单目标 —— 一切预渲染,请求时啥都不跑。
155
+
156
+ ```ts
157
+ export const staticAdapter: AdapterDefinition = {
158
+ name: "static",
159
+ async build(ctx) {
160
+ // 完全跳过 SSR bundle
161
+ await prerenderRoutes(ctx); // 每个路由都必须是 renderMode: "prerender"
162
+ copyStaticAssets(ctx, "dist/client", "dist/static");
163
+ // 无服务器入口 —— 只有静态文件
164
+ },
165
+ };
166
+ ```
167
+
168
+ 验证每个路由都能预渲染:
169
+
170
+ ```ts
171
+ if (!ctx.routes.every((r) => r.renderMode === "prerender")) {
172
+ throw new Error("Static adapter requires every route to be renderMode: 'prerender'");
173
+ }
174
+ ```
175
+
176
+ ## 自动检测扩展
177
+
178
+ 内置 `"auto"` adapter 按顺序查环境变量:
179
+
180
+ ```ts
181
+ function detectAdapter(env: Record<string, string>): string {
182
+ if (env.VERCEL === "1") return "vercel";
183
+ if (env.CF_PAGES === "1") return "cloudflare";
184
+ if (env.NETLIFY === "true") return "netlify";
185
+ return "node";
186
+ }
187
+ ```
188
+
189
+ 自定义 adapter 有已知环境标签的话,在项目 `vite.config.ts` 自己包一层自动检测:
190
+
191
+ ```ts
192
+ function pickAdapter() {
193
+ if (process.env.DENO_DEPLOYMENT_ID) return denoDeployAdapter;
194
+ return "node";
195
+ }
196
+
197
+ finesoftFrontViteConfig({
198
+ adapter: pickAdapter(),
199
+ });
200
+ ```
201
+
202
+ ## 测 adapter
203
+
204
+ 集成测试:跑构建,验证发出的入口:
205
+
206
+ ```ts
207
+ import { describe, test, expect } from "vite-plus/test";
208
+ import { build } from "vite";
209
+ import { denoDeployAdapter } from "./deno-deploy";
210
+
211
+ describe("denoDeployAdapter", () => {
212
+ test("emits a Deno-compatible entry", async () => {
213
+ await build({
214
+ root: "test/fixtures/basic",
215
+ plugins: [
216
+ finesoftFrontViteConfig({
217
+ ssr: { entry: "src/ssr.ts" },
218
+ adapter: denoDeployAdapter,
219
+ }),
220
+ ],
221
+ });
222
+
223
+ const entry = await readFile("test/fixtures/basic/dist/main.ts", "utf-8");
224
+ expect(entry).toContain("Deno.serve");
225
+ expect(entry).toContain("./server.js");
226
+ });
227
+ });
228
+ ```
229
+
230
+ 冒烟测试运行时:本地起平台真打 `/`。这能抓单元测试抓不到的平台怪癖(CORS、头归一化、body 解码)。
231
+
232
+ ## 坑
233
+
234
+ ### 别把 Node 内置模块打进 edge 运行时
235
+
236
+ `fs`、`path`、`http` 等在 Workers / Deno 不存在。`generateSSREntry` 接 `external` 列表 —— 设成平台不兼容的模块,让打包器在构建期出错而不是部署时请求崩。
237
+
238
+ ### `process.env` 每个平台不同
239
+
240
+ - Node、Vercel:`process.env.FOO`
241
+ - Cloudflare Workers:通过 `fetch()` 的 `env` 参注入秘密
242
+ - Deno:`Deno.env.get("FOO")`
243
+
244
+ 框架对声明的 proxy auth key 处理 `process.env`,但你自己的运行时 env 读取要包成平台感知的 helper。
245
+
246
+ ### 资源的文件系统访问
247
+
248
+ 你在请求时依赖读文件(罕见;多数通过 `staticDir` serve),只有 Node 类 adapter 有原生 fs 访问。edge 运行时要把资源嵌入 bundle 或通过 KV 存代理。
249
+
250
+ ## 上游贡献
251
+
252
+ 自定义 adapter 针对的是流行平台但框架未内置,考虑开 PR。adapter 住 `packages/server/src/adapters/`,结构一致 —— `cloudflare.ts` 是最干净参考。
253
+
254
+ 框架 adapter API 有意保持小。贡献保持最小:
255
+
256
+ - `adapters/` 里一个文件
257
+ - `auto.ts` 里一个条目用于自动检测(若适用)
258
+ - 本文档里一段
259
+
260
+ ## 参考
261
+
262
+ - 内置 adapter:`packages/server/src/adapters/`
263
+ - 你会用的共享 helper:`packages/server/src/adapters/shared.ts`
264
+ - [第 9 章:服务器与部署](../09-server-and-deployment.md) —— adapter 包的是什么