🚀 基于 provide/inject 的表单组校验 —— 在老项目中实现最小入侵式增强

在一些业务复杂、UI 自定义程度极高的项目中,即使前端团队很有经验,也常会遇到类似的现实情况:

  • UI 组件库并不能满足复杂或个性化的布局需求
  • 表单组件完全是手工打造的
  • 表单校验逻辑也属于“自研”,缺乏统一扩展机制

在这种情况下,如果突然新增一个“跨多表单项联动校验”的需求,很容易让整个校验体系崩掉,或需要大面积重构。

本文分享的方案,正是在 无需大规模改造既有组件 的前提下,借助 Vue 的 provide/inject 特性,为项目加入了一个 “表单组校验组件”,实现以下需求:

一组连续的表单项:全部都没填时无需校验,但只要填写其中一个,其他项必须补全,才能通过校验

例如示例页面中的“家庭联络人2”,若全部字段为空则允许提交,但若填写“姓名”,则职务/单位/手机等也必须填写。

图片


1️⃣ 需求背景

项目中表单 UI 和校验都是自研的,表单项组件为 InputItem.vue
核心需求:

  • 相邻的多个字段可以组成一个 校验组 ValidateGroup
  • 当组内所有字段都为空 → 不触发必填校验
  • 当任意字段填写 → 所有字段均变为必填
  • 校验提示逻辑仍留在 InputItem 内部,不改造父级提交逻辑

这类规则非常常见:

  • 紧急联系人: 姓名、电话
  • 工作经历:公司、岗位、时间段
  • 婚姻情况:配偶姓名、配偶电话
  • 邮寄地址:省、市、区、详细地址

2️⃣ provide/inject:为什么能解决问题?

Vue 2 中的 provide/inject 本质上为任意层级的组件提供了一种简单的“依赖注入”机制:

  • 父组件 provide 某些能力(值/方法)
  • 子组件 inject 后直接使用

对于这里的场景:

  • 每个 ValidateGroup.vue 提供一个“组的上下文”
  • 每个 InputItem.vue 注入这个上下文,就知道自己属于哪个组
  • 组内统一处理“要不要触发必填校验”的逻辑

也就是说:

页面 → Provide(校验组)
    → InputItem1(inject)
    → InputItem2(inject)
    → InputItem3(inject)

这就避开了“跨多个 InputItem 互相通信”的麻烦。


3️⃣ ValidateGroup.vue —— 只做一件事:统计组内是否有值

<template>
  <div>
    <slot></slot>
  </div>
</template>

<script>
export default {
  provide() {
    return {
      ValidateGroup: this
    }
  },
  data() {
    return {
      hasValueMemberCount: 0,
    };
  },
  computed: {
    shouldRequireValidate() {
      return this.hasValueMemberCount > 0;
    }
  },
  methods: {
    add() {
      this.hasValueMemberCount++
    },
    minus() {
      this.hasValueMemberCount--
    },
  }
};
</script>

它负责的逻辑非常纯粹:

  • 记录组内有值的字段数量 hasValueMemberCount
  • 只要该值 > 0,则所有项目都必须参与必填验证

4️⃣ InputItem.vue —— 最小修改,却获得“组级校验能力”

项目本身已有完善的表单校验逻辑,因此我们做了“最小入侵”的增强:

① 注入 ValidateGroup

inject: {
  ValidateGroup: { default: null }
}

② 监听 value 变化,向组汇报状态变化

handleMemberValueChangeForGroup(newVal, oldVal) {
  if (oldVal === "" && newVal !== "") {
    this.ValidateGroup.add();
  }
  if (oldVal !== "" && newVal === "") {
    this.ValidateGroup.minus();
  }
}

③ 校验时判断:是否跳过 required 校验?

修改校验逻辑中的“空值处理”部分:

if (
  rule.empty &&
  this.value == "" &&
  this.isSkipMemberRequireValidate()
) {
  return true;
}

即:

  • rule.empty = 非必填(允许为空,空值不校验)
  • value 空
  • 组内没有其他值(跳过成员必填校验)
    → 可以跳过校验

对应地,加一个判断方法:

isSkipMemberRequireValidate() {
  if (this.hasValidateGroup && this.ValidateGroup.shouldRequireValidate) {
    return false;
  }
  return true;
}

这样 InputItem 根本不需要知道它所在组里有哪些字段,只需要判断:

  • “组内其他成员有没有值?”

5️⃣ 页面使用 —— 最自然的方式

只需包一层 <validate-group>

<validate-group>
  <input-item title="姓名" v-model="guardianName2" :rules="guardianNameRules2"/>
  <input-item title="关系" v-model="relation2" :rules="relationRules2"/>
  <input-item title="职务" v-model="job2" :rules="guardianJobRules2"/>
  <input-item type="textarea" title="工作单位" v-model="work2" :rules="guardianWorkRules2"/>
  <input-item title="手机号码" v-model="telephoneNum2" :rules="guardiantelephoneNumRules2"/>
</validate-group>

无需额外传参,无需在父组件写任何额外逻辑。
所有“跨字段联动校验”逻辑,都封装在 ValidateGroup + InputItem 内部完成。


6️⃣ 最终效果

是否填写任意字段 其他字段是否必填 结果
全部为空 ❌ 不校验 提交通过
填写任意一个字段 ✔ 其他字段全部校验必填 若未填完整 → 不通过

这正是表单“关联必填”最常见的使用场景。


🧩 总结:一次优雅的“最小入侵式增强”

这个方案的优点非常显著:

无需修改表单外层的业务代码

无需调整 InputItem 的 props 或现有调用方式

可对任意一组 InputItem 增强联动校验能力

校验逻辑保持独立、可复用、可扩展

长期可维护,并可灵活支持更多复杂校验规则

这正是 provide/inject 在老项目中最适合发挥作用的场景之一:
帮助我们在不破坏既有代码结构的前提下,实现跨层级、跨组件的逻辑共享与增强。

posted @ 2025-11-20 15:14  Better-HTQ  阅读(8)  评论(0)    收藏  举报