[{"data":1,"prerenderedAt":244},["ShallowReactive",2],{"content:\u002Fposts\u002F2025\u002Fzod-use":3,"surround:\u002Fposts\u002F2025\u002Fzod-use":232},{"id":4,"title":5,"body":6,"canonical":210,"categories":211,"date":213,"description":214,"draft":215,"extension":216,"image":217,"meta":218,"navigation":219,"path":220,"permalink":210,"readingTime":221,"recommend":210,"references":210,"seo":226,"sitemap":227,"stem":228,"tags":229,"type":210,"updated":213,"__hash__":231},"content\u002Fposts\u002F2025\u002Fzod-use.md","Zod 基础用法",{"type":7,"value":8,"toc":192},"minimark",[9,14,18,21,24,27,31,42,45,49,57,60,66,69,72,78,81,87,90,93,99,102,105,111,114,120,123,126,132,136,142,150,153,157,164,173,176,180,183,189],[10,11],"link-card",{"link":12,"title":13},"https:\u002F\u002Fzod.dev\u002F","Zod 官网地址",[15,16,17],"p",{},"现在我们越来越多的在 UI 组件库里找到 Zod 的存在，Zod 是什么？我们为什么要用 Zod 呢？",[15,19,20],{},"根据其官网说法，zod 利用静态类型推断进行 TypeScript 优先模式验证。在 shadcn、nuxt ui 等组件库中，其主要是用来进行表单的校验；在一些 js 全栈项目里，它用来规定项目的数据结构。",[15,22,23],{},"我们就来简单的看一下它是怎么使用的。",[15,25,26],{},"这里我使用 pnpm 作为包管理工具，并使用 vue 项目举例。",[28,29,30],"h2",{"id":30},"安装",[32,33,39],"pre",{"className":34,"code":36,"language":37,"meta":38},[35],"language-shell","pnpm add zod\n","shell","",[40,41,36],"code",{"__ignoreMap":38},[28,43,44],{"id":44},"一些常用的内容",[46,47,48],"h3",{"id":48},"一些常用的字符串的验证",[32,50,55],{"className":51,"code":53,"language":54,"meta":38},[52],"language-ts","\u002F\u002F 验证\nz.string().max(5); \u002F\u002F 最大长度\nz.string().min(5); \u002F\u002F 最小长度\nz.string().length(5); \u002F\u002F 字符长度\nz.string().email();\nz.string().url();\nz.string().emoji();\nz.string().uuid();\nz.string().regex(regex); \u002F\u002F 使用正则校验\nz.string().includes(string);\nz.string().startsWith(string);\nz.string().endsWith(string);\nz.string().datetime(); \u002F\u002F ISO 8601；默认值为无 UTC 偏移，选项见下文\nz.string().ip(); \u002F\u002F 默认为 IPv4 和 IPv6，选项见下文\n\n\u002F\u002F 转变\nz.string().trim(); \u002F\u002F 减除空白\nz.string().toLowerCase(); \u002F\u002F 小写化\nz.string().toUpperCase(); \u002F\u002F 大写化\n","ts",[40,56,53],{"__ignoreMap":38},[15,58,59],{},"创建字符串模式时，你可以自定义一些常见的错误信息",[32,61,64],{"className":62,"code":63,"language":54,"meta":38},[52],"const name = z.string({\n    required_error: \"Name is required\",\n    invalid_type_error: \"Name must be a string\",\n});\n",[40,65,63],{"__ignoreMap":38},[46,67,68],{"id":68},"自定义错误消息",[15,70,71],{},"使用验证方法时，你可以传递一个附加参数，以提供自定义错误信息",[32,73,76],{"className":74,"code":75,"language":54,"meta":38},[52],"z.string().min(5, { message: \"Must be 5 or more characters long\" });\nz.string().max(5, { message: \"Must be 5 or fewer characters long\" });\nz.string().length(5, { message: \"Must be exactly 5 characters long\" });\nz.string().email({ message: \"Invalid email address\" });\nz.string().url({ message: \"Invalid url\" });\nz.string().emoji({ message: \"Contains non-emoji characters\" });\nz.string().uuid({ message: \"Invalid UUID\" });\nz.string().includes(\"tuna\", { message: \"Must include tuna\" });\nz.string().startsWith(\"https:\u002F\u002F\", { message: \"Must provide secure URL\" });\nz.string().endsWith(\".com\", { message: \"Only .com domains allowed\" });\nz.string().datetime({ message: \"Invalid datetime string! Must be UTC.\" });\nz.string().ip({ message: \"Invalid IP address\" });\n",[40,77,75],{"__ignoreMap":38},[46,79,80],{"id":80},"特定的数字验证",[32,82,85],{"className":83,"code":84,"language":54,"meta":38},[52],"z.number().gt(5); \u002F\u002F 最小为 5\nz.number().gte(5); \u002F\u002F alias .min(5)\nz.number().lt(5); \u002F\u002F 最大为 5\nz.number().lte(5); \u002F\u002F alias .max(5)\n\nz.number().int(); \u002F\u002F 必须是一个整数\n\nz.number().positive(); \u002F\u002F     > 0\nz.number().nonnegative(); \u002F\u002F  >= 0\nz.number().negative(); \u002F\u002F     \u003C 0\nz.number().nonpositive(); \u002F\u002F  \u003C= 0\n\nz.number().multipleOf(5); \u002F\u002F Evenly divisible by 5. Alias .step(5)\n\nz.number().finite(); \u002F\u002F value must be finite, not Infinity or -Infinity\nz.number().safe(); \u002F\u002F value must be between Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGE\n",[40,86,84],{"__ignoreMap":38},[15,88,89],{},"和字符串一样，也可以传递第二个参数来自定义错误消息",[46,91,92],{"id":92},"对象类型",[32,94,97],{"className":95,"code":96,"language":54,"meta":38},[52],"const userSchema = z.object({\n    username: z.string(),\n    password: z.string()\n        .min(5, { message: '密码最小为 5 位' })\n        .max(10, { message: '密码最大为 10 位' }),\n})\n\n\u002F\u002F 可以使用 z.infer来提取 schema 的 ts 类型\ntype User = z.infer\u003Ctypeof userSchema>\n\u002F\u002F 相当于 type User = {username: string, password: string}\n",[40,98,96],{"__ignoreMap":38},[15,100,101],{},"受 TypeScript 内置的Pick和Omit工具类型的启发，所有 Zod 对象模式都有.pick和 .omit方法，可以返回一个修改后的版本",[15,103,104],{},"要想只保留某些 Key，使用 .pick .",[32,106,109],{"className":107,"code":108,"language":54,"meta":38},[52],"const passwordSchema = userSchema.pick({ password: true });\ntype Password = z.infer\u003Ctypeof passwordSchema>; \u002F\u002F => type Password = {password: string}\n",[40,110,108],{"__ignoreMap":38},[15,112,113],{},"要删除某些 Key，请使用 .omit .",[32,115,118],{"className":116,"code":117,"language":54,"meta":38},[52],"const passwordSchema = userSchema.pick({ username: true });\ntype Password = z.infer\u003Ctypeof passwordSchema>; \u002F\u002F => type Password = {password: string}\n",[40,119,117],{"__ignoreMap":38},[46,121,122],{"id":122},"optional",[15,124,125],{},"你可以用z.optional()使任何模式成为可选:",[32,127,130],{"className":128,"code":129,"language":54,"meta":38},[52],"const userSchema = z.object({\n    username: z.string(),\n    password: z.string(),\n    age: z.number().optional()\n})\n",[40,131,129],{"__ignoreMap":38},[46,133,135],{"id":134},"可以使用-coerce-对原始类型进行强制转换","可以使用 coerce 对原始类型进行强制转换",[32,137,140],{"className":138,"code":139,"language":54,"meta":38},[52],"\u002F\u002F 对传入的 id 做校验并强制转换为数字类型\n\u002F\u002F \u002Fdetail\u002F:id\nconst paramsSchema = z.object({\n    id: z.coerce.number()\n})\nconst { id } = paramsSchema.parse(router)\n",[40,141,139],{"__ignoreMap":38},[143,144,145,148],"blockquote",{},[15,146,147],{},"还有很多其他的校验内容，可以去 zod 的官网或 github 仓库去查看",[10,149],{"link":12,"title":13},[28,151,152],{"id":152},"使用技巧",[46,154,156],{"id":155},"_1-在-vue-项目中校验-router-传递的参数","1. 在 vue 项目中校验 router 传递的参数",[15,158,159,160,163],{},"这里使用",[40,161,162],{"code":162},"safeParse","进行校验，校验失败后不会抛出错误，我们可以根据它返回的 success 是否为 true 来判断校验是否通过。并可以在 error 中得到校验失败的信息进行相应的处理。",[32,165,171],{"className":166,"code":168,"filename":169,"language":170,"meta":38},[167],"language-vue","\u003Cscript lang=\"ts\" setup>\nimport { useRoute } from \"vue-router\"\nimport { z } from \"zod\"\n\nconst route = useRoute()\n\nconst paramsSchema = z.object({\n    id: z.coerce.number({ message: \"id必须是一个数字\" }),\n})\nconst { success, data: params, error } = paramsSchema.safeParse(route.params)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n    \u003Cdiv>detail {{ params?.id }}\u003C\u002Fdiv>\n    \u003Cpre v-if=\"!success\">{{ error }}\u003C\u002Fpre>\n\u003C\u002Ftemplate>\n","detail.vue","vue",[40,172,168],{"__ignoreMap":38},[15,174,175],{},"同理，我们也可以校验它的 query 类型，此处便不再重复。",[46,177,179],{"id":178},"_2-传递数据时只传递指定的字段并对其进行校验","2. 传递数据时只传递指定的字段并对其进行校验",[15,181,182],{},"向接口传递数据时，我们有时会有一些多余的参数，此时我们可以构建一个 schema 来对传递的参数做校验，校验后获得的值为 schema 中规定的数据结构，可以帮助我们校验数据类型并且去除 schema 中未定义的多余参数",[32,184,187],{"className":185,"code":186,"language":54,"meta":38},[52],"import { z } from \"zod\"\n\nconst userSchema = z.object({\n    username: z.string(),\n    age: z.number(),\n})\n\nfunction submit() {\n    \u002F\u002F 这里模拟从别处拿到的值\n    const user = {\n        username: \"月空人\",\n        password: '123456',\n        age: 26,\n        id: 1,\n    }\n\n    console.log(\"submit\", userSchema.parse(user))\n    \u002F\u002F 这里打印的值是 submit {username: '月空人', age: 26}\n}\nsubmit()\n",[40,188,186],{"__ignoreMap":38},[15,190,191],{},"同理，如果我们是一个 node 的接口，我们也可以使用这个技巧来向前端传递指定 schema 类型的数据。",{"title":38,"searchDepth":193,"depth":193,"links":194},4,[195,197,206],{"id":30,"depth":196,"text":30},2,{"id":44,"depth":196,"text":44,"children":198},[199,201,202,203,204,205],{"id":48,"depth":200,"text":48},3,{"id":68,"depth":200,"text":68},{"id":80,"depth":200,"text":80},{"id":92,"depth":200,"text":92},{"id":122,"depth":200,"text":122},{"id":134,"depth":200,"text":135},{"id":152,"depth":196,"text":152,"children":207},[208,209],{"id":155,"depth":200,"text":156},{"id":178,"depth":200,"text":179},null,[212],"代码","2025-03-15 18:10:39","本文简单讨论一下 Zod 在项目中的实际使用",false,"md","https:\u002F\u002Fbitmc.uno\u002Fpicgo\u002Fzod-use.jpg",{},true,"\u002Fposts\u002F2025\u002Fzod-use",{"text":222,"minutes":223,"time":224,"words":225},"6 min read",5.71,342600,1142,{"title":5,"description":214},{"loc":220},"posts\u002F2025\u002Fzod-use",[230],"Zod","3apA-YvAG4m7pHbWMfTS42iAc-uJCyHGU1dhbUIiu20",[233,238],{"title":234,"path":235,"stem":236,"date":237,"type":210,"children":-1},"【翻译】如何使用 Vitest 在 Vue 中进行视觉回归测试？","\u002Fposts\u002F2025\u002Fhow-to-do-visual-regression-testing-in-vue-with-vitest","posts\u002F2025\u002Fhow-to-do-visual-regression-testing-in-vue-with-vitest","2025-03-02 10:10:47",{"title":239,"path":240,"stem":241,"date":242,"type":243,"children":-1},"逃离北京","\u002Fposts\u002F2025\u002Fescape-from-beijing","posts\u002F2025\u002Fescape-from-beijing","2025-03-17 20:16:05","story",1789642589860]