@finesoft/front 0.5.1 → 0.5.3
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-CR5vhgXg.mjs +1317 -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-BiRlUanX.d.mts +786 -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-2dSWO-Xw.d.mts +53 -0
- package/dist/proxy-z02VvGIj.mjs +7520 -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-BQBfaaPO.mjs +3825 -0
- package/dist/src-Ftl_0rhu.mjs +28 -0
- package/dist/ssr-BLzYP4wU.d.mts +207 -0
- package/dist/ssr-Tn4YkuxM.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-B1BT0N3t.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 +7 -332
- 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 +9 -155
- 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 +7 -332
- 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 +9 -155
- 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,264 +1,30 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 依赖与资源归属
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
1. **服务端请求隔离** —— 每个 SSR 请求拿到自己的 scope,请求级状态(鉴权、request id)不跨请求泄漏。
|
|
6
|
-
2. **解耦测试** —— Controller 从容器里 resolve 依赖,测试时可以任意 swap。
|
|
7
|
-
|
|
8
|
-
容器有意保持很小。没有装饰器、没有注解、没有自动装配。注册工厂,按 key resolve。
|
|
9
|
-
|
|
10
|
-
## 注册
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
import { Container } from "@finesoft/front";
|
|
14
|
-
|
|
15
|
-
const container = new Container();
|
|
16
|
-
|
|
17
|
-
container.register("userApi", () => new UserApi({ baseUrl: "/api" }));
|
|
18
|
-
container.register("logger", () => new ConsoleLogger());
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
工厂默认**每个容器调用一次**。结果被缓存。
|
|
22
|
-
|
|
23
|
-
要每次 resolve 都重新构造(非单例):
|
|
24
|
-
|
|
25
|
-
```ts
|
|
26
|
-
container.register("requestId", () => crypto.randomUUID(), false);
|
|
27
|
-
container.resolve("requestId"); // 每次都是新 id
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Resolve
|
|
31
|
-
|
|
32
|
-
```ts
|
|
33
|
-
const api = container.resolve<UserApi>("userApi");
|
|
34
|
-
const logger = container.resolve<Logger>("logger");
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
泛型参数只对 TypeScript 生效 —— 运行时无类型检查。
|
|
38
|
-
|
|
39
|
-
Resolve 未注册的 key 会抛错:
|
|
40
|
-
|
|
41
|
-
```ts
|
|
42
|
-
container.resolve("missing"); // Error: Dependency "missing" not registered
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## Scope —— 核心特性
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
const requestScope = framework.container.createScope();
|
|
49
|
-
requestScope.register("currentUser", () => loadUserFromSession(request));
|
|
50
|
-
|
|
51
|
-
// scope 里没有的 key 回退到父容器:
|
|
52
|
-
requestScope.resolve("userApi"); // 父
|
|
53
|
-
requestScope.resolve("currentUser"); // scope
|
|
54
|
-
|
|
55
|
-
requestScope.dispose();
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
子 scope:
|
|
59
|
-
|
|
60
|
-
- **继承**所有父 key(resolve 时回退)
|
|
61
|
-
- **覆盖**任意 key(注册自己的工厂)
|
|
62
|
-
- `dispose()` 时**清理** —— 子 scope 递归 dispose;自己从父的 children 集合移除
|
|
63
|
-
|
|
64
|
-
框架自动按 SSR 请求创建请求级容器,传给守卫和 Controller(作为 `ctx.container` / `execute()` 的第二个参数)。普通请求处理不需要你手动创建 scope。
|
|
65
|
-
|
|
66
|
-
## 什么时候自己开 scope
|
|
67
|
-
|
|
68
|
-
- 多租户应用,每个租户有自己的配置 / API 客户端(详见 [advanced/multi-tenant-scopes](./advanced/multi-tenant-scopes.md))
|
|
69
|
-
- 长跑操作需要自己的短期依赖
|
|
70
|
-
- 测试时想在基础容器上叠加 override
|
|
71
|
-
|
|
72
|
-
## Dispose 和泄漏
|
|
3
|
+
在 app/runtime 声明类型化 provider。普通调用使用 runtime.execute,由它管理调用 scope。同调用中的 scope provider 共享初始化;runtime provider 生存至 runtime.dispose。transient 每次获取创建并登记清理。默认拥有创建值,传入值默认外部管理,除非 owned=true。异步初始化和释放按依赖顺序等待。runtime provider 不可捕获请求绑定。命名 Container 仍支持控制器依赖,新的异步服务使用 context.get(token)。
|
|
73
4
|
|
|
74
5
|
```ts
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
1. 递归 dispose 任何未完成的子 scope
|
|
83
|
-
2. 对工厂结果实现了 `destroy()` 的对象调用(logger、recorder)
|
|
84
|
-
3. 自己从父的 children 集合移除
|
|
85
|
-
4. 幂等 —— 调用两次安全
|
|
86
|
-
|
|
87
|
-
**不 dispose 一个 scope 会泄漏里面每一个缓存值。** 请求 scope 活过响应就会持有:
|
|
88
|
-
|
|
89
|
-
- HTTP 客户端(以及它们待发的请求状态)
|
|
90
|
-
- Logger / recorder
|
|
91
|
-
- Controller 在里面 resolve 过的任何东西
|
|
92
|
-
|
|
93
|
-
框架处理它创建的请求 scope 的 dispose。**你**自己创建的 scope 你自己 dispose。
|
|
94
|
-
|
|
95
|
-
忘记 dispose 的症状,见 [陷阱:container scope 泄漏](./pitfalls/container-scope-leak.md)。
|
|
96
|
-
|
|
97
|
-
## 标准 DI key
|
|
98
|
-
|
|
99
|
-
用 `DEP_KEYS` 常量而不是字符串字面量,能在类型检查时抓到拼写错误:
|
|
100
|
-
|
|
101
|
-
```ts
|
|
102
|
-
import { DEP_KEYS } from "@finesoft/front";
|
|
103
|
-
|
|
104
|
-
container.register(DEP_KEYS.LOGGER, () => new ConsoleLogger());
|
|
105
|
-
container.register(DEP_KEYS.EVENT_RECORDER, () => myRecorder);
|
|
106
|
-
|
|
107
|
-
const logger = container.resolve(DEP_KEYS.LOGGER);
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
常量列表:
|
|
111
|
-
|
|
112
|
-
| Key | 标准类型 | 用于 |
|
|
113
|
-
| ------------------------- | ------------------ | ------------------------------------- |
|
|
114
|
-
| `DEP_KEYS.LOGGER` | `Logger` | 框架日志 |
|
|
115
|
-
| `DEP_KEYS.LOGGER_FACTORY` | `LoggerFactory` | 分类 logger(`logger.scope("auth")`) |
|
|
116
|
-
| `DEP_KEYS.NET` | `Net` | 网络状态检查(离线 / 计费网络) |
|
|
117
|
-
| `DEP_KEYS.STORAGE` | `Storage` | KV 持久化(localStorage / 内存) |
|
|
118
|
-
| `DEP_KEYS.FEATURE_FLAGS` | `FeatureFlags` | Feature flag 读取 |
|
|
119
|
-
| `DEP_KEYS.METRICS` | `MetricsClient` | 计数器 / gauge / 计时 |
|
|
120
|
-
| `DEP_KEYS.FETCH` | `typeof fetch` | `HttpClient` 底层 fetch(可 mock) |
|
|
121
|
-
| `DEP_KEYS.EVENT_RECORDER` | `EventRecorder` | 结构化事件记录 |
|
|
122
|
-
| `DEP_KEYS.LOCALE` | `LocaleAttributes` | 解析出的 locale(lang + dir) |
|
|
123
|
-
| `DEP_KEYS.PLATFORM` | `PlatformInfo` | 检测到的 user-agent 平台信息 |
|
|
124
|
-
| `DEP_KEYS.TRANSLATOR` | `Translator` | 翻译函数 |
|
|
125
|
-
|
|
126
|
-
框架在 `Framework.create()` 期间为这些 key 注册默认实现。要覆盖就在 framework 创建后再注册:
|
|
127
|
-
|
|
128
|
-
```ts
|
|
129
|
-
const framework = Framework.create({
|
|
130
|
-
/* ... */
|
|
6
|
+
import { createToken, provide, defineApp, defineOperation, createRuntime } from "@finesoft/front";
|
|
7
|
+
const tenant = createToken<string>("tenant");
|
|
8
|
+
const who = defineOperation({
|
|
9
|
+
id: "who",
|
|
10
|
+
kind: "query",
|
|
11
|
+
handler: async (_: undefined, context) => context.get(tenant),
|
|
131
12
|
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
container.register("featureBucketing", () => new BucketingService());
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
想让自己的 key 有类型安全,定义自己的常量 map:
|
|
146
|
-
|
|
147
|
-
```ts
|
|
148
|
-
// src/lib/di-keys.ts
|
|
149
|
-
export const APP_KEYS = {
|
|
150
|
-
USER_API: "userApi",
|
|
151
|
-
SESSION: "session",
|
|
152
|
-
FEATURE_BUCKETING: "featureBucketing",
|
|
153
|
-
} as const;
|
|
154
|
-
|
|
155
|
-
// 使用
|
|
156
|
-
container.register(APP_KEYS.USER_API, () => new UserApi({ baseUrl: "/api" }));
|
|
157
|
-
const api = container.resolve<UserApi>(APP_KEYS.USER_API);
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
## 生命周期顺序
|
|
161
|
-
|
|
162
|
-
```
|
|
163
|
-
Framework.create({ ... })
|
|
164
|
-
│
|
|
165
|
-
▼
|
|
166
|
-
默认 DEP_KEYS 注册(logger、locale、platform、...)
|
|
167
|
-
│
|
|
168
|
-
▼
|
|
169
|
-
你的自定义注册(在 onBeforeStart 或 bootstrap 里)
|
|
170
|
-
│
|
|
171
|
-
▼
|
|
172
|
-
─── 每个请求 ──────────────────────────────────
|
|
173
|
-
framework.container.createScope() ← 请求 scope
|
|
174
|
-
│
|
|
175
|
-
▼
|
|
176
|
-
beforeLoad 守卫(ctx.container = scope)
|
|
177
|
-
│
|
|
178
|
-
▼
|
|
179
|
-
controller.execute(params, scope)
|
|
180
|
-
│
|
|
181
|
-
▼
|
|
182
|
-
afterLoad 守卫(ctx.container = scope)
|
|
183
|
-
│
|
|
184
|
-
▼
|
|
185
|
-
renderApp() / response
|
|
186
|
-
│
|
|
187
|
-
▼
|
|
188
|
-
scope.dispose() ← 框架清理
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
## 在测试中使用容器
|
|
192
|
-
|
|
193
|
-
按 scope 层注入 mock:
|
|
194
|
-
|
|
195
|
-
```ts
|
|
196
|
-
import { Framework } from "@finesoft/front";
|
|
197
|
-
|
|
198
|
-
const framework = Framework.create({
|
|
199
|
-
/* ... */
|
|
13
|
+
const app = defineApp({
|
|
14
|
+
id: "example",
|
|
15
|
+
operations: [who],
|
|
16
|
+
providers: [
|
|
17
|
+
provide({
|
|
18
|
+
token: tenant,
|
|
19
|
+
lifetime: "scope",
|
|
20
|
+
create: ({ bindings }) => String(bindings.tenant),
|
|
21
|
+
}),
|
|
22
|
+
],
|
|
200
23
|
});
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
testScope.dispose();
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
完整模式见 [工程实践 · 测试](./engineering/testing.md)。
|
|
211
|
-
|
|
212
|
-
## 反模式
|
|
213
|
-
|
|
214
|
-
### 别在模块顶层 resolve
|
|
215
|
-
|
|
216
|
-
```ts
|
|
217
|
-
// 不好 —— import 时跑,在 framework.create() 之前
|
|
218
|
-
const logger = container.resolve(DEP_KEYS.LOGGER);
|
|
219
|
-
export function log(msg: string) {
|
|
220
|
-
logger.info(msg);
|
|
221
|
-
}
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
这里能拿到的 `container` 不是请求 scope;你拿到的是父容器,丢失请求隔离。
|
|
225
|
-
|
|
226
|
-
正确做法是在能访问 scope 的函数里 resolve:
|
|
227
|
-
|
|
228
|
-
```ts
|
|
229
|
-
export function logFromController(container: Container, msg: string) {
|
|
230
|
-
container.resolve<Logger>(DEP_KEYS.LOGGER).info(msg);
|
|
24
|
+
const runtime = createRuntime({ app });
|
|
25
|
+
try {
|
|
26
|
+
await runtime.execute(who, undefined, { bindings: { tenant: "one" } });
|
|
27
|
+
} finally {
|
|
28
|
+
await runtime.dispose();
|
|
231
29
|
}
|
|
232
30
|
```
|
|
233
|
-
|
|
234
|
-
### dispose 后别留 scoped 实例的引用
|
|
235
|
-
|
|
236
|
-
```ts
|
|
237
|
-
// 不好
|
|
238
|
-
let api: UserApi;
|
|
239
|
-
beforeLoad: (ctx) => {
|
|
240
|
-
api = ctx.container.resolve("userApi");
|
|
241
|
-
return next();
|
|
242
|
-
};
|
|
243
|
-
// `api` 现在指向一个 scope 已 dispose 的实例
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
如果你需要跨请求共享状态,注册到父容器,不是请求 scope。
|
|
247
|
-
|
|
248
|
-
### 别在守卫里注册
|
|
249
|
-
|
|
250
|
-
```ts
|
|
251
|
-
// 不好 —— 每个请求都跑
|
|
252
|
-
beforeLoad: (ctx) => {
|
|
253
|
-
ctx.container.register("userApi", () => new UserApi(/*...*/));
|
|
254
|
-
return next();
|
|
255
|
-
};
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
每次请求都新建一个工厂闭包。在 framework 启动时注册一次;scope 会继承。
|
|
259
|
-
|
|
260
|
-
## 下一步
|
|
261
|
-
|
|
262
|
-
- [可观测性](./08-observability.md) —— 通过 DI 接 Logger / EventRecorder / ReportCallback
|
|
263
|
-
- [工程实践:测试](./engineering/testing.md) —— 用 scope 隔离测试
|
|
264
|
-
- [陷阱:container scope 泄漏](./pitfalls/container-scope-leak.md) —— 忘记 dispose 时会发生什么
|
|
@@ -1,287 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 可观测性
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
- **`Logger`** —— 行式(debug/info/warn/error)
|
|
6
|
-
- **`EventRecorder`** —— 结构化事件记录(`{ name, fields }`)
|
|
7
|
-
- **`ReportCallback`** —— 把 `warn`/`error` 日志 fan-out 到外部监控
|
|
8
|
-
|
|
9
|
-
外加通过 `IntersectionImpressionObserver` 做 impression 追踪。
|
|
10
|
-
|
|
11
|
-
## Logger
|
|
12
|
-
|
|
13
|
-
基础的 `ConsoleLogger` 把日志写到 `console.{debug,info,warn,error}`,带分类前缀。Scope 化 logger 加嵌套前缀。
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
import { ConsoleLogger } from "@finesoft/front";
|
|
17
|
-
|
|
18
|
-
const logger = new ConsoleLogger("app");
|
|
19
|
-
logger.info("hello"); // [app] hello
|
|
20
|
-
|
|
21
|
-
const auth = logger.scope("auth");
|
|
22
|
-
auth.warn("token expired"); // [app:auth] token expired
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
级别:`debug`、`info`、`warn`、`error`。框架对内部失败(守卫失败、dispatch 错误)会在自己的 logger 上调 `warn`/`error`。
|
|
26
|
-
|
|
27
|
-
### 替换框架 logger
|
|
3
|
+
可移植结构化事件使用 runtime recorder。Web 日志配置放在 app.configuration,不为注册诊断再创建框架实例。ReportingLoggerFactory 可通过业务回调转发警告/错误。公开诊断不能带私有输入、凭据或任意上游错误。浏览器曝光观察使用浏览器入口。
|
|
28
4
|
|
|
29
5
|
```ts
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
create(category) {
|
|
35
|
-
return myLoggerImpl.scope(category);
|
|
36
|
-
},
|
|
37
|
-
}));
|
|
6
|
+
import { ConsoleEventRecorder, createRuntime } from "@finesoft/front";
|
|
7
|
+
const runtime = createRuntime({ app, recorder: new ConsoleEventRecorder() });
|
|
8
|
+
// In an operation: context.record("checkout.completed", { itemCount: 2 });
|
|
9
|
+
// In defineWebApp: configuration: { eventRecorder: recorder, reportCallback }
|
|
38
10
|
```
|
|
39
|
-
|
|
40
|
-
## `ReportCallback` —— 把日志转发到监控
|
|
41
|
-
|
|
42
|
-
把 `warn`/`error` 日志送到 Sentry / Datadog / 自家收集器最干净的方式:
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
import { Framework, type ReportCallback } from "@finesoft/front";
|
|
46
|
-
|
|
47
|
-
const reportCallback: ReportCallback = (level, category, args) => {
|
|
48
|
-
Sentry.captureMessage(`[${category}] ${args.join(" ")}`, level);
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const framework = Framework.create({
|
|
52
|
-
reportCallback,
|
|
53
|
-
// ...
|
|
54
|
-
});
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
工作原理:
|
|
58
|
-
|
|
59
|
-
- `Framework.create({ reportCallback })` 在 `ConsoleLogger` 上装 `ReportingLoggerFactory`
|
|
60
|
-
- 所有 `logger.warn(...)` 和 `logger.error(...)` 调用都触发回调**并且**继续走 console
|
|
61
|
-
- `debug` 和 `info` 不转发(避免在量里淹没信号)
|
|
62
|
-
- 回调在 `try/catch` 里跑 —— reporter 失败不会让框架崩
|
|
63
|
-
|
|
64
|
-
### 分类
|
|
65
|
-
|
|
66
|
-
每个 `logger.scope("auth")` 变成传给回调的 category。用它们在监控系统里做路由:
|
|
67
|
-
|
|
68
|
-
```ts
|
|
69
|
-
reportCallback(level, category, args) {
|
|
70
|
-
if (category.startsWith("auth")) {
|
|
71
|
-
sentry.captureMessage(/* 送到 auth 项目 */);
|
|
72
|
-
} else {
|
|
73
|
-
sentry.captureMessage(/* 默认项目 */);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
### 反爆量
|
|
79
|
-
|
|
80
|
-
`ReportCallback` 每次都跑。如果你有热循环重复打同一条告警一千次,你会发出一千个 event。在回调里加去重:
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
const seen = new Map<string, number>();
|
|
84
|
-
const reportCallback: ReportCallback = (level, category, args) => {
|
|
85
|
-
const key = `${category}:${args[0]}`;
|
|
86
|
-
const now = Date.now();
|
|
87
|
-
const last = seen.get(key) ?? 0;
|
|
88
|
-
if (now - last < 60_000) return; // 每个 (category, message) 每分钟最多 1 次
|
|
89
|
-
seen.set(key, now);
|
|
90
|
-
Sentry.captureMessage(/* ... */);
|
|
91
|
-
};
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## `EventRecorder` —— 结构化事件
|
|
95
|
-
|
|
96
|
-
要带字段的事件而不是自由文本日志时用 `EventRecorder`。
|
|
97
|
-
|
|
98
|
-
```ts
|
|
99
|
-
import { Framework, ConsoleEventRecorder, type EventRecorder } from "@finesoft/front";
|
|
100
|
-
|
|
101
|
-
const recorder: EventRecorder = new ConsoleEventRecorder();
|
|
102
|
-
const framework = Framework.create({ eventRecorder: recorder });
|
|
103
|
-
|
|
104
|
-
// 任意位置:
|
|
105
|
-
recorder.record({
|
|
106
|
-
name: "PageView",
|
|
107
|
-
fields: { url: "/products/42", referrer: "/search?q=widget" },
|
|
108
|
-
});
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### 内置事件
|
|
112
|
-
|
|
113
|
-
框架通过 `framework.didEnterPage(page)` 自动记录 `PageView`。每次导航成功(SSR + CSR)都触发,字段:`{ intentId, url, renderMode }`。
|
|
114
|
-
|
|
115
|
-
### 组合 recorder
|
|
116
|
-
|
|
117
|
-
`CompositeEventRecorder` 把事件分发给多个 sink:
|
|
118
|
-
|
|
119
|
-
```ts
|
|
120
|
-
import { ConsoleEventRecorder, CompositeEventRecorder } from "@finesoft/front";
|
|
121
|
-
|
|
122
|
-
const recorder = new CompositeEventRecorder([
|
|
123
|
-
new ConsoleEventRecorder(), // dev 可见性
|
|
124
|
-
productionAnalyticsRecorder, // 上送后端
|
|
125
|
-
]);
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
一个 recorder 抛错时其他仍然收到事件。
|
|
129
|
-
|
|
130
|
-
### 添加公共字段
|
|
131
|
-
|
|
132
|
-
`WithFieldsRecorder` 装饰另一个 recorder,给每个事件前置字段:
|
|
133
|
-
|
|
134
|
-
```ts
|
|
135
|
-
import { WithFieldsRecorder, ConsoleEventRecorder } from "@finesoft/front";
|
|
136
|
-
|
|
137
|
-
const recorder = new WithFieldsRecorder(new ConsoleEventRecorder(), [
|
|
138
|
-
{ getFields: () => ({ app: "myApp", version: "1.0.0" }) },
|
|
139
|
-
{ getFields: () => ({ userId: getCurrentUserId() }) },
|
|
140
|
-
]);
|
|
141
|
-
|
|
142
|
-
recorder.record({ name: "Click", fields: { id: "buy" } });
|
|
143
|
-
// → { app, version, userId, id }
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
适用于不想在每次 `record()` 调用里重复的 session 级字段。
|
|
147
|
-
|
|
148
|
-
### 写自己的 recorder
|
|
149
|
-
|
|
150
|
-
实现 `EventRecorder` 接口:
|
|
151
|
-
|
|
152
|
-
```ts
|
|
153
|
-
import type { EventRecorder, EventRecord } from "@finesoft/front";
|
|
154
|
-
|
|
155
|
-
class HttpEventRecorder implements EventRecorder {
|
|
156
|
-
constructor(private endpoint: string) {}
|
|
157
|
-
record(event: EventRecord): void {
|
|
158
|
-
// fire-and-forget;不要 await —— record() 不该阻塞 UI
|
|
159
|
-
navigator.sendBeacon(this.endpoint, JSON.stringify(event));
|
|
160
|
-
}
|
|
161
|
-
destroy(): void {
|
|
162
|
-
// flush、关闭连接等
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
批处理、重试、生命周期处理见 [高阶 · 自定义 event recorder](./advanced/custom-event-recorder.md)。
|
|
168
|
-
|
|
169
|
-
## Impression 追踪
|
|
170
|
-
|
|
171
|
-
通过 `IntersectionObserver` 跟踪元素进入视口:
|
|
172
|
-
|
|
173
|
-
```ts
|
|
174
|
-
import { IntersectionImpressionObserver, type EventRecorder } from "@finesoft/front";
|
|
175
|
-
|
|
176
|
-
const observer = new IntersectionImpressionObserver((entries) => {
|
|
177
|
-
for (const entry of entries) {
|
|
178
|
-
recorder.record({
|
|
179
|
-
name: "Impression",
|
|
180
|
-
fields: { id: entry.id, ...entry.metadata },
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// 元素挂载时绑定
|
|
186
|
-
observer.observe(productCardElement, "product-card-123", {
|
|
187
|
-
category: "featured",
|
|
188
|
-
position: 3,
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
// 元素卸载时解绑
|
|
192
|
-
observer.unobserve(productCardElement);
|
|
193
|
-
|
|
194
|
-
// 应用 teardown 时
|
|
195
|
-
observer.destroy();
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
observer 对每个元素每次可见性变化触发一次回调。它不会跨 observe/unobserve 循环去重 —— 你的回调决定是否再次记录。
|
|
199
|
-
|
|
200
|
-
## 通过 `fallback` 处理错误
|
|
201
|
-
|
|
202
|
-
Controller 错误会自动变成记录的事件:
|
|
203
|
-
|
|
204
|
-
```ts
|
|
205
|
-
class HomeController extends BaseController<{}, HomePage> {
|
|
206
|
-
readonly intentId = "home";
|
|
207
|
-
|
|
208
|
-
async execute(_params, container) {
|
|
209
|
-
const api = container.resolve<UserApi>("userApi");
|
|
210
|
-
return { kind: "home", users: await api.list() };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
fallback(_params, error) {
|
|
214
|
-
// 1. 框架通过自己的 logger 记录错误(→ reportCallback)
|
|
215
|
-
// 2. 你返回降级页面
|
|
216
|
-
return { kind: "home", users: [], degraded: true };
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
错误时的操作顺序:
|
|
222
|
-
|
|
223
|
-
1. `execute()` 抛错。
|
|
224
|
-
2. `BaseController` catch 之后调 `framework.logger.error("[controller:home]", error)`。
|
|
225
|
-
3. Logger 触发 `reportCallback("error", "controller:home", [error])` → Sentry。
|
|
226
|
-
4. `fallback()` 返回你设计的 `Page`。
|
|
227
|
-
5. 渲染继续,用 fallback 页面(默认 HTTP 200)。
|
|
228
|
-
|
|
229
|
-
要返回非 200 状态码,让 page 带「错误」标记,在 `afterLoad` 守卫里检查:
|
|
230
|
-
|
|
231
|
-
```ts
|
|
232
|
-
afterLoad: [
|
|
233
|
-
(ctx) => {
|
|
234
|
-
if ("degraded" in ctx.page && ctx.page.degraded) {
|
|
235
|
-
return deny(503, "Service degraded");
|
|
236
|
-
}
|
|
237
|
-
return next();
|
|
238
|
-
},
|
|
239
|
-
],
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
## 指标(计数器 / gauge / 计时)
|
|
243
|
-
|
|
244
|
-
框架的 `MetricsClient` 是最小的:
|
|
245
|
-
|
|
246
|
-
```ts
|
|
247
|
-
import { DEP_KEYS } from "@finesoft/front";
|
|
248
|
-
|
|
249
|
-
const metrics = framework.container.resolve(DEP_KEYS.METRICS);
|
|
250
|
-
|
|
251
|
-
metrics.increment("requests.total");
|
|
252
|
-
metrics.increment("login.failed", { reason: "wrong_password" });
|
|
253
|
-
metrics.gauge("queue.depth", 17);
|
|
254
|
-
metrics.timing("render.duration", 142);
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
默认实现是 no-op。注册自己的:
|
|
258
|
-
|
|
259
|
-
```ts
|
|
260
|
-
framework.container.register(DEP_KEYS.METRICS, () => new StatsdMetrics(/* ... */));
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
大多数应用 `EventRecorder` 就够 —— 你的分析后端已经把事件聚合成指标了。
|
|
264
|
-
|
|
265
|
-
## Trace ID 和请求关联
|
|
266
|
-
|
|
267
|
-
常见模式:给每个日志和事件打上请求级 trace id。
|
|
268
|
-
|
|
269
|
-
```ts
|
|
270
|
-
// beforeLoad 守卫
|
|
271
|
-
function traceIdGuard(ctx) {
|
|
272
|
-
const traceId = ctx.getHeader("x-request-id") ?? crypto.randomUUID();
|
|
273
|
-
ctx.container.register("traceId", () => traceId);
|
|
274
|
-
ctx.container.register(
|
|
275
|
-
DEP_KEYS.EVENT_RECORDER,
|
|
276
|
-
() => new WithFieldsRecorder(baseRecorder, [{ getFields: () => ({ traceId }) }]),
|
|
277
|
-
);
|
|
278
|
-
return next();
|
|
279
|
-
}
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
通过 `WithFieldsRecorder` 注册 scope 化的 recorder,让本请求里每个事件自动带 trace id。
|
|
283
|
-
|
|
284
|
-
## 下一步
|
|
285
|
-
|
|
286
|
-
- [服务器与部署](./09-server-and-deployment.md) —— 这些原语在 HTTP 层是怎么用的
|
|
287
|
-
- [高阶 · 自定义 event recorder](./advanced/custom-event-recorder.md) —— 生产级实现
|