Vue3 + Element Plus:多个 el-form 共用同一 ref 导致表单校验部分失效
关键词:Vue3、Element Plus、el-form、模板 ref、表单校验失效、validate
一、问题背景
一个表单类组件内部按业务模块拆成了多个 el-form(模块一、模块二、模块三),每个模块包含若干必填项,组件通过 defineExpose 暴露统一的 validate() 方法给父组件在提交前调用。
实际现象:点击提交时,只有其中一个模块的必填校验生效,其余模块的校验被静默跳过,数据不填也能提交成功,且控制台无任何报错。
二、错误示例代码
vue
<template>
<div class="survey-form">
<!-- 模块一 -->
<el-form ref="formRef" :model="formModel" label-width="120px">
<el-form-item
label="问题1"
prop="answerA"
:rules="[{ required: true, message: '请填写问题1', trigger: 'blur' }]"
>
<el-input v-model="formModel.answerA" />
</el-form-item>
</el-form>
<!-- 模块二:注意!这里重复使用了同一个 ref 名称 -->
<el-form ref="formRef" :model="formModel" label-width="120px">
<el-form-item
label="问题2"
prop="answerB"
:rules="[{ required: true, message: '请选择问题2', trigger: 'change' }]"
>
<el-radio-group v-model="formModel.answerB">
<el-radio label="是" />
<el-radio label="否" />
</el-radio-group>
</el-form-item>
</el-form>
<!-- 模块三:同样重复使用了同一个 ref 名称 -->
<el-form ref="formRef" :model="formModel" label-width="120px">
<el-form-item
label="问题3"
prop="answerC"
:rules="[{ required: true, message: '请选择问题3', trigger: 'change' }]"
>
<el-select v-model="formModel.answerC">
<el-option label="选项一" value="1" />
<el-option label="选项二" value="2" />
</el-select>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import type { FormInstance } from 'element-plus'
const formRef = ref<FormInstance>()
const formModel = reactive({
answerA: '',
answerB: '',
answerC: '',
})
// 暴露给父组件的校验方法
const validate = () =>
new Promise((resolve, reject) => {
if (!formRef.value) return resolve(true)
formRef.value.validate((valid: boolean) =>
valid ? resolve(true) : reject(new Error('表单校验失败'))
)
})
defineExpose({ validate })
</script>
三、错误原因
同一个模板中,多个元素使用了相同的静态 ref 名称。
Vue 3 中,非 v-for 场景下的同名 ref 不会自动收集为数组,而是按挂载顺序依次赋值、互相覆盖 ------formRef.value 最终只指向最后挂载的那个 el-form 实例(本例中为模块三)。
因此 validate() 实际只调用了一个表单的 validate:
- 模块三(最后挂载):校验正常生效;
- 模块一、模块二:所有必填校验被静默跳过,不填也能通过。
由于整个过程没有任何报错和警告,问题非常隐蔽,往往到线上脏数据出现后才被发现。
关键认知:
el-form的校验是按表单实例隔离 的。el-form-item只会注册到离它最近的祖先el-form上,form.validate()只能校验自己名下的字段,管不到其他表单。
四、正确写法
方案一:合并为单个 el-form(推荐)
如果各模块的数据同属一个 model、提交时整体校验一次即可,直接用一个 el-form 包裹所有模块:
vue
<template>
<div class="survey-form">
<el-form ref="formRef" :model="formModel" label-width="120px">
<!-- 模块一 -->
<el-form-item
label="问题1"
prop="answerA"
:rules="[{ required: true, message: '请填写问题1', trigger: 'blur' }]"
>
<el-input v-model="formModel.answerA" />
</el-form-item>
<!-- 模块二 -->
<el-form-item
label="问题2"
prop="answerB"
:rules="[{ required: true, message: '请选择问题2', trigger: 'change' }]"
>
<el-radio-group v-model="formModel.answerB">
<el-radio label="是" />
<el-radio label="否" />
</el-radio-group>
</el-form-item>
<!-- 模块三 -->
<el-form-item
label="问题3"
prop="answerC"
:rules="[{ required: true, message: '请选择问题3', trigger: 'change' }]"
>
<el-select v-model="formModel.answerC">
<el-option label="选项一" value="1" />
<el-option label="选项二" value="2" />
</el-select>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
// validate 保持不变,一次调用即可校验全部字段
const validate = () =>
new Promise((resolve, reject) => {
if (!formRef.value) return resolve(true)
formRef.value.validate((valid: boolean) =>
valid ? resolve(true) : reject(new Error('表单校验失败'))
)
})
defineExpose({ validate })
</script>
方案二:多个 el-form 使用不同的 ref,并聚合校验
如果确实需要保留多个表单(例如要按模块单独校验 / 单独清除校验状态),给每个表单独立的 ref 名称,validate 内部用 Promise.all 聚合:
vue
<template>
<div class="survey-form">
<el-form ref="formRefSectionA" :model="formModel" label-width="120px">
<!-- 模块一表单项 -->
</el-form>
<el-form ref="formRefSectionB" :model="formModel" label-width="120px">
<!-- 模块二表单项 -->
</el-form>
<el-form ref="formRefSectionC" :model="formModel" label-width="120px">
<!-- 模块三表单项 -->
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { FormInstance } from 'element-plus'
const formRefSectionA = ref<FormInstance>()
const formRefSectionB = ref<FormInstance>()
const formRefSectionC = ref<FormInstance>()
// 聚合校验:任何一个表单校验失败则整体失败
const validate = () =>
Promise.all(
[formRefSectionA, formRefSectionB, formRefSectionC].map(
(f) => f.value?.validate() ?? Promise.resolve(true)
)
).then(() => true)
defineExpose({ validate })
</script>
注意:
el-form的validate()在校验通过时 resolve、失败时 reject,因此Promise.all任一失败即整体 reject,符合"全部通过才算通过"的语义。
五、两种方案优缺点对比
| 维度 | 方案一:单一 el-form | 方案二:多 el-form + 多 ref |
|---|---|---|
| 校验完整性 | 天然完整,不可能漏 | 依赖人工聚合,漏写一个 ref 就会静默跳过(原 bug 的同形态风险) |
| 模板简洁度 | 少嵌套、无重复配置 | :model、label-width 等配置重复多份 |
| prop 冲突检测 | 所有 prop 在同一命名空间,重名立即暴露 | 各表单 prop 互相隔离,重名不易发现 |
| 细粒度控制 | validate/clearValidate/resetFields/scrollToField 只能整体调用 |
可按模块独立调用,灵活 |
| 可拆分性 | 模块与表单耦合在一个组件里 | 每个模块天然可拆成独立子组件 |
选型建议:
- 字段同属一个数据对象、提交时整体校验 → 用方案一,简单且不会出错。
- 有"按模块分别校验 / 分别清除校验状态 / 分别滚动到错误项"的需求 → 用方案二,但务必把"聚合所有表单 ref"写成显式列表,新增表单时同步维护。
六、经验总结
- 同一组件模板中,禁止给多个元素使用相同的静态 ref 名称 (
v-for除外,v-for中同名 ref 会收集为数组,是另一套语义)。需要多个实例时,要么用不同名称,要么用函数 ref 收集。 - 多个
el-form的校验互相隔离 ,共用 ref 时validate容易"只校验了最后一个挂载的表单",且无任何报错提示,属于静默 Bug,Code Review 时应重点关注。 - 暴露给父组件的
validate()若聚合多个表单,推荐显式列出 ref 数组 +Promise.all,并在新增表单时同步维护,避免再次遗漏。