7.7 KiB
Implementation Plan: Zod 重构数据模型
[Overview]
将 src/data/models/ 下 28 个数据类从「手写 readonly + 手动 ?? 兜底 + fromJson 守卫」模式重构为「Zod schema 驱动 + 类包装」模式,样板代码量减少约 70%,同时获得运行时验证能力。
[Types]
Schema 类型(每个数据类三个导出)
每个数据类使用同一份 Zod schema 同时驱动验证、类型推导、默认值兜底:
| 导出 | 来源 | 用途 |
|---|---|---|
XxxSchema |
z.object({...}) |
验证规则与默认值定义 |
XxxInput |
z.input<typeof XxxSchema> |
构造输入类型(字段可缺失,触发 .default()) |
XxxData |
z.output<typeof XxxSchema> |
解析后类型(所有字段已应用默认值) |
class Xxx |
手写 + Object.assign |
不可变类,保留自定义方法 |
Zod 字段类型映射
| Dart 字段 | Zod schema |
|---|---|
required String id |
z.string() |
@Default('web') String platform |
z.string().default('web') |
String? email |
z.string().default('') |
@Default(0) int intimacy |
z.number().default(0) |
bool? isGuest |
z.boolean().default(false) |
required List<T> items |
z.array(TItemSchema) |
PersonalityTraits? personalityTraits |
PersonalityTraitsSchema.default({}) |
Map<String, dynamic> |
z.record(z.string(), z.unknown()) |
28 个类清单
user/ (8): PersonalityTraits, User, CreditsData, CreditsHistoryData, AvatarData, UpdateProfileRequest, RecentMemory, UserStatsResponse
auth/ 请求 (9): SendCodeRequest, RegisterRequest, GoogleLoginRequest, FacebookLoginRequest, GuestLoginRequest, LoginRequest, AppleLoginRequest, RefreshTokenRequest, FbIdLoginRequest
auth/ 响应 (4): LoginResponse, GuestLoginResponse, LogoutResponse, RefreshTokenResponse
chat/ (10): ChatMessage, ChatSendResponse, ChatHistoryResponse, ChatSyncRequest, ChatSyncData, SyncMessage, GuestChatQuota, ImageUploadResponse, SendMessageRequest, SttData
metrics/ (2): AppEvent, PwaEvent
[Files]
新增
| 路径 | 用途 |
|---|---|
implementation_plan.md |
本文档 |
修改
| 路径 | 修改 |
|---|---|
package.json |
添加 zod: ^3.23.0 依赖 |
src/data/models/**/*.ts |
28 个模型文件全部重构为 Zod 驱动 + 类包装模式 |
src/data/models/index.ts |
不变(仍然 export * from "./...") |
src/data/models/auth/index.ts |
不变 |
src/data/models/chat/index.ts |
不变 |
src/data/models/metrics/index.ts |
不变 |
src/data/models/user/index.ts |
不变 |
pnpm-lock.yaml |
pnpm install 自动更新 |
删除
无(保留原文件结构,重写内容)
[Functions]
移除的样板函数(每个类)
| 函数 | 位置 | 移除原因 |
|---|---|---|
requireString 局部辅助 |
每个 fromJson |
由 Zod schema 验证替代 |
requireNumber 局部辅助 |
每个 fromJson |
由 Zod schema 验证替代 |
构造函数中 params.x ?? defaultX ×N |
每个类 | 由 .default() schema 替代 |
保留/调整的方法
| 方法 | 签名 | 变化 |
|---|---|---|
static fromJson(json) |
(json: unknown) => Xxx |
接收 unknown(更安全),内部 Schema.parse(json) |
toJson() |
() => XxxData |
返回 Schema.parse(this),保证输出一致性 |
静态常量(GuestChatQuota) |
static readonly X |
不变 |
静态方法(GuestChatQuota.threshold 等) |
static get X() |
不变 |
实例方法(GuestChatQuota.needsReset) |
(todayString: string) => boolean |
不变 |
[Classes]
重构模式(统一范式)
import { z } from "zod";
// 1. Schema(单一数据源)
export const XxxSchema = z.object({
field1: z.string(),
field2: z.number().default(0),
// ...
});
export type XxxInput = z.input<typeof XxxSchema>;
export type XxxData = z.output<typeof XxxSchema>;
// 2. 类(包装 + 不可变 + 自定义方法)
export class Xxx {
// 仅类型声明,无运行时初始化
declare readonly field1: string;
declare readonly field2: number;
// ...
private constructor(data: XxxData) {
Object.assign(this, data); // 运行时填值
Object.freeze(this); // 不可变
}
static from(input: XxxInput): Xxx {
return new Xxx(XxxSchema.parse(input));
}
static fromJson(json: unknown): Xxx {
return Xxx.from(json as XxxInput);
}
toJson(): XxxData {
return XxxSchema.parse(this);
}
}
28 个类的具体调整
| 类 | 特殊处理 |
|---|---|
PersonalityTraits |
嵌套引用基类,5 个 double 字段各有 .default(0.5) |
User |
引用 PersonalityTraitsSchema.default({}) |
UserStatsResponse |
引用 PersonalityTraitsSchema + RecentMemory 数组 |
LoginResponse |
引用 User |
GuestLoginResponse |
引用 User(默认值 new User({id:"",username:""}) 仍需保留逻辑) |
ChatHistoryResponse |
数组字段 z.array(ChatMessageSchema) |
ChatSyncRequest |
数组字段 z.array(SyncMessageSchema) |
CreditsHistoryData |
records: z.array(z.record(z.string(), z.unknown())) |
GuestChatQuota |
保留所有静态常量/方法/实例方法 |
| 其他 19 个 | 简单结构,直接映射 |
[Dependencies]
新增
| 包 | 版本 | 用途 |
|---|---|---|
zod |
^3.23.0 |
模式验证与类型推导 |
安装命令
pnpm add zod
版本选择
选择 zod v3(最新稳定版),原因:
- 文档完善,社区生态成熟
- 与 TypeScript 5 完全兼容
- 体积适中(按需 tree-shake 后约 10-15KB)
- v4 仍在 beta,暂不采用
[Testing]
验证策略
每个批次完成后:
npx tsc --noEmit— 0 错误pnpm lint— 通过
全部完成后进行端到端测试:
- 构造最小输入(仅 required 字段)→
Model.fromJson({...})→ 验证默认值已应用 - 构造完整输入 →
Model.fromJson({...})→.toJson()→ 验证往返一致性 - 构造无效输入(缺失 required 字段)→
Model.fromJson({...})→ 验证抛出ZodError
端到端测试脚本(临时)
实施完成后在 src/_test_zod.ts 写入以下验证代码(完成后删除):
import { User, ChatMessage, AppEvent } from "@/data/models";
import { z } from "zod";
// 1. 最小输入 + 默认值
const u1 = User.fromJson({ id: "u1", username: "alice" });
console.assert(u1.email === "");
console.assert(u1.platform === "web");
console.assert(u1.intimacy === 0);
console.assert(u1.personalityTraits.cheerful === 0.5);
// 2. 完整输入 + 往返
const original = { id: "u1", username: "alice", email: "a@b.com", intimacy: 10 };
const u2 = User.fromJson(original);
const json = u2.toJson();
console.assert(JSON.stringify(json) === JSON.stringify({...original, platform: "web", dolBalance: 0, ...}));
// 3. 无效输入抛错
try {
User.fromJson({ id: "u1" }); // 缺 username
console.assert(false, "应该抛错");
} catch (e) {
console.assert(e instanceof z.ZodError);
}
[Implementation Order]
按依赖关系倒序执行,5 个批次:
- 安装依赖:
pnpm add zod(修改package.json+pnpm-lock.yaml) - 批次 A:
user/(8 文件)PersonalityTraits(最底层,被User引用)User(引用PersonalityTraits)RecentMemory(被UserStatsResponse引用)UserStatsResponse(引用PersonalityTraits+RecentMemory)CreditsData、CreditsHistoryData、AvatarData、UpdateProfileRequest
- 批次 B:
auth/请求 (9 文件) — 独立 - 批次 C:
auth/响应 (4 文件) — 依赖User - 批次 D:
chat/(10 文件) — 内部ChatMessage引用 - 批次 E:
metrics/(2 文件) — 独立 - 最终验证:
tsc --noEmit+lint+ 端到端测试