人人都会AI编程

16.1 原生表单双向绑定与校验实现

更新时间:2026-07-11

Vue 本身提供了足够的能力来应对大多数表单场景。用好 v-model 和内置的响应式 API,不需要额外引入第三方库就能实现数据双向绑定和基础校验。这一节从最常用的模式讲起,逐步过渡到完整的带校验表单。

v-model:双向绑定的“语法糖”

v-model 本质上是 :value 绑定和 @input 事件的结合体。它让开发者不用手动写“数据 -> 视图”和“视图 -> 数据”两条独立的绑定链路,一行指令就能同时搞定。

<template>
  <input v-model="text" placeholder="输入内容" />
  <p>你输入了:{{ text }}</p>
</template>

<script setup>
import { ref } from 'vue'
const text = ref('')
</script>

当用户在输入框中打字时,text.value 自动更新;如果通过代码修改 text.value,输入框也会跟着变化。这种双向同步就是“双向绑定”的直接体现。

不同表单元素的使用方式

v-model 会根据不同的表单控件自动选择对应的事件和属性:

| 元素 | 默认绑定的属性 | 默认监听的事件 |
|------|---------------|--------------|
| <input type="text"> / <textarea> | value | input |
| <input type="checkbox"> | checked | change |
| <input type="radio"> | checked | change |
| <select> | value | change |

文本与多行文本

<input v-model="username" placeholder="用户名" />
<textarea v-model="intro" rows="3"></textarea>

单选框

<label><input type="radio" v-model="gender" value="male" /> 男</label>
<label><input type="radio" v-model="gender" value="female" /> 女</label>
<p>选中:{{ gender }}</p>

gender 会直接等于选中的 value 所对应的值。

复选框

  • 单个布尔值:常用于同意协议等开关。
<label><input type="checkbox" v-model="agreed" /> 我已阅读协议</label>
<p>{{ agreed ? '已同意' : '未同意' }}</p>
  • 多个复选框绑定到数组
<label><input type="checkbox" v-model="hobbies" value="reading" /> 阅读</label>
<label><input type="checkbox" v-model="hobbies" value="sports" /> 运动</label>
<label><input type="checkbox" v-model="hobbies" value="music" /> 音乐</label>
<p>爱好:{{ hobbies.join(', ') }}</p>

hobbies 初始化为 ref([]),选中项会被 push 进数组,取消选中则移除。

下拉选择

<select v-model="city">
  <option disabled value="">请选择城市</option>
  <option value="beijing">北京</option>
  <option value="shanghai">上海</option>
</select>
<p>城市:{{ city }}</p>

多选下拉只需在 <select> 上加上 multiple,绑定的 city 声明为数组即可。

修饰符:改变默认行为

v-model 支持三个常用修饰符,写在指令之后,用点号连接:

  • .lazy:把监听事件从 input 改为 change,即输入框失去焦点时才同步数据。适合不需要实时更新的场景(如搜索框,希望用户输完再搜索)。
  <input v-model.lazy="keyword" />
  
  • .number:自动将输入值转为数字类型。如果转换结果为 NaN 则保持原字符串。
  <input type="number" v-model.number="age" />
  
  • .trim:自动删除输入内容的首尾空格。
  <input v-model.trim="username" placeholder="用户名" />
  

修饰符可以组合使用,例如 v-model.lazy.trim

自定义 v-model(组件上使用)

在封装表单控件组件时,需要手动实现父子之间的双向绑定。这通过 modelValue prop 和 update:modelValue 事件达成:

<!-- MyInput.vue -->
<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

父组件使用 v-model="val" 即可,等价于 :modelValue="val" @update:modelValue="val = $event"

Vue 3.4+ 还提供了 defineModel 宏,进一步简化:

<!-- MyInput.vue (Vue 3.4+) -->
<template>
  <input v-model="model" />
</template>

<script setup>
const model = defineModel()
</script>

一步到位,不用手动定义 props 和 emits。

表单校验:用计算属性与侦听器实现

不依赖第三方库时,表单校验的核心思路是:为每个字段定义校验规则,用计算属性动态生成错误信息,用侦听器做实时反馈

基础模式:字段状态与错误信息

<template>
  <form @submit.prevent="handleSubmit">
    <div>
      <input v-model="form.username" placeholder="用户名 (3-10 字符)" />
      <span v-if="errors.username" class="error">{{ errors.username }}</span>
    </div>
    <div>
      <input v-model="form.email" placeholder="邮箱" />
      <span v-if="errors.email" class="error">{{ errors.email }}</span>
    </div>
    <button type="submit" :disabled="!isValid">提交</button>
  </form>
</template>

<script setup>
import { reactive, computed } from 'vue'

const form = reactive({
  username: '',
  email: ''
})

// 字段级校验规则
const rules = {
  username: (val) => {
    if (!val) return '用户名不能为空'
    if (val.length < 3 || val.length > 10) return '长度应为3-10位'
    return ''
  },
  email: (val) => {
    if (!val) return '邮箱不能为空'
    const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    if (!re.test(val)) return '邮箱格式不正确'
    return ''
  }
}

// 计算错误信息对象
const errors = computed(() => {
  const errorMap = {}
  for (const field in rules) {
    errorMap[field] = rules[field](form[field])
  }
  return errorMap
})

// 整个表单是否通过
const isValid = computed(() => {
  return Object.values(errors.value).every(msg => msg === '')
})

function handleSubmit() {
  if (!isValid.value) return
  // 提交数据...
  console.log('提交数据:', { ...form })
}
</script>

<style scoped>
.error { color: red; font-size: 0.85rem; margin-left: 8px; }
</style>

这种方式简单可控,规则清晰。当字段增多时,可以把校验逻辑抽成单独的函数或对象,甚至封装成一个可复用的 useFormValidation 组合式函数。

进阶:失焦时触发校验,输入时清空错误
可以通过添加 @blur 事件配合状态管理来实现更细腻的交互:

<template>
  <input
    v-model="username"
    @blur="touched.username = true"
    placeholder="用户名"
  />
  <span v-if="touched.username && errors.username">{{ errors.username }}</span>
</template>

<script setup>
import { ref, reactive, computed } from 'vue'
const username = ref('')
const touched = reactive({ username: false })
// ... 校验逻辑同上
</script>

此时错误信息只在用户离开该字段后才显示,避免一开始就铺满红色警告。

真实场景补充:处理数值、下拉与复选框的校验

上例展示了文本输入,其他表单元素需要留意绑定的数据类型:

  • 年龄用 v-model.number 确保数值类型,校验可直接判断 typeof age === 'number'
  • 复选框数组若要求至少选择一项,校验 hobbies.length > 0
  • 下拉默认值常设为空字符串,校验时判断非空即可。

这些校验逻辑都可以统一塞进 rules 对象里,与字段值配合使用。

原生方案虽然比不上专业表单库(如 VeeValidate)在声明式规则、异步校验、国际化等方面的完整度,但胜在零依赖、完全可定制,适合中低复杂度的场景。当表单变得非常庞大、规则交织复杂时,再考虑引入第 16.2 节介绍的 VeeValidate 等专用方案也不迟。