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 等专用方案也不迟。