一、你的表单,是否正在失控?
想象一个场景,你正在开发一个"企业贷款申请"或"保险理赔"系统。
最初,页面只有 5 个字段,你写得优雅从容。随着业务迭代,表单像吹气球一样膨胀到了 50 多个字段: "如果用户选了'个体工商户',不仅要隐藏'企业法人'字段,还得去动态请求'经营地'的下拉列表,同时'注册资本'的校验规则还要从'必填'变成'选填'......"
于是,你的 Vue 文件变成了这样:
<template>里塞满了深层嵌套的v-if和v-show。<script>里到处是监听联动逻辑的watch和冗长的if-else。- 最痛苦的是: 当后端决定调整字段名,或者公司要求把这套逻辑复用到小程序时,你发现逻辑和 UI 已经像麻绳一样死死缠在一起,拆不开了。
"难道写表单,真的只能靠体力活吗?"
为了摆脱这种低效率重复,我们尝试将 中间件思想 引入 Vue 3,把复杂的业务规则从 UI 框架中剥离出来。今天,我就把这套"一次编写,到处复用"的工程化方案分享给你。
二、 核心思想:让数据自带"说明书"
传统模式下,前端像是一个**"搬运工":拿到后端数据,手动判断哪个该显、哪个该隐。
而工程化模式下,前端更像是一个"组装厂"**:数据在进入 UI 层之前,先经过一套"中间件流水线",数据会被自动标注上 UI 描述信息(Schema)。
1. 什么是 Schema 推断?
数据不再是冷冰冰的键值对,而是变成了一个包含"元数据"的对象。通过 TypeScript 的类型推断,我们让数据自己告诉页面:
- 我应该用什么组件渲染(
componentType) - 我是否应该被显示(
visible) - 我依赖哪些字段(
dependencies) - 我的下拉选项去哪里拉取(
request)
2. UI 框架只是"皮肤"
既然逻辑都抽离到了框架无关的中间件里,那么 UI 层无论是用 Ant Design 还是 Element Plus,都只是换个"解析器"而已。
三、 实战:构建 Vue 3 自动化渲染引擎
1. 组件注册表
首先,我们要定义一个组件映射表,把抽象的字符串类型映射为具体的 Vue 组件。
TypeScript
javascript
// src-vue/components/FormRenderer/componentRegistry.ts
import NumberField from '../FieldRenderers/NumberField.vue'
import SelectField from '../FieldRenderers/SelectField.vue'
import TextField from '../FieldRenderers/TextField.vue'
import ModeToggle from '../FieldRenderers/ModeToggle.vue'
export const componentRegistry = {
number: NumberField,
select: SelectField,
text: TextField,
modeToggle: ModeToggle,
} as const
2. 组装线:自动渲染器(AutoFormRenderer)
这是我们的核心引擎。它不关心业务,只负责按照加工好的 _fieldOrder 和 _schema 进行遍历。
ini
<template>
<a-row :gutter="[16,16]">
<template v-for="key in orderedKeys" :key="key">
<component
v-if="shouldRender(key)"
:is="resolveComponent(key)"
:value="data[key]"
:config="schema[key].fieldConfig"
:dependencies="collectDeps(schema[key])"
:request="schema[key].request"
@update:value="onFieldChange(key, $event)"
/>
</template>
</a-row>
</template>
<script setup lang="ts">
const props = defineProps<{ data: any }>();
const schema = computed(() => props.data?._schema || {});
const orderedKeys = computed(() => props.data?._fieldOrder || Object.keys(props.data));
// 根据中间件注入的 visible 函数判断显隐
function shouldRender(key: string) {
const s = schema.value[key];
if (!s || s.fieldConfig?.hidden) return false;
return s.visible ? s.visible(props.data) : true;
}
function resolveComponent(key: string) {
const type = schema.value[key]?.componentType || 'text';
return componentRegistry[type];
}
</script>
3. 原子化:会"思考"的字段组件
以 SelectField 为例,它不再是被动等待赋值,而是能感知依赖。当它依赖的字段(如"省份")变化时,它会自动重新调用 request。
xml
<script setup lang="ts">
const props = defineProps(['value', 'dependencies', 'request']);
const options = ref([]);
async function loadOptions() {
if (props.request) {
options.value = await props.request(props.dependencies || {});
}
}
// 深度监听依赖变化,实现联动效果
watch(() => props.dependencies, loadOptions, { deep: true, immediate: true });
</script>
四、 方案的"真香"时刻
1. 逻辑与 UI 的彻底解耦
所有的联动规则、校验逻辑、接口请求都定义在独立于框架的 src/core 下。如果你明天想把项目从 Vue 3 迁到 React,你只需要重写那几个基础字段组件,核心业务逻辑 一行都不用动。
2. "洁癖型"提交
很多动态表单方案会将 visible、options 等 UI 状态混入业务数据,导致传给后端的 JSON 极其混乱。我们的方案在提交前会运行一次"清洗中间件":
ini
const cleanPayload = submitCompileOutputs(formData.compileOutputs);
// 自动剔除所有以 _ 开头的辅助字段和临时状态
后端拿到的永远是干净、纯粹的业务模型。
3. 开发体验的飞跃
现在,当后端新增一个字段时,你的工作流变成了:
-
在类型推断引擎里加一行规则。
-
刷新页面,字段已经按预定的位置和样式长好了。
你不再需要去 .vue 文件里翻找几百行处的 template 插入 HTML,更不需要担心漏掉了哪个 v-if。
结语:不要为了用框架而用框架
很多时候,我们觉得 Vue 或 React 难维护,是因为我们将过重的业务决策 交给了视图层。
通过引入中间件和 Schema 推断,我们实际上在 UI 框架之上建立了一个"业务逻辑防火墙"。Vue 只负责监听交互和渲染结果,而变幻莫测的业务规则被关在了纯 TypeScript 编写的沙盒里。
这种"工程化"的思维,不仅是为了今天能快速复刻功能,更是为了明天业务变动时,我们能优雅地"配置升级",而不是"推倒重来"。
你是如何处理复杂表单联动的?欢迎在评论区分享你的"避坑"指南!