MetaForm 低代码引擎系列 · 第 2 篇
技术栈:Vue.js 3 + Composition API + 动态组件

一、前端硬编码的终结

在传统前端开发中,表单页面是这样写的:

<!-- 硬编码的噩梦 -->
<el-form>
  <el-input v-model="form.name" placeholder="姓名" />
  <el-select v-model="form.gender">
    <el-option label="男" value="male" />
    <el-option label="女" value="female" />
  </el-select>
  <el-date-picker v-model="form.birthday" />
</el-form>

每个 <input>、每个 <select> 都硬编码在 .vue 文件中。这种做法在低代码系统中无法存活

  1. 结构不可预知:表单由租户管理员在运行时动态创建,前端不可能在编译期知道会存在哪些字段。
  2. 变更成本极高:一个 Placeholder 的变更都要走 拉分支 → 修改代码 → 构建 → 部署 的漫长流程。

要解决这个问题,前端必须与业务逻辑完全解耦:前端只提供原子化的组件和布局容器,页面的拓扑形态完全由后端下发的一份 JSON 元数据(Schema)动态决定。

这种架构在 Salesforce 中被称为 FlexiPageLayout 体系。


二、定义 Layout Schema 协议

在动手写前端代码之前,我们需要与后端确立一套接口规范。前端发起请求:

GET /api/layout/{form_id}

后端返回如下结构:

{
  "layout_id": "lay_1001",
  "title": "入职申请表",
  "action_url": "/api/data/frm_1001",
  "sections": [
    {
      "section_id": "sec_basic",
      "section_title": "基础信息",
      "columns": [
        {
          "items": [
            {
              "field_name": "employee_name",
              "field_type": "string",
              "component_type": "MetaInput",
              "label": "姓名",
              "required": true,
              "max_length": 50,
              "placeholder": "请输入真实姓名"
            }
          ]
        },
        {
          "items": [
            {
              "field_name": "gender",
              "field_type": "string",
              "component_type": "MetaSelect",
              "label": "性别",
              "required": false,
              "options": ["男", "女"]
            }
          ]
        }
      ]
    },
    {
      "section_id": "sec_detail",
      "section_title": "详细信息",
      "columns": [
        {
          "items": [
            {
              "field_name": "join_date",
              "field_type": "date",
              "component_type": "MetaDate",
              "label": "入职日期"
            }
          ]
        }
      ]
    }
  ]
}

注意那个关键的 component_type 字段——它将指导 Vue 引擎进行组件的动态装载。


三、Vue 3 动态渲染引擎实现

3.1 原子组件封装

首先,将底层 UI 库的组件封装为符合 Meta 规范的原子组件:

<!-- MetaInput.vue -->
<template>
  <!-- 动态读取 schema 中的 rules,转化为底层 UI 库的属性 -->
  <el-form-item :label="schema.label" :required="schema.required">
    <el-input
      v-model="internalValue"
      :placeholder="schema.placeholder"
      :maxlength="schema.max_length"
      :show-word-limit="!!schema.max_length"
    />
  </el-form-item>
</template>

<script setup>
import { computed } from "vue";

const props = defineProps({
  schema: Object,
  modelValue: [String, Number],
});
const emit = defineEmits(["update:modelValue"]);

const internalValue = computed({
  get: () => props.modelValue,
  set: (val) => emit("update:modelValue", val),
});
</script>

同理封装 MetaSelect.vueMetaDate.vue 等原子组件。

架构师提示(关于动态校验规则下发):底层的原子组件不仅负责渲染 UI,更需要忠实地继承 Layout Schema 中定义的业务规则。如上面的 MetaInput,通过直接读取 JSON 中的 requiredmax_length 属性,并绑定到 <el-input> 上,前端无需硬编码任何繁复的校验逻辑,便自然拥有了浏览器和 UI 库提供的表单拦截能力。

3.2 动态 Component Factory 解析器

核心魔法是 Vue 的内置 <component> 指令,结合 is 属性:

<!-- DynamicFormParser.vue -->
<template>
  <div class="dynamic-form" v-if="layoutSchema">
    <h2>{{ layoutSchema.title }}</h2>

    <div
      v-for="section in layoutSchema.sections"
      :key="section.section_id"
      class="form-section"
    >
      <h3>{{ section.section_title }}</h3>

      <!-- 细化层级结构:遍历列 (Column) -->
      <div class="section-layout" style="display: flex; gap: 24px;">
        <div
          v-for="(col, colIdx) in section.columns"
          :key="colIdx"
          class="layout-column"
          style="flex: 1;"
        >
          <!-- 引擎核心:<component :is> 动态加载该列中的 items -->
          <component
            v-for="item in col.items"
            :key="item.field_name"
            :is="getComponent(item.component_type)"
            :schema="item"
            v-model="formData[item.field_name]"
          />
        </div>
      </div>
    </div>

    <el-button type="primary" @click="submitData">提交表单</el-button>
  </div>
</template>

<script setup>
import { ref, onMounted } from "vue";
import axios from "axios";
import MetaInput from "./components/MetaInput.vue";
import MetaSelect from "./components/MetaSelect.vue";
import MetaDate from "./components/MetaDate.vue";

const props = defineProps({ formId: String });

const layoutSchema = ref(null);
const formData = ref({}); // 核心:所有动态组件的数据归宿

const componentMap = { MetaInput, MetaSelect, MetaDate };
const getComponent = (typeStr) => componentMap[typeStr] || MetaInput;

onMounted(async () => {
  const { data } = await axios.get(`/api/layout/${props.formId}`);
  layoutSchema.value = data;
});

const submitData = async () => {
  // formData.value 就是完美的 JSON Payload
  await axios.post(layoutSchema.value.action_url, formData.value);
};
</script>

<style scoped>
.grid {
  display: grid;
  gap: 16px;
}
.form-section {
  margin-bottom: 24px;
}
</style>

四、双向绑定的精髓

这套代码中最精妙的一笔是 v-model="formData[item.field_name]"

formData 是一个初始化为空的 ref({}) 对象。当 Vue 渲染包含 field_name: "employee_name" 的组件时,它会自动在 formData 中创建键值 formData.employee_name,并通过 update:modelValue 事件实现双向绑定。

我们完全不用操心有多少个字段、什么嵌套结构。点击"提交"时,formData.value 里就是一份完美的、准备好塞给后端 JSONB payload 的数据体。


五、Schema 驱动渲染管线图解

Schema 驱动渲染管线

整个流程分为 4 步:

  1. API Response:后端返回 Layout JSON 描述(字段类型、名称、校验规则等)
  2. Vue Component Factory<component :is="..."> 解析器读取 JSON 并进行路由分发
  3. UI Components:分发出具体的原子组件实体(MetaInput、MetaNumber、MetaDate)
  4. State Collection:所有组件的值通过 v-model 双向绑定到集中的 JSON Payload State

小结

  • 前端零硬编码:所有表单结构由后端元数据驱动,无需手写模板
  • 可无限扩展:新增字段只需在元数据层配置,前端自动渲染对应组件
  • 数据自动收集formData 统一收集所有组件的值,与后端 DML 引擎无缝对接
下一篇预告:前端收集好了 formData,这份 JSON Payload 如何安全地经过类型转换、规范化编码后落入 PostgreSQL 的 JSONB 堆表?请看第 3 篇《运行时数据引擎 —— DML 拦截与 JSONB 检索》。

标签: none

添加新评论