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。以下几项值得借鉴:

  1. 向后兼容的 IDidstring | number 的联合类型,这样旧的存储数据就不会炸掉。
  2. 严格的时间戳验证timestamp 必须是可解析的日期字符串;否则视为无效。没有“尽力而为”的猜测。
  3. 可选部分的默认值 – 许多嵌套对象使用 .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

支持项目

  • 赞助构建
  • 为仓库加星
Back to Blog

相关文章

阅读更多 »