下载工作台
前端框架精通

表单校验与复杂状态管理

试读上半部分 · 解锁后可读全文

第 12 章 · 表单校验与复杂状态管理

本章目标:掌握 Vue(VeeValidate / Element Plus rules)React(React Hook Form + Zod) 的表单校验体系;理解 Pinia / Zustand / Context 选型;实现多步骤表单、动态字段与异步校验;完成可上线的 「商品上架表单」 完整示例。

学时建议:4~5 小时(含 90 分钟跟练)

前置:完成本模块 ch01~ch03(Vue)或 ch04~ch05(React)、ch11(UI 组件库)。


12.1 为什么表单是后台项目的「深水区」

痛点原生 JS框架 + 校验库
字段联动手写 onchange 链式更新声明式 watch / useWatch
错误展示手动维护 errors 对象库自动绑定 meta.errors
提交防重复自己管 loading 标志isSubmitting 内置
复杂嵌套深层对象路径易错fieldArray / useFieldArray
行业案例 · 贤紫优选商城:商户上架一件 SKU 需填写基础信息、规格矩阵、运费模板、资质图片共 40+ 字段;2025 年后台从「提交后后端报错再改」迁移到「前端 schema 校验 + 异步 SKU 查重」,表单提交失败率从 23% 降至 4% 以下。

12.2 Vue:Element Plus 表单 rules

12.2.1 基础 rules 模式

<script setup lang="ts">
import { reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'

interface ProductForm {
  name: string
  price: number | null
  categoryId: string
}

const formRef = ref<FormInstance>()
const form = reactive<ProductForm>({ name: '', price: null, categoryId: '' })

const rules: FormRules<ProductForm> = {
  name: [
    { required: true, message: '请输入商品名称', trigger: 'blur' },
    { min: 2, max: 60, message: '长度 2~60 字', trigger: 'blur' },
  ],
  price: [
    { required: true, message: '请输入售价', trigger: 'blur' },
    {
      validator: (_r, v, cb) =>
        v != null && v > 0 ? cb() : cb(new Error('售价须大于 0')),
      trigger: 'blur',
    },
  ],
  categoryId: [{ required: true, message: '请选择类目', trigger: 'change' }],
}

async function onSubmit() {
  const valid = await formRef.value?.validate().catch(() => false)
  if (!valid) return
  // 调用 ch13 axios 封装提交
}
</script>

<template>
  <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
    <el-form-item label="商品名称" prop="name">
      <el-input v-model="form.name" />
    </el-form-item>
    <el-form-item label="售价" prop="price">
      <el-input-number v-model="form.price" :min="0" :precision="2" />
    </el-form-item>
    <el-form-item label="类目" prop="categoryId">
      <el-select v-model="form.categoryId" placeholder="请选择">
        <el-option label="数码" value="cat-digital" />
        <el-option label="家居" value="cat-home" />
      </el-select>
    </el-form-item>
    <el-button type="primary" @click="onSubmit">提交上架</el-button>
  </el-form>
</template>
trigger适用
blur文本、数字失焦校验
change下拉、开关、日期即时校验
自定义 validator跨字段、业务规则

12.2.2 动态 rules

类目为「食品」时追加「保质期」必填:

import { computed } from 'vue'

const rules = computed<FormRules>(() => ({
  ...baseRules,
  ...(form.categoryId === 'cat-food'
    ? { shelfLife: [{ required: true, message: '请填写保质期(天)' }] }
    : {}),
}))

模板中配合 v-if="form.categoryId === 'cat-food'" 渲染表单项,规则与 UI 同步出现


12.3 Vue:VeeValidate 4 + Zod(推荐 TS 项目)

npm install vee-validate @vee-validate/zod zod
// schemas/product.ts
import { z } from 'zod'

export const productStep1Schema = z.object({
  name: z.string().min(2).max(60),
  price: z.number().positive('售价须大于 0'),
  sku: z.string().regex(/^[A-Z0-9-]{6,20}$/, 'SKU 格式不正确'),
})

export type ProductStep1 = z.infer<typeof productStep1Schema>
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { productStep1Schema } from '@/schemas/product'

const { defineField, errors, handleSubmit, isSubmitting } = useForm({
  validationSchema: toTypedSchema(productStep1Schema),
})

const [name] = defineField('name')
const [price] = defineField('price')
const [sku] = defineField('sku')

const onSubmit = handleSubmit(async (values) => {
  await api.createProductDraft(values)
})
</script>
对比Element rulesVeeValidate + Zod
与 UI 库耦合强(el-form弱,可换 Ant Design Vue
Schema 复用前后端共享 Zod(ch13)
学习成本低(已在用 Element)中,适合 TS 团队

建议:贤紫后台已用 Element Plus 的页面继续 rules;新模块或需与后端共享校验时引入 VeeValidate。


12.4 React:React Hook Form + Zod

npm install react-hook-form @hookform/resolvers zod
// schemas/product.ts — 可与 Vue 共用同一份 zod
import { z } from 'zod'

export const productSchema = z.object({
  name: z.string().min(2).max(60),
  price: z.coerce.number().positive(),
  specs: z.array(z.object({ key: z.string(), value: z.string() })).min(1),
})

export type ProductFormValues = z.infer<typeof productSchema>
const { control, register, handleSubmit, formState: { errors, isSubmitting } } =
  useForm<ProductFormValues>({ resolver: zodResolver(productSchema) })
const { fields, append, remove } = useFieldArray({ control, name: 'specs' })
// Form.Item + register / Controller 绑定;append/remove 管理动态规格行

性能要点:RHF 默认非受控,大表单少重渲染;Ant Design 需 Controller 绑定 value/onChange


12.5 状态管理选型:Pinia vs Zustand vs Context

                    状态复杂度
                         ▲
                         │
     Pinia / Zustand ────┤  全局:用户、购物车、多步骤表单草稿
                         │
     Context ────────────┤  主题、locale、轻量共享(<5 字段)
                         │
     组件本地 state ─────┤  单页表单、弹窗开关
                         │
                         └──────────────────────► 跨组件深度
方案VueReact适用
组件内ref / reactiveuseState单表单、无跨页
Contextprovide/injectcreateContext主题、权限快照;避免大对象频繁更新
Pinia官方推荐用户会话、商品草稿、步骤进度
Zustand可用但少见轻量首选中等全局状态,无 Provider 嵌套

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。