
前阵子产品一口气提了三个新后台模块每个模块里都塞着两三个复杂表单页。字段倒也不算特别难但老项目里已经攒了好几套写法有的用 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 决定数据模型里的 keylabel 决定展示名称props 透传给组件rules 做校验options 给下拉、单选这类组件提供选项layout 控制整体排版。具体项目的 Schema 字段名可能有差异但骨架基本是这套。这里最值得玩味的是Schema 只是数据不携带任何业务组件实现。正因为如此同一个 Schema 可以同时被 Vue 渲染器和 React 渲染器消费只要你实现了对应组件的解析也可以被 H5 端、小程序端渲染。设计一次万物皆可渲染这个价值在跨项目场景下特别明显。3. 把 FcDesigner 装进现有项目从拉代码到拖拽出第一个表单3.1 基于 Vue3 的快速接入步骤如果你用的是 Vue3 技术栈接 FcDesigner 的过程其实不复杂。这里以最常见的 Element Plus 组合为例。先在你的项目里安装依赖。具体包名以你拉取的源码仓库说明为准通常设计器和渲染器是分开的两个包例如fc-designer和fc-renderernpm 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 classform-designer-page fc-designer refdesignerRef / /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 :schemaformSchema v-modelformData submithandleSubmit / /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 :iscomponentMap[field.type] :modelValuemodelValue[field.field] update:modelValuehandleUpdate(field.field, $event) v-bindfield.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 校验函数没法直接存进 JSONSchema 是 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属性比如classPrefixfc-rc-避免和各个项目的命名风格撞车。数据权限边界这个问题跨项目之后会被放大同一个 Schema 在不同项目里渲染谁能看、谁能改不是渲染器能决定的而是由后端接口权限控制。比如维护人员能看全部字段普通用户只能看到部分字段。很多团队想在渲染器里做动态字段隐藏但更稳的姿势是后端根据用户权限过滤 Schema把用户没有权限的字段直接剔除再返回给前端渲染。这样前端不用感知权限渲染器也不需要一堆权限判断逻辑。问题表现根因解决版本不一致同一表单不同项目渲染结果不同设计器和渲染器版本脱节统一版本号升级同步组件未注册字段区域空白渲染器缺少组件映射共享组件包注册表校验校验函数丢失校验不生效JSON 无法存函数配置化校验 注册校验器样式污染组件样式错乱全局样式互相影响scoped / CSS Modules权限不均衡字段不该显示的却显示了前端过度控制权限后端过滤 Schema6. 再往前一步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-renderer1.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不需要关心组件层级和内部状态。表单资产沉淀得越久这个设计给你省的时间就越多。