news 2026/9/23 5:22:33

Designable+Formily本地集成避坑:版本对齐与依赖去重实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Designable+Formily本地集成避坑:版本对齐与依赖去重实战

先交代一下背景:我这边接了个内部需求,要搭一套表单搭建平台,设计器选型用了 Designable,表单运行时交给 Formily,最后统一落库成 JSON Schema 交给业务后端消费。这个组合从理论上讲非常顺——Designable 负责可视化拖拽,Formily 负责协议驱动的表单渲染,官方也提供了现成的扩展包。但实际在本地跑起来的时候,问题一个接一个,很多问题在官方 Demo 里压根不会出现,因为你一旦做了自定义组件扩展、改过构建配置、或者本机 node_modules 安装得不够干净,那些“开箱即用”的说法就变成“开箱即爆”了。

这篇文章把我踩过的坑按类型整理了一遍,包含报错现场、排查思路和最终修复方式。如果你正准备在本地把 Designable 的 Formily 扩展跑起来,建议先花五分钟看完,能少走好几天的弯路。

1. 起手式:把“版本对齐”当成需求来做

1.1 一个不显眼但致命的多 React 副本问题

我最初的做法很直接:创建一个标准的 React 应用,把@designable/react@designable/formily@formily/react@formily/antd这几个包装上,然后照着官方文档的例子写入口代码。第一跑,浏览器白屏,但终端没有任何编译报错,控制台报了一个让我印象深刻的错误:Invalid hook call

Invalid hook call这个问题在 React 社区里基本等同于“项目里存在两个 React 实例”。Designable 内部会把 React 作为 peerDependency,如果 npm 在安装依赖时没有正确去重,就会出现reactreact-dom被安装了两份的情况。一部分组件用了根目录的 React,另一部分组件引用了某个子包 node_modules 里的 React,两者不是同一个模块实例,Hook 状态自然连不上。

你可以用npm ls react来验证:

npm ls react react-dom

如果输出里有多个版本,或者同一个版本出现在多级 node_modules 目录下,基本可以确定是这个原因。解决办法也不是硬编码版本号,而是利用 lockfile 去重后,再看一遍npm explain react是谁把它拉进来的。

1.2 Formily 与 Designable 的版本矩阵

我之前吃过一个亏:@designable/formily是某个测试版本,但@formily/core装的是当时最新的 2.x。结果就是组件能拖进画布,但节点树转成 Schema 之后,Formily 运行时解析出的字段结构总是差一截,比如x-decorator明明配了,渲染端却不生效。

后来我把@designable/formily@designable/core@designable/react@formily/react@formily/core@formily/antd这些包放在同一个大版本线里,重新安装后才恢复正常。

依赖推荐策略说明
react / react-dom17.x 或 18.x用 18 时建议关掉 StrictMode,本地联调个人觉得 17 最稳
@designable/core / react / setters同一批发布的版本混用不同 tag 会出现协议转换对不上
@designable/formily与 @designable/react 保持同步这里的 transform 逻辑依赖核心包的节点模型
@formily/core / react / antd和 @designable/formily 配套否则 Reactive 作用域容易出现多实例
rxjs与 @designable/core 要求的版本一致设计器内部很多地方依赖 rxjs 行为

不要只盯着“最新版本”。官方 GitHub 仓库里 formily 扩展的示例 lockfile 本身就是一套经过了本地验证的组合,建议先用它跑通,再做升级。升级要一次只升一个包,升完立刻跑一遍“从拖拽到 Schema 输出”的冒烟链路。

1.3 npm 依赖去重的两个有效手段

清理多副本 React 和 Reactive 相关包,最简单的方式是直接删掉 node_modules 和 lockfile 重新安装,然后立刻执行一次npm dedupe。如果项目用的是 yarn,可以在 package.json 里加resolutions;npm 用户则用overrides

{ "overrides": { "react": "17.0.2", "react-dom": "17.0.2" } }

注意,overrides会强制所有子依赖使用指定版本,这种做法要谨慎,但面对 Designable 这种对 React 实例极其敏感的工具链,它是性价比最高的兜底方案。执行完npm install之后再跑一次npm ls react,确保整个依赖树里只有一个 React。

2. 本地跑起来的第一批报错:process 未定义与样式失踪

2.1 process is not defined 的根源与 CRACO 修复

把版本问题解决之后,项目终于能编译通过了,但浏览器控制台还是报了一个经典的运行时错误:process is not defined。我第一反应是代码里写了什么不该写的环境判断,搜索了一圈发现没有。后来定位到是 Designable 内部某些依赖默认引用了 Node 环境的全局变量process,浏览器里根本没有这个对象。

如果你用的是 Create React App 逃逸出来的 webpack 配置,而且恰好是 webpack 5,这个问题会格外明显。因为 webpack 5 不再自动注入 Node 全局变量的 polyfill,很多老包就暴露了。

我没有选择弹射 CRA,而是接入了 CRACO,在craco.config.js里加了一段配置:

const webpack = require('webpack') module.exports = { webpack: { alias: { process: 'process/browser', }, plugins: { add: [ new webpack.ProvidePlugin({ process: 'process/browser', }), ], }, }, }

同时记得把process这个 npm 包装上,否则 alias 之后找不到模块。再启动项目,process is not defined就没再出现过。

2.2 三层样式表的加载顺序决定了设计器 UI 是否正常

这个坑很有意思,报错不是红色报错,而是“看起来不对”:设计器左侧的物料面板能出来,但画布里的组件没有虚线和选中态,所有组件像一堆静态标签一样铺在那里,完全进入不了可编辑的视觉状态。

排查了很久,发现是样式加载顺序的问题。Designable 的设计器底层依赖 antd,同时又要覆盖 antd 的部分样式来实现拖拽辅助线、选中框、吸附状态这类交互 UI。如果你先把 Designable 样式导入了,再导入 antd 样式,那 antd 的权重会后发制人,直接把 Designable 的覆盖样式全部压掉。

我最终的入口样式顺序固定为:

import 'antd/dist/antd.min.css' import '@formily/antd/dist/formily.antd.min.css' import '@designable/react/dist/designable.antd.min.css' import '@designable/setter/dist/designable-setters.antd.min.css'

这个顺序不是拍脑袋定的,它是“基础组件库 → 表单组件库 → 设计器 UI → 设置器 UI”的依赖方向。调整完刷新,画布中的组件选中态、拖拽手柄、Schema 节点的高亮才全部恢复正常。

2.3 环境变量和 .env 文件里的变量注入坑

本地运行时还有一个容易被忽略的问题,就是 Designable 相关组件在开发环境下会读取一些运行时配置。官网示例里经常出现process.env.NODE_ENV之类的判断,这在 CRA 下没问题,但如果你在自定义 webpack 配置中用了自己的环境变量注入方式,某些变量可能拿不到。

我遇到过process.env.APP_PLATFORM没被注入导致组件渲染分支走了生产逻辑的情况。建议在.env.development里把需要的变量显式声明,并在代码里对所有必需变量做兜底默认值,不要过度依赖构建工具注入。

3. 让表单元器件“可拖可配”:Formily 扩展的三段式注册

3.1 SchemaField 侧:先确保运行时能渲染出组件

在 Designable 里扩展一个自定义表单元件,第一步不是写设计器侧的代码,而是先保证这个组件在运行时能被 Formily 渲染出来。这听起来像废话,但很多人恰恰是先写了设计器物料,然后发现运行时一片空白。

运行时端用createSchemaField注册组件:

import { createSchemaField } from '@formily/react' import { FormItem, Input, Select, ArrayCards } from '@formily/antd' import { CustomTable } from './components/CustomTable' const SchemaField = createSchemaField({ components: { FormItem, Input, Select, ArrayCards, CustomTable, }, })

这里的 key 就是自定义节点 Schema 里x-component的值。如果你在设计器里写了x-component: 'CustomTable',但运行时 SchemaField 的 components 里没注册,那 Formily 只会渲染一个空节点,控制台也不会给你任何报错,这是最恶心的情况之一。

3.2 Designable 侧:把物料注册成可拖拽节点

运行时能渲染后,再回到 Designable 侧。先通过createResource注册物料资源,让组件出现在左侧物料面板里,允许拖到画布上。一个典型的自定义表格组件资源是这样的:

import { createResource } from '@designable/core' export const CustomTableResource = createResource({ title: '自定义表格', icon: 'TableOutlined', elements: [ { componentName: 'Field', props: { type: 'void', 'x-component': 'CustomTable', 'x-decorator': 'FormItem', }, }, ], })

这里有一个很容易踩的点:type要写成void,并且x-decorator要显式声明。如果你的组件纯粹是展示型组件,没有直接对应的字段值,漏写type: 'void'会让 Formily 把它当成普通字段对待,后续字段数据联动时会出现很多诡异问题,比如校验器试图去验证一个不存在的 value。

3.3 属性设置器:registerDesignerProps 把右侧面板接上

拖进去之后,下一个坑出现在右侧属性设置器。如果你只是注册了资源,选中组件后属性面板可能是空的,因为组件还没有绑定设置器配置。要在本地扩展 Formily 字段,通常还需要用到registerDesignerProps来定义这个组件的 props 面板结构:

import { registerDesignerProps } from '@designable/react' registerDesignerProps({ CustomTable: { propsSchema: { type: 'object', properties: { columns: { title: '列配置', type: 'array', 'x-component': 'ArrayCards', 'x-decorator': 'FormItem', items: { type: 'object', properties: { title: { title: '列标题', type: 'string', 'x-component': 'Input', }, dataIndex: { title: '字段名', type: 'string', 'x-component': 'Input', }, }, }, }, }, }, }, })

注意,registerDesignerProps是全局注册,适合放在入口文件的顶层调用。如果你在组件模块内部重复调用,HMR 多次执行后可能造成重复注册,属性面板里出现重复菜单。我建议把它放在一个独立文件里,比如registerDesignerProps.ts,只被入口引入一次。

这三段式顺序不要乱:先运行时注册,再物料注册,最后设置器注册。每一步都有独立的验证方式,跳过任何一步,表面上项目不报错,但业务闭环就是缺一环。

4. 画布渲染异常:Schema 明明有节点但组件空白

4.1 空白的两个高频原因:组件名匹配失败与 x-decorator 缺失

画布空白是本地联调时出现频率最高的问题。我统计过自己的排查记录,原因基本集中在两类。

第一类是组件名匹配失败。Designable 的节点树里写的是字符串x-component,Formily 运行时通过这个字符串去SchemaField的 components 映射里找组件。两边命名只要差一个大小写、差一个空格,或者代码里做了路径别名导致组件模块没有真正加载,画布就会只显示一个空 div。

第二类是x-decorator缺失。decorator在 Formily 里负责布局包装,比如FormItem提供标签、错误信息和校验样式。如果节点树里只有x-component没有x-decorator,有些组件会失去外层包裹,看起来就像没渲染。

4.2 用 transformToSchema 打印完整 JSON 节点树定位

遇到这种问题,不要瞎猜,直接打印 Schema 树。Designable 的 Formily 扩展提供了transformToSchema,把当前设计器节点树转成 JSON Schema 后打到控制台,能非常直观地看到节点结构:

import { transformToSchema } from '@designable/formily' const schema = transformToSchema(designer.getCurrentTree()) console.log(JSON.stringify(schema, null, 2))

打印之后,重点检查三件事:

  • x-component字符串和运行时注册的 key 是否完全一致。
  • typeobjectvoid还是array,和组件的实际定位是否匹配。
  • x-decorator是否存在,以及x-decorator的 props 里有没有被传入多余字段。

这招比用断点逐步调试快得多,因为 Formily 的渲染链路比较长,从画布节点到最终 React 组件渲染中间隔了好几层抽象,直接看最终 Schema 是最高效的。

4.3 Reactive 作用域分裂:别让 Formily 与 Designable 各拿一套响应式

有一种更难排查的空白,是组件渲染出来了,但字段值和设计器修改之间不联动。表现为你在右侧属性面板改了一个文本,画布里的组件毫无反应,或者要刷新整个页面才能看到新值。

这类问题十有八九是@formily/reactive存在多个实例。Reactive 是 Formily 响应式系统的核心,如果依赖树里有两个@formily/reactive副本,Designable 里用了一个实例创建响应式对象,Formily 运行时用另一个实例去观察,它们之间无法建立依赖追踪,改动自然不会被响应。

我处理过的一个项目里,子依赖把@formily/reactive锁到了不同的 2.x 补丁版本,导致两个副本同时存在。执行npm ls @formily/reactive能清楚看到依赖树结构,然后统一版本后重新安装,问题立即消失。

4.4 React 18 StrictMode 引起的副作用重复执行

如果你在用 React 18 的 StrictMode,还会遇到另一个本地独有现象:StrictMode 会在开发模式下故意双调用副作用,这会导致 Formily 的响应式绑定重复建立和销毁,有时表现是拖拽一个新组件进来,组件闪一下又消失,或者选中状态错乱。

Designable 的官方 Demo 默认不用 StrictMode 包根组件,我建议你在本地也用常规模式,或者把 StrictMode 放在业务侧而不是设计器侧。这个限制不影响生产构建,但确实会在本地联调时制造大量困惑。

5. Monaco、HMR 和本地 devServer 的边界问题

5.1 monaco-editor 的 worker 配置

如果你的属性面板里用了代码编辑类组件,比如给某个字段的联动规则书写 JSON 或 JavaScript 表达式,那么大概率会接触到monaco-editor。这个编辑器在本地跑起来后,经常出现代码补全不工作、编辑器区域一片空白的现象。

原因是 monaco 依赖 Web Worker,默认配置下 devServer 找不到 worker 文件。我用@monaco-editor/react的 loader 显式加载 monaco,并且配置了MonacoEnvironment

import { loader } from '@monaco-editor/react' import * as monaco from 'monaco-editor' import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker' import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker' self.MonacoEnvironment = { getWorker(_: string, label: string) { if (label === 'json') { return new jsonWorker() } return new editorWorker() }, } loader.config({ monaco })

如果你是 webpack 项目,也可以直接用monaco-editor-webpack-plugin,但本地跑起来之前一定要确认 devServer 能正确返回 worker 文件路径。这个坑最烦的地方在于终端不会报错,只有打开控制台看 Network 请求才会发现 worker 在 404。

5.2 热更新导致设计器 store 被重复初始化

Designable 这类低代码设计器,本质是一个重量级状态机,内部维护着节点树、选中状态、拖拽状态和历史记录。本地开发时,如果你改了某个自定义物料组件,fast refresh 默认会尽量保留组件状态,但设计器 store 的初始化代码如果被再次执行,容易出现画布上的节点树还在,但内部引用关系已经断裂的玄学状态。

常见现场是:修改自定义组件源码后,页面自动刷新,左侧物料面板还在,但画布上所有组件都消失了,或者拖拽新组件时位置定位失效。

我的处理方式是给入口文件单独配置 full reload,不让它走部分热更新。在 CRA 或 CRACO 环境里,可以直接在 index 文件里对设计器模块做强制刷新控制。不要试图去兼容 HMR 对这类重型状态容器的特殊处理,性价比太低。

5.3 本地历史路由与资源访问路径

如果你的设计器页面挂在某个子路由下,比如/designer,并且用的是 BrowserRouter,本地开发时直接访问这个地址通常没问题,但如果 devServer 没有开启 historyApiFallback,刷新后就会 404。

CRA 内置的 devServer 默认支持,但如果你改用了自定义 server 或者把 devServer 代理到了某个端口,就需要手动打开:

historyApiFallback: true

还有一个小细节:Designable 内部加载的一些静态资源路径在本地模式下可能依赖PUBLIC_URL。如果 devServer 的 publicPath 配置非默认值,组件图标偶尔会 404,原因比较隐蔽,可以通过在控制台 Network 里看静态资源请求路径来定位。

6. 我在本地联调阶段会坚持的检查清单

经过这一轮的折腾,我最后总结出一条适合所有“Designable + Formily 本地扩展”场景的检查路径。这五步看起来简单,但每一步都能拦住一类问题。

第一,改任何依赖之前,先跑npm ls react @formily/reactive @formily/core @designable/core,看到输出结果里没有重复实例再继续动手。依赖树不干净时,后面所有调试可能都是在浪费时间。

第二,每次新增一个自定义物料,按“运行时注册、物料资源注册、设置器注册”三段式顺序操作,每完成一段就打印一次 Schema 验证。不要让组件在半个注册状态下跑太久,否则很容易把问题归因到错误阶段。

第三,保证入口样式顺序固定。基础样式、表单样式、设计器样式、设置器样式这四层一旦乱掉,各种“看不到组件但节点树正常”的奇葩问题都会冒出来。

第四,遇到画布空白先打印transformToSchema,重点比较x-component字符串和运行时注册 key 是否一致。这是最快的收敛手段。

第五,本地联调环境不追求“最新版本”,优先复刻官方示例的依赖组合。No code 平台这类项目,稳定性比版本号新鲜感重要得多,先把链路跑通,再谈升级。

如果你正准备在本地启动一个 Designable + Formily 的自定义表单设计器,以上这些坑基本上是你绕不开的必经之路。尤其是版本和响应式实例这两个底层问题,它们不会像语法报错那样显眼,却会以各种匪夷所思的形态干扰整个开发过程。按照这套检查清单逐个排查,能帮你在本地跑通这条链路之前,省下相当大的调试成本。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 5:19:12

柯美C6100/6085故障排除:周期定位与转印调整实战指南

简介:面向柯美C6100-6085多功能一体机的故障排除手册,专为维修工程师、技术员及关注设备维护的普通用户编写,提供标准化诊断与修复流程,覆盖图像质量、纸张输送、传动带、墨粉等高频故障模块。图像质量篇针对圆点、白点、鱼眼效应…

作者头像 李华
网站建设 2026/9/23 5:19:02

大模型应用开发实战:从LangChain、RAG到LangGraph的进阶路线

1. 从“调API”到“造系统”:大模型应用开发到底在学什么很多人第一次接触大模型应用开发,都是从一行openai.ChatCompletion.create()开始的。调通那一刻确实兴奋,感觉自己摸到了新时代的门槛。但很快就会发现,能跑通一个对话demo…

作者头像 李华
网站建设 2026/9/23 5:16:02

高校选课系统高并发与可解释推荐实战

1. 这不是又一个“学生管理系统”:为什么高校选课系统是高并发与智能推荐的天然练兵场我带过三届计算机系毕业设计,每年都有至少5个团队选“选课系统”——结果80%最后交的是带登录页的增删改查demo,连“两个学生同时抢同一门课”这种基础并发…

作者头像 李华
网站建设 2026/9/23 5:12:53

边缘AI SoC选型指南:12种组合的权衡与实战

1. 边缘AI场景下SoC选型的底层逻辑边缘AI这个词这两年热得发烫,但真正落到硬件选型上,很多人第一反应还是“算力越大越好”。我接触过不少做智能摄像头、工业质检盒子、车载DMS系统的团队,初期选型时盯着NPU的TOPS数字看,结果板子…

作者头像 李华