vue-vben-admin 为什么用 provide/inject 替代 props 穿透?3 个设计模式拆解
【免费下载链接】vue-vben-adminA modern vue admin panel built with Vue3, Shadcn UI, Vite, TypeScript, and Monorepo. It's fast!项目地址: https://gitcode.com/GitHub_Trending/vu/vue-vben-admin
你复制了 5 个几乎一样的表单弹窗,每次加字段都要翻 5 遍代码?vue-vben-admin 的答案是把表单、弹窗这类"高频复用的 UI 零件"拆成原子组件,再用一个 API 对象 + 跨层通信把零件拼成业务页面。本文带你从源码层面看这套"先拆、再通、后拼"的组合式组件库设计。
场景一:零件的边界——Form、FormField、Modal 各自只管一件事
表单只负责"整表"交互
表单入口 vben-use-form.vue 不渲染任何具体输入框,它只处理三件整表级的事:转发 props、渲染默认操作按钮、拦截回车提交。
function handleKeyDownEnter(event: KeyboardEvent) { if (!state?.value.submitOnEnter || !forward.value.formApi?.isMounted) { return; } // 如果是 textarea 不阻止默认行为,否则会导致无法换行。 if (event.target instanceof HTMLTextAreaElement) { return; } event.preventDefault(); forward.value.formApi?.validateAndSubmit(); }字段组件只渲染"一个输入框"
单个字段由 form-field.vue 负责,它的边界清晰到只处理一件事:把component: 'Input'这样的配置解析成真实组件,并挂上自己的错误状态。
const FieldComponent = computed(() => { const finalComponent = isString(component) ? componentMap.value[component] : component; if (!finalComponent) { // 组件未注册 console.warn(`Component ${component} is not registered`); } return finalComponent ? markRaw(toRaw(finalComponent)) : finalComponent; });注意它对未注册组件只打警告不抛错——schema 是声明式配置,写错组件名时页面应该能继续渲染其他字段,而不是整表崩掉。
弹窗组件只管"壳子"
modal.vue 不关心内容是什么表单还是富文本,它只负责打开/关闭、锁定、loading 这些壳子状态,全部交给ModalApi管理。
const [Modal, modalApi] = useVbenModal({ async onConfirm() { modalApi.close(); }, });如果没有这层切分,一个"带校验、带按钮、可回车的表单"至少 300 行;拆开后每个文件都能独立测试,form-ui 包里就有 15+ 个__tests__文件对着不同切片验证。
场景二:状态怎么流转——API 对象 + provide/inject
每个组件配一个"API 对象"当状态中枢
这是整个组件库最核心的设计:useVbenForm和useVbenModal都不直接返回 DOM,而是返回[组件, api]二元组。api 内部是一个响应式 store,setState、close、validate都收口在它身上。
export function useVbenForm(options: VbenFormProps<any, any, any, any>) { const api = new FormApi<any, any, any, any>(options); const extendedApi = api as ExtendedFormApi<any, any, any, any>; extendedApi.useStore = (selector: any) => { return useSelector(api.store, selector); }; const Form = defineComponent((props, { attrs, slots }) => { api.setState({ ...props, ...attrs }); return () => h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots); }, { name: 'VbenUseForm', inheritAttrs: false }); return [Form, extendedApi] as const; }(摘自 use-vben-form.ts)
这样业务代码里formApi.validate()、modalApi.open()和组件本身是同一个响应式数据源,不存在"命令式 ref 和声明式 state 不同步"的经典问题。
用 provide 干掉 props 穿透
字段组件离表单根组件隔了好几层,如果靠 props 一层层传formApi,每加一层渲染都要改三处代码。项目用 provide/inject 解决,vben-use-form.vue 里只有两行注入:
provideFormProps([forward, form]); provideComponentRefMap(componentRefMap);子层任何深度都能取回:
const formRenderProps = injectRenderFormProps(); const formApi = formRenderProps.form; if (!formApi) { throw new Error('Form api is required in <FormField />'); }数据流向一句话描述:状态在 API 对象(向上:子组件调 api 方法触发);能力在 context(向下:根组件 provide,任意层级 inject)。props/emit 只保留给同层组件之间的直接协作,比如 Form 把keydown.enter事件抛给外层处理。想看完整实现,顺着 form-render/context.ts 和 use-form-context.ts 读即可。
场景三:组合出真实页面——弹窗里放一张表单
以"编辑用户信息"为例,页面只需要做两件事:Modal 打开时回填表单、提交成功后关闭弹窗。两个组件互不知道对方的存在:
<script setup lang="ts"> import { useVbenForm, useVbenModal } from '@vben/common-ui'; const [Modal, modalApi] = useVbenModal({ async onOpenChange(isOpen: boolean) { if (isOpen) { const data = modalApi.getData(); await formApi.setValues(data); // 打开即回填 } }, }); const [Form, formApi] = useVbenForm({ schema: userSchema }); // 内部逻辑已封装,此处省略 </script> <template> <Modal> <Form @submitted="modalApi.close()" /> </Modal> </template>这个组合模式在 form-modal-demo.vue 里有完整可运行版本。组合优于修改源码,落下来就是三条:
- 组件之间不直接引用,只通过各自的 api 和 context 协作,替换任何一半都不影响另一半;
- 业务差异(回填、校验规则、提交接口)写在页面这一层,源码层永远保持"不知道业务";
- 新需求优先查 schema 配置和插槽,动源码前先看
__tests__里有没有对应的兼容约束。
避坑与进阶:源码里写明的三个陷阱
症状:schema 里组件名拼错,页面不报错
原因:form-field.vue 对未注册组件只console.warn不抛错,设计如此。正确做法:在 config.ts 的COMPONENT_MAP里确认组件名,或自定义组件时通过componentMapprop 补注册,别指望运行时帮你兜底。
症状:Modal 的 title 改了没生效
原因:当你用connectedComponent把 Modal 抽成独立文件后,壳子上的 props 会被 use-modal.ts 显式警告拦截——"When 'connectedComponent' exists, do not set props"。正确做法:改状态一律走modalApi.setState({ title }),而不是往<Modal>标签上挂属性。
进阶:利用内置防抖写"即改即存"
表单值监听已经内置了防抖提交,无需自己套 lodash:
const handleValuesChangeDebounced = useDebounceFn(async () => { state?.value.submitOnChange && forward.value.formApi?.validateAndSubmit(); }, state?.value?.changeDebouncedTime ?? 300);(vben-use-form.vue 第 75~77 行)打开submitOnChange即可获得"改完 300ms 自动校验提交"的行为,changeDebouncedTime可配置——实时搜索类场景直接复用,别重复造防抖。
接着往哪走
下一步你可以自己走读三个地方把这条线串起来:表单原子层的 packages/@core/ui-kit/form-ui/、弹窗组合层的 packages/@core/ui-kit/popup-ui/,以及能跑起来的组合示例 playground/src/views/examples/modal/。读完 demo 再回看__tests__里的 form-integration 测试,你会更清楚这套"拆—通—拼"的设计在哪些边界上被反复验证过。
【免费下载链接】vue-vben-adminA modern vue admin panel built with Vue3, Shadcn UI, Vite, TypeScript, and Monorepo. It's fast!项目地址: https://gitcode.com/GitHub_Trending/vu/vue-vben-admin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考