资讯动态

Designable + Formily 低代码表单扩展:本地接入与踩坑指南

发布时间:2026/9/18 7:49:58 来源:尧图企业网站定制
先交代下背景我最近在把一个中后台项目的表单模块整体迁到Designable formily这套低代码方案上目标很直接运营和产品能自己在页面上拖表单不用每次新增字段都来找前端。折腾了差不多一周大部分时间都耗在本地开发环境的适配和自定义组件的接入上。中间踩的坑有的翻遍 GitHub issue 都找不到标准答案最后是自己断点调试加读源码才弄明白的。这篇文章就把我在本地运行Designable的formily表单扩展时遇到的典型问题、排查过程和最终解决方案整理出来。如果你正准备做类似的事情或者已经在Designable里接formily但被各种本地报错卡住这篇应该能帮你省下不少时间。1. 这个组合到底在做什么先理清架构再动手1.1 Designable 和 formily 各自负责哪一环先说清楚这两个项目的关系很多初次接触的人会把它们搞混。Designable是一个可视化低代码搭建方案它本身不负责表单的数据管理和校验它只解决“用户在界面上拖拖拽拽生成一份描述界面的 Schema JSON”这个问题。而formily是表单状态管理和渲染方案它负责把 Schema JSON 真正翻译成可交互、可校验、可联动的表单页面。两者配合起来就是一条完整的链路Designable 设计器拖拽 - 生成 Schema JSON - formily 渲染器运行时 - 真正的表单页面我项目里的做法是管理端进入“表单设计”页面加载Designable设计器运营人员拖好组件、配好校验规则点保存把 Schema JSON 提交到后端业务端根据“表单编码”拉取 Schema JSON再交给formily/react的SchemaField组件渲染出来。1.2 为什么选择这套方案而不是自己造轮子之前我们表单页面是纯手写的每个页面一个 Formik Yup 配置字段一多代码量膨胀很快。后来评估了form-render、x-render和Designable formily最终选后者的原因有三个第一formily的 Schema 描述能力和联动能力足够强。x-reactions这种响应式联动声明可以实现字段显隐、禁用、赋值等常见交互不需要额外写业务代码。第二Designable提供了完整的“设计器外壳”。组件面板、画布、属性配置面板都是现成的虽然二次开发定制也有学习成本但比从零写一个拖拽引擎要靠谱得多。第三社区的生态插件相对成熟。formily官方维护了antd、element等组件库的桥接层组件扩展只需要写一次注册代码设计器和渲染器都能复用。1.3 本地开发的完整链路图本地跑通这套东西需要同时存在两个上下文一个是设计器上下文跑的是Designable它内部维护一份 Schema JSON 的草稿状态画布中渲染的自定义组件其实是“设计态”的展示模型不真正参与表单校验。另一个是渲染器上下文跑的是formily它接收 Schema JSON 后真正创建表单状态执行校验、联动、提交。这两套上下文可以放在同一个应用里也可以分到两个应用里。像我为了调试方便在本地用一个create-react-app启动前端同时启动了设计器页面和表单预览页面Schema JSON 直接通过内存变量传递不走接口这样排查问题最直接。提示我第一次接的时候没想清楚这两层的关系结果在设计器里拖了组件预览页死活不渲染。后来发现是只注册到了设计器没在渲染器里注册表单组件一运行就报“找不到对应组件”。这个点后续详细讲。2. 本地跑起来之前的那些环境坑2.1 Node 版本和包管理器选型这套组合对 Node 版本有隐性要求不是说你用最新版就行。我本机的 Node 从 14 换到 16 再换到 18各有各的问题。最后锁在 Node 16.20.2 pnpm 7.x这个组合跑Designable官方示例的 monorepo 最稳。如果你用的是 npm 而不是 pnpm大概率会在安装依赖时报一堆peerDep冲突因为Designable仓库本身是用 pnpm workspace 管理的各个designable/*包之间存在工作区级别的依赖引用npm 处理不了 workspace 协议。具体安装命令# 使用 pnpm 安装注意版本 npm install -g pnpm7 pnpm install2.2 版本锁定是最容易翻车的地方Designable的版本迭代和formily的版本迭代并不同步。我踩过一个大坑跑Designable官方示例时它自带的是formily/react2.0.x但是我业务项目里已经装了formily/react2.2.x结果在本地一启动designable/react内部引用的SchemaField和我业务代码引用的SchemaField根本不是同一个导致组件注册无效。这个问题的本质是designable/*包将formily/*作为 peerDependency如果你不显式对齐版本pnpm 的依赖提升会把两个版本都装进来最终出现“双份 formily 实例”。我的建议是统一锁定版本以下这组版本是我实测稳定运行的包名版本designable/core2.0.4designable/react2.0.4designable/formily2.0.4formily/core2.2.xformily/react2.2.xformily/antd2.2.xantd4.24.x提醒designable/formily这个包很关键它提供了设计器和 formily 之间的桥接层包括SchemaField的设计态渲染适配。不要漏装。2.3 依赖安装后的启动报错“Invalid hook call”我第一次pnpm install完启动项目直接报了一个非常经典的 React 错误Warning: Invalid hook call. Hooks can only be called inside of the body of a function component.排查步骤是先在node_modules里搜索了所有react的安装路径果然出现了两个 React 实例。一个是业务项目自己的另一个是某个designable包内嵌的依赖项导致提升出来的。解决方案是在项目的package.json中加resolutions强制统一 React 版本{ resolutions: { react: ^18.2.0, react-dom: ^18.2.0 } }然后删掉 node_modules 和锁文件重新安装rm -rf node_modules pnpm-lock.yaml pnpm install这一步做完问题基本解决。后续如果再遇到 hook 相关报错优先怀疑是不是依赖树里出现了两个 React 或两个 formily/react用npm ls react或pnpm why react就能确认。3. 表单扩展的核心套路把自定义组件同时注册到两个上下文3.1 扩展一个组件需要做哪些事前面提到过Designable formily是两套上下文。所以你想扩展一个新组件比如一个“手机号输入框”或者更复杂的业务组件至少要做四件事写一个符合 formily 约定的普通表单组件。用registerSchemaField注册到 formily 渲染器让运行时能渲染。用Resource、createBehavior、createFieldSchema注册到 Designable 设计器让拖拽面板里能显示、拖出来。如果需要配置属性还要通过createSettingsForm编写属性配置面板让设计器右侧能编辑字段属性。这四件事你听着可能觉得多其实代码结构梳理好之后任何一个新组件基本就是复制粘贴改改。3.2 第一步写一个 formily 字段组件假设我要封装一个“评分组件”底层基于 antd 的Rate。formily 的字段组件本质上就是一个普通的 React 组件接收value和onChange作为受控属性// components/RateField.tsx import React from react; import { Rate } from antd; import { connect, mapProps, mapReadPretty } from formily/react; import { Rate as RatePreview } from ./RatePreview; const RateField (props: any) { const { value, onChange, disabled } props; return Rate value{value} onChange{onChange} disabled{disabled} /; }; export const RateFieldComponent connect( RateField, mapProps({ disabled: disabled, }), mapReadPretty(RatePreview) );这里特别注意connect和mapProps这个写法这是 formily 对外部组件的“适配层”它会把 formily 内部的状态映射成组件能识别的 props。不要直接暴露那个没有包装过的RateField否则你在 Schema 里配置disabled、placeholder等属性都不会生效。3.3 第二步注册到 formily 渲染器在应用入口或者模块初始化的地方调用registerSchemaField// register.ts import { registerSchemaField } from formily/react; import { RateFieldComponent } from ./components/RateField; registerSchemaField({ Rate: RateFieldComponent, });这一步做完渲染器里遇到type: string, x-component: Rate的 Schema 节点时就会用你的组件去渲染。你可以用下面这个 Schema 在渲染器里快速验证{ type: object, properties: { rate: { type: number, title: 评分, x-component: Rate, x-component-props: { allowHalf: true } } } }3.4 第三步注册到 Designable 设计器这一步是很多人的拦路虎也是本地运行最容易报错的地方。先看代码// designerRegister.ts import { createResource, createBehavior, createFieldSchema } from designable/core; import { RateFieldComponent } from ./components/RateField; const RateSchema createFieldSchema({ type: number, x-component: Rate, x-component-props: { allowHalf: true, }, }); const RateBehavior createBehavior({ name: Rate, extends: [Field], selector: Rate, designerProps: { title: 评分, propsSchema: createSettingsForm({ // 属性面板配置可配置 allowHalf、disabled 等 }), }, }); export const RateResource createResource({ title: 评分, icon: RateIcon, elements: [ { componentName: Field, schema: RateSchema, }, ], });上面这段代码表示在设计器左侧的组件面板中新增一个名为“评分”的资源。用户拖出来的时候会在画布上生成一个Field节点这个节点背着上面那段 Schema。然后把这几个注册项交给设计器import { createDesigner } from designable/core; import { Designer } from designable/react; import { RateResource, RateBehavior } from ./designerRegister; const designer createDesigner({ engines: { Resource: [RateResource], Behavior: [RateBehavior], }, }); export const DesignerApp () { return ( Designer designer{designer} {/* 这里放 Canvas、SettingsPanel 等组件 */} /Designer ); };3.5 第四步属性配置面板的扩展createSettingsForm是基于 formily 的 Schema 来描述属性面板的。比如我要给“评分”组件加一个“是否允许半选”的开关可以这样配置import { createSettingsForm } from designable/formily; const RateSettings createSettingsForm({ x-component-props.allowHalf: { type: boolean, title: 允许半选, x-decorator: FormItem, x-component: Switch, }, });然后把这个RateSettings传给createBehavior的designerProps.propsSchema。这样在设计器里选中“评分”组件右侧属性面板就会显示“允许半选”这个开关修改后直接同步到当前选中节点的 Schema 中。到这里一个最简单但完整的自定义表单扩展就闭环了。设计器能拖渲染器能出真表单属性面板能改配置。4. 本地运行的高频报错与问题排查实录4.1 设计器里能显示组件但渲染器报“Field 不存在”这个场景很典型在设计器画布里把组件拖出来了schema 里也有对应节点但是切到预览页控制台报类似[formily/react] SchemaField: field component not found, name: Rate原因基本就是渲染器上下文里没有注册这个组件。检查一遍registerSchemaField是否在渲染器入口执行了组件名是否和 Schema 里的x-component完全一致大小写敏感。这个坑的隐蔽之处在于如果你同时启动了多个入口文件比如designer.tsx和preview.tsx很容易在designer.tsx里注册了但preview.tsx里没写注册代码。我是在一个应用里同时挂两个页面注册逻辑写在了公共模块结果preview页面因为模块加载顺序问题没执行到注册代码后来改成在preview页面显式 import 一次注册模块才解决。4.2 样式全部错乱formily 组件和 antd 组件混在一起本地跑起来后表单组件出现了明显的样式问题按钮大小不一致、表单项间距被吃掉、弹层位置不对。排查下来是formily/antd的样式和业务项目的 antd 样式产生了冲突本质上是版本不一致导致的。formily/antd2.2.x 对应的是antd4.x如果你业务项目里用的是antd5.x那么表单样式必然乱。antd 5的 CSS-in-JS 机制和antd 4的 less 变量机制差异很大formily/antd底层的表单布局依赖的还是antd 4的Form结构。解决方案有两种第一种把业务项目的antd降到4.24.x与 formily 对齐这是最省事的方式。第二种保持antd5改用formily/antd-v5这个桥接包。这个包是社区维护的适配了 antd 5 的 API。我当时为了稳定起见选择了降级到 antd 4因为团队其他模块还大量依赖 antd 4 的组件统一在一个大版本下风险更小。注意如果在本地看到Cannot read properties of undefined (reading Item)这类报错通常就是formily/antd和当前 antd 大版本不匹配Form 组件的引用方式变了。4.3 热更新频繁失效改完代码页面白屏本地开发免不了开热更新。这套组合里Designable的Designer组件内部状态很重热更新时 React Fast Refresh 很难正确地保留它的状态经常出现改一个文件整个页面白屏必须手动刷新。我的处理办法是把设计器页面和预览页面拆到两个路由预览页面不加载Designer组件只加载SchemaField渲染逻辑。这样改表单扩展组件时只刷新预览页面避免Designer内部状态被破坏。还有一个小技巧对于扩展组件的调试不要每次都进设计器拖拽验证而是写一个固定的测试 Schema专门跑渲染器。这样排查问题速度至少快一倍。4.4 pnpm 模式下出现“双份 React”导致的 socket 报错这个现象很诡异表现是本地启动没问题但只要一操作设计器画布控制台就报Error: Minified React error #321; visit https://reactjs.org/docs/error-decoder.html?invariant321React 错误 #321 就是“Invalid hook call”的 minified 版本。原因和我前面说的类似依赖树里有多个 React 副本。在 pnpm 的严格模式下designable/react解析到了它自己 node_modules 下的 React而不是你项目根目录的 React。我的排查和解决手段分三步第一步pnpm why react查看 React 的解析路径确认是有多个实例。第二步在项目根目录package.json添加resolutions统一版本并重新安装这一步前面提到过。第三步也是后来真正稳定的方案在项目根目录添加.npmrc配置public-hoist-pattern[]*react* public-hoist-pattern[]*formily*这个配置让 pnpm 把 React 和 formily 相关的包都提升到根目录 node_modules避免不同包各自引用私有副本。4.5 表格速查本地运行常见报错与解法报错特征根本原因解决方案Invalid hook call / React error #321依赖树存在多个 React 实例检查pnpm why react用 resolutions 统一版本配置 public-hoist-patternfield component not found组件只注册到设计器渲染器未注册检查渲染器入口是否执行 registerSchemaField组件名是否大小写一致antd 组件样式错乱formily/antd 与 antd 大版本不匹配将 antd 统一到 4.24.x或切换 formily/antd-v5Cannot read properties of undefined (reading Item)桥接包引用 Form 结构不存在确认 formily/antd 版本与 antd 版本匹配热更新白屏Designer 内部状态不适合 Fast Refresh设计器与预览页拆成独立路由改组件时只刷新预览页pnpm install 报 peerDep 冲突workspace 协议依赖使用 pnpm 7.x不要用 npm 安装designable/*包5. 一些值得说透的底层经验和避坑建议5.1 理解 Schema 的“两段式”生命期能少踩一半坑很多人出问题是因为没理解 Sc hema 在设计和渲染两个阶段的差异。在设计器阶段Schema 的x-component字段对应的是“资源名称”比如Rate设计器用它来匹配createBehavior里注册的行为描述在渲染阶段x-component字段被 formily 用来匹配registerSchemaField里注册的“实际组件”。这也就是为什么我建议把“组件注册”这件事抽象成独立模块设计器和渲染器各自 import。以我目前团队定的规范为例每个自定义组件目录下必须有一个register.tsx里面包含了createFieldSchema、createBehavior、createResource、registerSchemaField四件套的调用设计器入口和渲染器入口都 import 一次这个文件确保两套上下文都能拿到。5.2 本地调自定义组件时用最小化 schema 验证调试自定义组件时不要一上来就打开完整的设计器去拖拽测试。那样链路太长中间任何一个环节出错你都不知道是组件问题、注册问题还是设计器配置问题。我习惯的做法是在项目里放一个单独的“调试页”直接在代码里写死一份最小 schemaconst schema { type: object, properties: { rate: { type: number, title: 评分, x-component: Rate, x-component-props: { allowHalf: true, }, }, }, }; export const RateDebugPage () { const form useMemo(() createForm(), []); return ( FormProvider form{form} SchemaField schema{schema} / button onClick{() console.log(form.values)}提交/button /FormProvider ); };把这份 schema 渲染成真实表单先验证这个组件在渲染器中正常工作再进入设计器链路测试拖拽行为和属性配置。两层分开验证定位问题的效率完全是两个级别。5.3 版本治理和升级策略最后说一个长期问题Designable的更新其实并不算活跃formily则还在迭代。这两者的版本一旦错位本地运行可能不报错但线上偶现“组件行为异常”这种难查的问题。所以我的建议是锁死版本不要随意升级。我把版本号写进package.json时全部用的是精确版本不用^或~并且每次升级前先在本地跑完自动化冒烟用例确认设计器拖拽、属性面板、渲染器三端都正常再考虑合并到主干。这套流程在团队里跑了一个月几乎没再遇到底层版本的幺蛾子。另外如果你想深入研究建议直接读designable/formily的源码里面那个DesignableField组件是把设计态表单和运行时表单打通的关键理解了它你就真正掌握了这个方案的核心。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价