☰
FcDesigner 表单设计器接入与跨项目渲染落地实践
2026/10/1 3:48:00 网站建设 项目流程

前阵子产品一口气提了三个新后台模块,每个模块里都塞着两三个复杂表单页。字段倒也不算特别难,但老项目里已经攒了好几套写法:有的用 el-form 手写,有的套了半成品配置化方案,有的表单甚至直接从别的项目复制过来改字段名。改一个校验逻辑要翻四五个文件,改完还要担心影响别的页面。也就是那阵子,我开始认真研究 FcDesigner 这类免费开源的表单设计器,并且把设计器和渲染器彻底拆开,做成了跨项目复用的能力。

FcDesigner 的基本思路很直接:可视化拖拽生成表单,保存成一份 JSON 结构,再通过渲染器在任意项目里把 JSON 还原成可用表单。设计一次,到处渲染,中后台快速交付表单页的团队、做低代码平台的团队、需要给运营同学自助配表单的场景都能用上。这篇博文我把接入过程、架构拆分、跨项目渲染的完整链路和踩过的坑都捋一遍,给你一条可以直接落地的路线。

1. 先说说表单开发里那些反反复复的活

1.1 表单业务为什么总在重复造轮子

很多人觉得表单不就是几个输入框加一个提交按钮,有什么好折腾的?真做后台项目的人都懂,表单才是业务系统里最容易膨胀的部分。

你仔细回想一下:每个模块的查询条件要一组表单,新增编辑要一组表单,详情页要一组只读表单,审批流程要一组表单。字段之间的联动关系、校验规则、布局排列,换个业务场景就完全不一样。产品说“这个下拉框选中以后要把另一个字段置灰”,你改完一个表单,下个表单还会出现几乎一样的逻辑,于是又复制一遍。

更要命的是,每个开发写表单的习惯不一样。有人喜欢把校验写在 rules 里,有人喜欢在 change 事件里手动 validate,有人把字段全部摊平,有人嵌套了三四层对象。同一套表单逻辑散落在不同人手里,维护成本成倍上涨。新同事接手后,光读懂这些代码就要半天,改起来更是战战兢兢。

我见过一个被表单拖垮的后台项目:查询区表单、弹窗表单、详情表单加起来一百多个,每个页面都独立实现一遍,后面想统一加一个“必填红星提示样式”,小团队改了整整两周。不是技术做不到,是重复代码太多,四处开花的改法总会有漏网之鱼。

1.2 FcDesigner 的核心思路:把“设计”和“渲染”切开

这类免费开源的表单设计器,解决的不是“少写几个 input 标签”的问题,而是把表单的整个生命周期重新拆了一遍。

传统写法里,表单和页面是强绑定的。你在页面 A 里写一个 el-form,这个表单就只能活在页面 A,想拿到页面 B 用,只能复制代码然后改。而 FcDesigner 这类方案,把表单拆成了三段:

  • 设计器:负责可视化拖拽、配置字段属性、排布布局,最终产出一份表单描述文件(Schema)。
  • Schema:一份与 Vue、React 无关的 JSON 结构,描述了这个表单有哪些字段、字段类型、校验规则、默认值、布局方式。
  • 渲染器:接收 Schema,动态生成表单组件,并处理好数据回显、校验、联动、提交这些运行时逻辑。

这个拆分思路很妙。设计器产生的 Schema 是纯数据,不依赖具体框架;渲染器是通用的,拿到任何合法 Schema 都能画出来。于是表单可以在管理后台里设计,在用户端项目里渲染;在一个项目里设计,在另一个项目里渲染。

这也解释了为什么会有人把 FcDesigner 单独提出来做跨项目渲染——因为核心资产已经从“代码”变成了“数据”,代码分散到多个项目也没关系,Schema 跟着配置走,渲染器跟着模型走,整个复杂度一下子就降下来了。

2. 看懂 FcDesigner 的架构:设计器、渲染器、Schema 中转站

2.1 三个角色的边界划分

想用好 FcDesigner,不能只把它当成一个“拖拽组件”,得先理解它内部的三个角色各自干什么。

先看一张分工表:

角色核心职责典型形态是否参与运行时
设计器(Designer)拖拽组件、配置属性、调整布局、导出 Schema一个管理后台页面,带画布和属性面板不参与。只在设计阶段使用
Schema存储表单结构和配置的 JSON 数据数据库记录、JSON 文件、接口返回值核心数据,贯穿始终
渲染器(Renderer)读取 Schema,动态渲染表单,处理数据回显、校验、提交Vue/React 组件,接收 schema 属性参与。所有业务项目里运行

这里的边界特别重要。设计器应该只负责“生成 Schema”,不应该掺和表单的提交逻辑;渲染器应该只负责“消费 Schema”,不应该依赖设计器的拖拽面板。

很多团队把表单设计器内置在业务系统里,结果设计器包体积巨大,还带着一串用不上的组件。跨项目渲染的第一步,就是先把设计器和渲染器从物理上拆开。设计器可以只放在管理端,用户端项目只需要引入渲染器,整个包能小一半以上。

2.2 Schema:表单的“图纸”,跨项目复用的关键

Schema 是整个方案的枢纽。它本质上是一份描述表单结构的 JSON,你可以把它理解成一张“图纸”:设计器是画图的人,渲染器是照着图纸施工的工程队,你手里拿的图纸(Schema)不管到了哪个工地,都能盖出同一栋楼。

一个典型的 Schema 会长这样:

{ "formId": "leave_apply_202406", "formName": "请假申请", "layout": { "labelWidth": 110, "labelPosition": "right" }, "fields": [ { "type": "input", "field": "userName", "label": "申请人", "placeholder": "请输入姓名", "maxlength": 20, "rules": [ { "required": true, "message": "请输入申请人", "trigger": "blur" } ] }, { "type": "select", "field": "leaveType", "label": "请假类型", "options": [ { "label": "事假", "value": "personal" }, { "label": "病假", "value": "sick" } ], "clearable": true } ] }

字段结构可以归纳成几个大类:type 决定渲染什么组件,field 决定数据模型里的 key,label 决定展示名称,props 透传给组件,rules 做校验,options 给下拉、单选这类组件提供选项,layout 控制整体排版。具体项目的 Schema 字段名可能有差异,但骨架基本是这套。

这里最值得玩味的是:Schema 只是数据,不携带任何业务组件实现。正因为如此,同一个 Schema 可以同时被 Vue 渲染器和 React 渲染器消费(只要你实现了对应组件的解析),也可以被 H5 端、小程序端渲染。设计一次,万物皆可渲染,这个价值在跨项目场景下特别明显。

3. 把 FcDesigner 装进现有项目:从拉代码到拖拽出第一个表单

3.1 基于 Vue3 的快速接入步骤

如果你用的是 Vue3 技术栈,接 FcDesigner 的过程其实不复杂。这里以最常见的 Element Plus 组合为例。

先在你的项目里安装依赖。具体包名以你拉取的源码仓库说明为准,通常设计器和渲染器是分开的两个包,例如fc-designer和fc-renderer:

npm install fc-designer fc-renderer

然后在入口文件里注册设计器:

import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import FcDesigner from 'fc-designer' import 'fc-designer/dist/style.css' const app = createApp(App) app.use(ElementPlus) app.use(FcDesigner) app.mount('#app')

接下来,在项目里新建一个页面,把设计器挂进去:

<template> <div class="form-designer-page"> <fc-designer ref="designerRef" /> </div> </template> <script setup> import { ref } from 'vue' const designerRef = ref(null) function handleSave() { // 设计器内部会维护一份 Schema,拿到后存到后端即可 const schema = designerRef.value.getSchema() console.log(schema) } </script>

跑起来之后,你会看到一个带画布、组件库、属性配置面板的设计器界面。左侧拖组件到中间画布,右侧配置字段属性和校验规则,保存时调用getSchema()就能拿到完整的表单描述。

这里有个容易被新手忽略的点:设计器内部依赖了 Element Plus 等组件库的样式,如果你项目的全局样式和它冲突,页面会变得很乱。建议给设计器页面单独配置样式作用域,或者放在一个独立的子路由下,不要和业务页面混在一起。

3.2 设计器页面在业务系统里的落地姿势

设计器接入之后,还要想清楚它在你系统里的位置。

常规做法是把设计器做成一个管理端页面,配合菜单和权限控制。只有具备“表单设计”权限的角色才能进入,普通用户只看渲染出来的表单。落地结构大致是这样:

  • /form-designer:设计器页面,负责创建和编辑表单。
  • /form-preview:预览页面,渲染一个 Schema 看效果。
  • /form-user:用户端页面,通过接口读取 Schema 并渲染成真实表单。

设计器的路由最好独立,不要嵌套在业务页面里。因为设计器要占全屏操作空间,左侧组件列表、中间画布、右侧属性面板三个区域同时存在,嵌套在其他布局里会显得非常拥挤。

权限控制方面,前端只做路由拦截,真正可靠的权限还是得靠后端。设计器本身只是工具,只要后端在接口层面控制好“谁可以设计表单”“谁可以发布表单”,就算有人拿到了设计器页面,也做不了越权操作。

接入完成后的第一个验证节点特别关键:拖一个输入框、一个下拉框,配置一条必填校验,然后导出 Schema,再用渲染器渲染出来,确认表单能正常回显和校验。这一步通了,后面的跨项目渲染才有基础。

4. 跨项目渲染的核心链路:表单存起来,换个项目继续用

4.1 第一步:把设计结果转成可存储的 Schema

设计器接好后,第一件事就是把 Schema 持久化。

最简单的方案:表单列表中点击“保存”,后端把 Schema 以 JSON 字段存进数据库;点击“发布”之后,所有需要渲染该表单的项目通过接口获取 Schema 并渲染。注意保存和发布是两件事,未发布的表单只能预览,发布的表单才能在业务页面里被读取。

数据模型大概是这样的:

CREATE TABLE form_definition ( id BIGINT PRIMARY KEY, form_code VARCHAR(64) UNIQUE, form_name VARCHAR(128), schema_json JSON, version INT, status TINYINT, -- 0 草稿 1 已发布 updated_at DATETIME );

这里有一个设计细节:表单是会演进的。同一个表单可能因为业务调整改了字段,但你总不能把已经发布的历史数据全作废。所以比较稳的做法是引入版本号,每次发布生成一个新版本,用户端渲染时默认读最新版本;已经产生的历史数据继续用旧版本 Schema 渲染,避免数据结构不兼容导致页面崩溃。

Schema 存进数据库之后,本质上表单业务就变成了一种“配置数据”的流动。接下来要解决的是:如何在完全不同的项目里把这份数据变成能用的表单。

4.2 第二步:在目标项目里搭一个渲染容器

跨项目渲染的目标项目,可以是一个新开发的后台系统,也可以是一个已经上线很久的老项目。目标项目不需要安装 FcDesigner 设计器,只需要安装渲染器。

渲染器通常被封装成一个组件,接受两个核心参数:Schema 和数据模型。

<template> <fc-form-renderer :schema="formSchema" v-model="formData" @submit="handleSubmit" /> </template> <script setup> import { ref, onMounted } from 'vue' import { FcFormRenderer } from 'fc-renderer' import { fetchFormSchema } from '@/api/form' const formSchema = ref({}) const formData = ref({}) onMounted(async () => { // 从后端读取表单定义 const { data } = await fetchFormSchema({ formCode: 'leave_apply_202406' }) formSchema.value = data.schemaJson }) function handleSubmit() { console.log(formData.value) } </script>

渲染器内部做的事情就是把 Schema 里的 fields 遍历一遍,根据 type 映射到对应的组件类型,然后把 props、rules、options 逐个透传给这些组件。

这个过程中你要注意:渲染器不负责数据来源。表单的初始值、编辑回显的数据,全部通过v-model传入。所以业务项目里通常要先请求表单定义(Schema),再请求表单数据(比如审批详情),等两份数据都到位后,渲染器才能完整地画出表单并回显内容。

4.3 第三步:Schema 与组件注册表的映射

跨项目渲染能不能跑通,很大程度取决于组件注册表。渲染器拿到一个{ "type": "input" }的字段,怎么知道该渲染哪个组件?靠的是一套映射表。

内置组件通常像这样:

import { ElInput, ElSelect, ElDatePicker, ElRadioGroup } from 'element-plus' const componentMap = { input: ElInput, select: ElSelect, datePicker: ElDatePicker, radioGroup: ElRadioGroup }

渲染器遍历 fields 时,根据componentMap[field.type]找到对应组件,然后用动态组件的方式渲染出来:

<template> <component :is="componentMap[field.type]" :modelValue="modelValue[field.field]" @update:modelValue="handleUpdate(field.field, $event)" v-bind="field.props" /> </template>

这里就是跨项目渲染最需要小心的地方:如果设计器里用了某个组件,但渲染器所在项目没有注册这个组件,页面上就会渲染成空白或者直接报错。比如设计器里加了一个“富文本编辑器”,但目标项目只装了基础渲染器,没有引入富文本组件,这个字段就会撑不起来。

所以跨项目渲染的组件注册表必须保持同步。你可以把自定义组件拆出来,做成一个共享包,让所有需要渲染的项目都装上同一个包,并在渲染器初始化的时候挂进去:

import { FcFormRenderer, registerComponent } from 'fc-renderer' import RichTextEditor from 'shared-form-components' registerComponent('richText', RichTextEditor)

组件注册表相当于“施工队的手册”,设计器造了一个“特殊零件”,渲染项目也得有对应的“安装手册”,否则图纸再完整也施工不了。

4.4 数据回显和提交,最容易被忽略的两件事

跨项目渲染的流程跑通之后,马上会遇到两个现实问题,一个是数据回显,一个是动态赋值。

数据回显的场景很典型:用户在发起页填了一张请假申请,审批人在审批详情页要看到这张表单。渲染器拿到同一个 Schema,把后端返回的数据填进去,这个不复杂。但有一个细节很容易翻车:接口返回的数据字段是后端驼峰命名的,Schema 里 field 也是驼峰,两者一致没问题;一旦某个表单字段在 sql 里被映射成下划线,回显时字段对不上,表单就是空的。

我建议从一开始就约定好:表单 field 命名必须和接口数据字段完全一致。Schema 里加一个字段映射规则也可以,但复杂度会上升,能不引入就不引入。

动态赋值这个问题在跨项目环境下更容易暴露。例如表单里有一个“当前登录人”字段,正常做法是触发某个事件后往表单数据里塞值,但渲染器本身不关心业务逻辑,它只负责渲染。解决办法是把联动逻辑也做成 Schema 可描述的能力,比如给字段增加一个effect配置,由渲染器按配置执行,而不是让每个业务项目各自写一套。

跨项目渲染的终极理想状态是:业务项目里只写一句“渲染这个表单”,其余的组件映射、字段联动、校验、提交都由渲染器统一处理。虽然现实往往要留一些扩展口子,但只要核心链路稳定,不同的业务项目就不需要再各自折腾表单了。

5. 跨项目渲染的坑,我基本都替你踩过一遍

5.1 版本不一致导致渲染“变形”

跨项目渲染最大的隐藏敌人是版本不一致。

设计器和渲染器其实是配套的,Schema 的一部分字段会随着设计器版本升级而变化。比如旧版设计器产出的 Schema 里,日期字段用"type": "date",新版改成了"type": "datePicker",如果旧项目里的渲染器还按旧结构解析,拿到新的 Schema 就会变成空白。

最直观的表现就是:同一个 Schema,在管理端设计器里预览是正常的,换到另一个项目渲染出来却布局错乱、字段丢失。遇到这种情况,九成原因是项目 A 的渲染器版本太老,不认新版 Schema。

解决办法只有一个:维护一张“版本兼容表”,设计器升级时明确列出哪些 Schema 字段有变更,渲染器同步升级。发布 npm 包的时候,让设计器和渲染器走同一个版本号,或者至少在大版本上保持一致。

提示:如果你在多个项目里直接复制了渲染器源码,版本同步会非常痛苦。尽早改成 npm 包依赖,升级一处,处处生效。

5.2 组件注册对不上,空白表单最常见

有个现象很讽刺:设计器里拖了一个“级联选择器”,预览一切正常,发布后用户访问,那一块区域直接消失了。页面不报错,也不显示组件,就空着一块。

原因就是目标项目的渲染器组件注册表里没有“cascader”这个组件。渲染器遇到未知 type 的字段时,有的实现是静默忽略,有的会抛警告但不至于白屏,但结果都一样——这个字段渲染不出来。

排查方法也很简单:找出那个消失的字段,看它的 type 是什么,再检查目标项目的 registerComponent 里有没有这个 key。没有就补上,补不上就回到设计器换一个通用组件。

这个坑在跨团队协作时特别容易犯,因为设计器可能由 A 团队维护,渲染器由 B 团队维护,两边对“哪些组件可以拖进画布”的理解不一致。为了避免这个问题,可以在 Schema 层面做一个校验:设计器导出时检查所有组件是否都在渲染器的注册表里,不在的话禁止发布。

5.3 校验函数没法直接存进 JSON

Schema 是 JSON,这是一个巨大的优势,但也带来了一个麻烦:函数没法存。

表单校验经常需要写正则、自定义校验函数,比如“密码必须包含大写字母和数字”“金额必须大于 0 且小于 10000”。这些逻辑如果用 JavaScript 函数写很简单,但函数不是数据,存不进 JSON,也没法通过接口下发。

一般的处理思路是把校验规则尽量配置化,常见的有required、min、max、pattern、validatorName这几种。渲染器内置一批校验器,Schema 里只写函数名:

{ "type": "input", "field": "salary", "label": "月薪", "rules": [ { "required": true, "message": "请输入月薪" }, { "validatorName": "rangeCheck", "params": ["0", "10000"] } ] }

然后在渲染器里注册校验器:

registerValidator('rangeCheck', (value, params) => { const [min, max] = params.map(Number) const num = Number(value) return num >= min && num <= max })

这样做跨项目渲染才能复用:设计器只是把“我要这个校验”记下来,真正的校验逻辑由渲染器实现。需要注意的是,校验函数名也得加进共享包,否则目标项目里没注册这个 validator,校验会悄悄失效。

5.4 样式串台和数据权限边界

样式问题在跨项目渲染里通常不致命,但很烦。

设计器的画布往往有自己的样式,比如拖拽时的虚线框、组件的 hover 效果;这些样式如果不加作用域,很容易污染目标项目的全局样式。反过来,目标项目的全局样式也可能影响渲染器产出的表单,比如某个项目的button { border-radius: 0 }全局样式,把表单里的按钮圆角全吞掉了。

建议把渲染器的所有样式都加上 scope 前缀,或者使用 CSS Modules,尽量做到对外隔离。同时给渲染器组件提供一个classPrefix属性,比如classPrefix="fc-rc-",避免和各个项目的命名风格撞车。

数据权限边界这个问题,跨项目之后会被放大:同一个 Schema 在不同项目里渲染,谁能看、谁能改,不是渲染器能决定的,而是由后端接口权限控制。比如维护人员能看全部字段,普通用户只能看到部分字段。很多团队想在渲染器里做动态字段隐藏,但更稳的姿势是后端根据用户权限过滤 Schema,把用户没有权限的字段直接剔除,再返回给前端渲染。这样前端不用感知权限,渲染器也不需要一堆权限判断逻辑。

问题表现根因解决
版本不一致同一表单不同项目渲染结果不同设计器和渲染器版本脱节统一版本号,升级同步
组件未注册字段区域空白渲染器缺少组件映射共享组件包,注册表校验
校验函数丢失校验不生效JSON 无法存函数配置化校验 + 注册校验器
样式污染组件样式错乱全局样式互相影响scoped / CSS Modules
权限不均衡字段不该显示的却显示了前端过度控制权限后端过滤 Schema

6. 再往前一步:npm 分包、Monorepo 与微前端里的渲染方案

6.1 从复制源码到发布 npm 包

跨项目渲染要走上正轨,第一步就是别再复制源码了。

有些团队图省事,直接把设计器和渲染器的源码拷到各个项目里。短期看确实方便,改代码不用发版,但后面升级共享组件、修 bug 的时候,你就会发现自己维护了 N 份分叉代码。今天在项目 A 修了一个联动 bug,项目 B 和项目 C 还抱着旧代码,明天照样出问题。

正确的做法是把设计器和渲染器分别打成 npm 包,有条件的走公司内部 registry,没条件的发到公共 npm 也没问题。渲染器要尽量轻量化,只依赖表单相关的组件库,不要引入 axios、状态管理这些业务味很重的库。

打 npm 包时有一个地方特别重要:外部依赖要声明成 peerDependencies。比如渲染器依赖 vue 和 element-plus,就应该把它们放在 peerDependencies 里,避免包里打包了两份 vue,导致组件渲染崩溃。

然后各个项目里统一安装:

npm install fc-renderer@1.2.0 --save

升级的时候改一下版本号就行。你会发现,只要包管理链路建立起来,跨项目渲染维护成本瞬间降低一大截。

6.2 用 Monorepo 管理一套表单资产

当表单一多,组件注册表、校验器、Schema 类型定义这些内容,散落在各个项目里也不好管理。这时候可以用 Monorepo 把它们收拢到一个仓库里统一管理。

一个典型的 Monorepo 目录结构长这样:

fc-form-studio/ ├── packages/ │ ├── core/ # 类型定义、工具函数 │ ├── schema/ # Schema 校验与转换 │ ├── designer/ # 设计器 │ ├── renderer/ # 渲染器 │ └── shared-components/ # 自定义公共组件 ├── examples/ │ ├── admin-app/ # 管理端示例 │ └── user-app/ # 用户端示例 └── package.json

用 pnpm workspace 管理的好处是:改了 shared-components 的代码,设计器和渲染器都能直接联动调试;发布新版本时,可以用 changesets 统一管理版本号,保证设计器和渲染器版本同步。跨项目渲染的“共享组件注册表”和“校验器集合”,也都有了唯一的源头。

我实际用下来的感受是,Monorepo 的价值不在于代码怎么放,而在于它把“一个 Schema 对应的所有资产”约束在了一个空间里。团队里再也不用满世界找“那个富文本组件在哪个项目里定义过”,翻一下 packages 目录就知道了。

6.3 微前端架构下设计器与渲染器的第三种形态

如果你的公司已经上了微前端,跨项目渲染还可以换一种玩法:把设计器和渲染器直接做成微应用,或者用 Module Federation 暴露远程组件。

以 qiankun 为例,你可以单独维护一个“表单设计器”子应用,主应用通过微前端的路由加载设计器页面;渲染器则可以做成一个 exposed 的远程组件,任意子应用都能复用。这样做的最大好处是:设计器更新时,不需要让所有项目重新发版,微前端框架会自动加载新版本。

但我也要泼一盆冷水:如果你的项目只是两三个中后台系统,没必要为了跨项目渲染专门引入微前端。微前端带来的部署复杂度、通信成本,可能比表单节省的成本还高。先用 npm 包 + Monorepo 的模式跑起来,收益已经足够;等确实出现了独立部署、独立发版、跨团队隔离这些强需求,再往微前端迁移也不迟。

注意:微前端场景下,渲染器最好通过全局状态共享组件注册表,避免每个子应用都重复注册一遍组件。一个简单的做法是把 registerComponent 收集的映射表挂到全局,设计器子应用注册一次,所有子应用都能用。

我个人实际操作中的体会是:跨项目渲染最怕的不是技术做不到,而是边界没划清楚。设计器只管出图,渲染器只管施工,Schema 就是那张图纸。把这条链路理顺,后续不管接多少个项目,都只是一次 npm install 的事。

最后分享一个很实用的小技巧:渲染器的 Props 只保留schema和data两个入口,外部的数据回显、提交、联动都收敛到渲染器内部处理。这样每个业务项目面对渲染器时只需要传一份 JSON,不需要关心组件层级和内部状态。表单资产沉淀得越久,这个设计给你省的时间就越多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询