Zod + 防御性解析在本地优先应用中:让你的离线数据值得信赖
发布: (2026年2月6日 GMT+8 01:14)
4 min read
原文: Dev.to
Source: Dev.to
Offline‑first 改变了“输入验证”的含义
大多数应用只验证你刚提交的 HTML 表单。
本地优先(local‑first)应用有更多的输入来源:
- 持久化的状态块(重新水化)
- 来自旧版本的 IndexedDB 行
- 导入/恢复流程
- 意外漂移的测试夹具
如果把这些都当作“因为在本地所以可信”,最终会发布一个版本:
- 在某些用户的长期数据上崩溃,或
- 加载成功但悄悄误解字段。
Pain Tracker 给出了明确的划分:
- TypeScript 类型 是编译时的真相。
- Zod 模式 是运行时的真相。
项目在 src/types.ts 中明确了这一点:
- 重新导出
src/types/index.ts中的规范PainEntry接口。 - 重新导出
src/types/pain-entry.ts中的 Zod 模式。
(并且指出模式仅用于运行时验证。)
PainEntrySchema
该模式位于 src/types/pain-entry.ts。以下几项值得借鉴:
- 向后兼容的 ID –
id为string | number的联合类型,这样旧的存储数据就不会炸掉。 - 严格的时间戳验证 –
timestamp必须是可解析的日期字符串;否则视为无效。没有“尽力而为”的猜测。 - 可选部分的默认值 – 许多嵌套对象使用
.default(...),这样缺失的部分不会迫使每个调用者重建完整结构。
默认值并不是验证的替代品;它们是让“有效但不完整”的输入落入稳定、可预测形状的一种方式。
Pain Tracker 将以下两件事分开:
- “这是不是一个有效的
PainEntry形状?” - “这是不是一个有效的新条目?”
创建模式的写法如下:
// src/types/pain-entry.ts
import { z } from "zod";
export const PainEntrySchema = z.object({
id: z.union([z.string(), z.number()]),
timestamp: z.string().refine((s) => !isNaN(Date.parse(s)), {
message: "Invalid timestamp",
}),
// …other fields…
});
export const CreatePainEntrySchema = PainEntrySchema
.omit({ id: true, timestamp: true })
.superRefine((data, ctx) => {
if (!data.locations?.length) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "At least one location must be selected",
});
}
});
该规则直接在 src/types/pain-entry.test.ts 中进行测试。
验证模式
- 保持“形状”模式在迁移/导入时保持稳定。
- 为面向用户的创建路径使用更严格的模式。
- 在 UI 中使用
safeParse(温和的错误处理)。 - 在不变量、边界检查或测试中使用
parse。
UI 示例
// src/components/pain-tracker/PainEntryForm.tsx
import { CreatePainEntrySchema } from "../../types/pain-entry";
const result = CreatePainEntrySchema.safeParse(formData);
if (!result.success) {
// display the first issue message
}
导出的辅助函数
// src/types/pain-entry.ts
export const validatePainEntry = (data: unknown) => PainEntrySchema.parse(data);
export const safeParsePainEntry = (data: unknown) => PainEntrySchema.safeParse(data);
保持模式“单调”(未来的你会感谢你)
一些让 schema‑first 应用保持可维护的规则:
- 更倾向于显式字段,而不是“捕获所有”的对象。
- 使用
superRefine处理跨字段逻辑(例如,“必须至少选择一个位置”)。 - 添加规则时同步添加测试。
- 将运行时验证视为迁移策略的一部分,而不仅仅是表单 UX。
系列下一篇
- 第 5 部分 – 创伤知情的 UX + 可访问性作为架构
- 前一篇:第 3 部分 – 不会让你惊讶的 Service Worker
支持项目
- 赞助构建
- 为仓库加星