ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

前端运行时数据校验实战指南:用 Zod / Valibot 守护 API 响应、表单、localStorage 与环境变量的信任边界

前端运行时数据校验实战指南:用 Zod / Valibot 守护 API 响应、表单、localStorage 与环境变量的信任边界 【免费下载链接】Front-End-Checklist The essential checklist for modern web development, for humans and AI agents项目地址https://gitcode.com/gh_mirrors/fr/Front-End-Checklist点击查看免费下载本文基于 Front-End-Checklist 仓库中 runtime-validation 规则 及其配套 SKILL.md 编写。TypeScript 的类型在编译期即被擦除来自网络、用户输入与存储的数据抵达运行时是未经检查的裸值本文讲解如何用 Zod或体积更小的 Valibot在 API 边界、表单处理器、localStorage 读取与应用启动等信任边界处做运行时校验并给出可直接复用的完整代码示例以及本仓库应用层的真实落地证据。为什么需要运行时校验TypeScript 类型在运行时并不存在TypeScript 类型是编译期工具。当代码在浏览器或服务器上运行时所有类型注解都已消失——interface、type、泛型约束统统不存在。因此仅仅给外部数据声明类型并不能保护你的程序。// The TypeScript type — looks safe interface Product { id: string name: string price: number inStock: boolean } // ❌ Unsafe: we cast the response to our type without checking async function fetchProduct(id: string): Promise { const res await fetch(/api/products/${id}) const data await res.json() return data as Product // the API might return { id: number, price: 12.99 } } // TypeScript is happy — but this crashes at runtime: const product await fetchProduct(abc) const total product.price * 1.2 // NaN if price is actually the string 12.99上面的代码能通过全部编译检查却在运行时产生NaN、undefined is not a function等难以排查的错误。后端 Schema 变更、API 配置错误或恶意输入都可能产生与 TypeScript 类型不匹配的数据而编译器永远不会警告你。运行时校验的价值正是在你原本会调用JSON.parse()的同一位置提前捕获这些不匹配、给出清晰错误信息阻止类型不安全的数据在应用中扩散。在本仓库中这条规则被收录为正式规则条目 runtime-validation.mdx元数据将其归类为javascript分类下patterns子类优先级为high难度为intermediate预估耗时 30 分钟。它的aiContext明确描述了适用场景审查调用fetch()、读取localStorage、访问process.env、处理表单提交但未显式校验数据形状的代码时使用。API 响应校验用 Zod 在边界处定义单一事实来源正确的做法是用 Zod Schema 同时承担两件事校验运行时数据与推导 TypeScript 类型杜绝类型重复声明导致的漂移。// 1. Define the schema — single source of truth const ProductSchema z.object({ id: z.string(), name: z.string().min(1), price: z.number().positive(), inStock: z.boolean(), // Coerce types that the API might send differently createdAt: z.coerce.date(), }) // 2. Infer the TypeScript type from the schema — no duplication type Product z.infertypeof ProductSchema // 3. Validate at the boundary async function fetchProduct(id: string): Promise { const res await fetch(/api/products/${id}) if (!res.ok) { throw new Error(Failed to fetch product: ${res.status}) } const json: unknown await res.json() return ProductSchema.parse(json) // throws ZodError with clear message if invalid }要点拆解z.coerce.date()当 API 可能返回字符串形式的日期时Zod 会将其强制转换为Date对象。类似的 coercion 还有z.coerce.number()、z.coerce.boolean()适合处理类型不严格的第三方接口。z.infertypeof ProductSchema类型由 Schema 唯一推导修改 Schema 时类型自动同步不会出现手写interface与 Schema 分道扬镳的问题。先检查res.ok再parse网络层错误404、500与数据形状错误是两类问题分别处理错误信息才清晰。这正是本仓库 content-collections.ts 采用的核心模式ruleSchema定义了 150 行的 Zod Schema 来描述每个规则文档的元数据形状标题、分类、优先级、难度、来源、相关规则等包含z.enum(subcategoryValues)、z.array(z.string())、z.union、z.record等组合然后用defineCollection({ schema: ruleSchema })让内容集合在构建期就接受校验——内容层385 个 rule 文件本质上是仓库内部最大的外部数据源。safeParse不想抛异常时的优雅失败当校验失败不应中断程序例如列表页降级为空态时使用.safeParse()async function fetchProducts(): Promise { const res await fetch(/api/products) const json: unknown await res.json() const result z.array(ProductSchema).safeParse(json) if (!result.success) { // result.error is a ZodError with field-level detail console.error(API response shape mismatch:, result.error.flatten()) return null } return result.data // fully typed: Product[] }.parse()与.safeParse()的选择原则方法失败行为适用场景.parse()抛出ZodError数据缺失即系统故障应快速失败并交给错误边界.safeParse()返回{ success, data, error }联合类型期望处理失败路径如降级、重试、展示字段级错误safeParse的返回类型是一个判别联合discriminated unionsuccess: true时data字段携带完整推导类型success: false时error携带ZodError可通过.flatten()或.format()得到字段级错误详情直接喂给 UI。本仓库的 Server Actions 正是这一思路的工程化体现在 safe-action.ts 中所有动作共享一个基于next-safe-action的actionClient并配置了defaultValidationErrorsShape: flattened——即校验错误统一以扁平化字段错误结构返回给客户端。而在 checklist-actions.ts 中每个动作都用z.object定义入参 Schemaconst createChecklistSchema z.object({ name: z.string().trim().min(1).max(120), description: z.string().trim().max(500).optional(), ruleIds: z.array(z.string()).default([]) })trim()、min/max、default()这些约束在数据进入数据库之前就把脏数据挡在门外这正是在信任边界校验一次的 Server Action 侧体现。表单校验字段级错误直接驱动 UI表单是典型的外部数据入口——FormData中的每个值都是字符串与目标类型毫无关系const ContactFormSchema z.object({ name: z.string().min(2, Name must be at least 2 characters), email: z.string().email(Enter a valid email address), message: z.string().min(10).max(1000), // Enum validation subject: z.enum([support, billing, feedback]), // Optional with default newsletter: z.boolean().default(false), }) type ContactForm z.infertypeof ContactFormSchema function handleSubmit(formData: FormData): void { const raw Object.fromEntries(formData.entries()) const result ContactFormSchema.safeParse(raw) if (!result.success) { // Field-level errors for the UI const errors result.error.flatten().fieldErrors displayErrors(errors) return } submitContact(result.data) // result.data is typed as ContactForm }值得注意的细节自定义错误消息z.string().min(2, Name must be at least 2 characters)的第二个参数是面向用户的提示文案.flatten().fieldErrors得到的{ name: [...], email: [...] }结构可直接映射到各字段下方。z.enum将subject限制为三个合法值之一杜绝任意字符串进入业务逻辑。.default(false)缺失字段时静默使用默认值而非报错适合非必填但有默认值的场景。Object.fromEntries(formData.entries())把FormData转为普通对象后再校验是处理原生表单的标准桥接方式。环境变量校验在启动时失败而不是在使用时环境变量配置错误往往要等到代码运行数小时后才在某次调用中暴露。正确做法是在应用启动时一次性校验const EnvSchema z.object({ NODE_ENV: z.enum([development, test, production]), DATABASE_URL: z.string().url(), API_SECRET: z.string().min(32, API_SECRET must be at least 32 characters), PORT: z.coerce.number().int().positive().default(3000), FEATURE_NEW_DASHBOARD: z.enum([true, false]).transform(v v true).optional(), }) // Call once at application startup — throws if any required variable is missing // Usage — fully typed, no string indexing into process.env console.log(Listening on port ${env.PORT})z.string().url()DATABASE_URL若缺少://前缀启动即报错。z.coerce.number().int().positive().default(3000)process.env中一切都是字符串coerce完成字符串到数字的转换int/positive收紧取值范围default提供兜底。.transform(v v true)把true/false字符串转换为布尔值配合.optional()表达特性开关未配置时关闭的语义。校验后的env对象全程类型安全不再需要process.env.XXX的字符串索引和到处!非空断言。localStorage 校验对抗陈旧与损坏的本地数据localStorage中的数据可能来自旧版本应用、被用户手工编辑、或被其他脚本污染。读取时必须当作完全不可信的外部数据const ThemePreferenceSchema z.object({ mode: z.enum([light, dark, system]), fontSize: z.number().min(12).max(24).default(16), }) type ThemePreference z.infertypeof ThemePreferenceSchema function loadThemePreference(): ThemePreference { try { const stored localStorage.getItem(theme) if (!stored) return ThemePreferenceSchema.parse({}) // uses defaults return ThemePreferenceSchema.parse(JSON.parse(stored)) } catch { // Stored value was invalid JSON or wrong shape — use defaults localStorage.removeItem(theme) return ThemePreferenceSchema.parse({}) } }这个模式的精妙之处在于用异常驱动的默认值回退JSON.parse的语法错误、parse的形状错误都被同一个catch捕获然后清掉脏数据并用parse({})生成含默认值的干净对象。fontSize被限制在12–24区间防止极端值破坏布局。Valibot更小的替代方案Valibot 提供同样的校验模式但采用可摇树tree-shakeable的模块化 API只打包实际使用的校验器从而显著减小产物体积——这对注重包体积的前端项目很关键。const UserSchema object({ id: string(), email: string([email()]), name: string([minLength(1)]), age: number(), active: boolean(), }) type User typeof UserSchema._types.output const user parse(UserSchema, apiResponse)与 Zod 的对照关系string([minLength(1)])等价于z.string().min(1)string([email()])等价于z.string().email()类型推导通过_types.output完成。其 API 按需导入import { object, string, email, minLength, parse } from valibot每个校验器独立导出未使用的部分会被打包器摇掉。校验边界在哪里只在信任边界校验一次运行时校验的适用对象是信任边界trust boundaries——数据从外部世界进入应用的地方。不要在你自己控制的内部模块之间的每个函数调用上加 Zod Schema那只会增加噪音而不提升安全性。正确模式是外部数据API 响应 / 表单 / localStorage / 环境变量 │ ▼ 信任边界校验一次.parse() / .safeParse() │ ▼ 类型安全的内部流转typed values pass through your application本仓库的应用层对这个原则执行得很彻底Server Actions 边界checklist-actions.ts 中所有入参checklistIdSchema、createChecklistSchema、updateChecklistSchema在进入数据库操作前完成校验内容集合边界content-collections.ts 用ruleSchema对全部规则文档的 frontmatter 做构建期校验Action 元数据边界safe-action.ts 甚至对actionName这类内部元数据也定义了actionMetadataSchema配合handleServerError统一捕获异常并上报遥测。例外情况Exceptions框架默认行为或浏览器自身行为本身不构成例外只有具备文档化约束与补偿性控制compensating controls的场景才能抑制该项发现。当某个 JavaScript 模式看起来不安全、但数据已完全受限、经过校验且绝不来自攻击者控制时应当显式记录该边界而非将其视为隐式默认。如果某条规则与更强的利用路径或运行时故障重叠优先修复最能直接导致妥协compromise或用户可见故障的问题。验证清单Verification完成实现后按以下四项逐一核验全量扫描入口搜索代码库中每一个fetch()调用、JSON.parse()和localStorage.getItem()确认每个结果在数据被使用前都经过了 Zod.parse()或.safeParse()。类型单一来源确认外部数据的 TypeScript 类型均由z.infertypeof Schema或 Valibot 的_types.output推导而非单独手写——重复的类型定义会随演化逐渐漂移。环境变量收口确认process.env的访问全部经过校验后的env对象而非直接的字符串索引。故障演练用 mock server 返回故意损坏的 API 响应如{ id: 123, price: 12.99 }运行应用并确认 Schema 错误被优雅捕获处理而非以运行时崩溃形式传播。速查小结TypeScript 类型仅存在于编译期——运行时它们被完全擦除API 响应可能与声明的类型不一致且不产生任何编译错误一个 Zod Schema 同时完成运行时校验与TypeScript 类型推导只在信任边界API 层、表单处理器、localStorage 读取、应用启动校验一次然后让类型安全的值在应用内部自由流转需要抛错快速失败用.parse()需要优雅处理失败路径用.safeParse()。更完整的工程上下文可继续阅读仓库中的 runtime-validation 规则文档、SKILL.md 及其代码示例也可参考 safe-action.ts 与 content-collections.ts 中真实运行的 Zod Schema。赞分享【免费下载链接】Front-End-Checklist The essential checklist for modern web development, for humans and AI agents项目地址https://gitcode.com/gh_mirrors/fr/Front-End-Checklist点击查看免费下载相关推荐Front-End-Checklist 运行时数据校验实战用 Zod/Valibot 在信任边界守护 TypeScript 类型安全Front End Checklist 运行时数据校验实战用 Zod/Valibot 在信任边界守护 TypeScript 类型安全 运行时校验Runtimcreate-t3-app 环境变量完全指南基于 Zod 与 t3-oss/env-nextjs 的运行时与构建时校验实战create t3 app 环境变量完全指南基于 Zod 与 t3 oss/env nextjs 的运行时与构建时校验实战 Create T3 App 通过开发工具CLI代码生成Wasp 环境变量完全指南客户端与服务端配置、Zod 校验与实战部署Wasp 环境变量完全指南客户端与服务端配置、Zod 校验与实战部署 环境变量是 Wasp 应用区分开发、预发布与生产环境的关键机制同一份代码可以在本地连接Web框架后端前端CLI开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表