provide/inject 这条实现路径是对的,但我建议上下文里别只放 addField,更推荐用一个 reactive 对象来承载 model、rules、labelWidth 和 disabled;其中 labelWidth 通过 CSS 变量下发,这样 FormItem 就不用逐层接收 props,打开 labelPosition: 'top' 切换时也只需要改一个值。
补充几个实现里最容易遗漏的点:
1. 字段收集必须成对注销,否则存在 v-if 切换的字段时,fields 数组会越积越多,validate() 会去校验已经不存在的项。在 FormItem 里这样写:
const field = { prop: props.prop, validate, resetField, clearValidate }
onMounted(() => addField(field))
onBeforeUnmount(() => removeField(field))
2. 校验触发要看“当前是否已有错误”:默认情况下,blur 时触发一次校验;如果这次校验不通过,就把当前字段的触发方式自动改成 change,做实时校验,等用户改对后再恢复成 blur。只靠 trigger 单值配置做不到这一点,需要在 FormItem 里维护一个 isError 状态。这就是 Element Plus 的行为,用户体验差别很大。
3. getValueByPath / setValueByPath 统一抽出来,'user.name' 和 'tags[0]' 都走同一套解析逻辑;resetFields 需要在首次挂载时深拷贝一份初始值快照,因为直接 setValue(prop, '') 会把 Boolean 和 Array 字段清成错误类型。
4. validate() 别用 Promise.all,改用 Promise.allSettled 或自己用 for 循环收集,保证所有字段都跑完再 reject,并且把 invalidFields 一起带出来;否则业务侧没法 scrollToField(Object.keys(invalidFields)[0]) 定位第一个错误。异步 validator 记得包一层 Promise,并把返回值统一成 string | true,避免有的返回 false、有的返回字符串。
5. 别忘了 clearValidate 和表单级 disabled 的透传:disabled 通过 inject 下发,Input 组件从上下文取默认值,比一层层加 prop 省事得多。
坑:如果 rules 是 defineProps 传进来的,不要在 setup 里直接 props.rules 存成普通对象,因为父组件替换 rules 时校验会用到旧值;用 computed 或 toRef 保持响应式。另外,required 写在 FormItem 上时,记得和 rules 里的 required 做合并去重,不然会出现两条同样的必填提示。
见习用户
正式会员
认证极客






