ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

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 在安装依赖时没有正确去重就会出现react和react-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 里加resolutionsnpm 用户则用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-componentFormily 运行时通过这个字符串去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 是否完全一致。type是object、void还是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并且配置了MonacoEnvironmentimport { 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 的自定义表单设计器以上这些坑基本上是你绕不开的必经之路。尤其是版本和响应式实例这两个底层问题它们不会像语法报错那样显眼却会以各种匪夷所思的形态干扰整个开发过程。按照这套检查清单逐个排查能帮你在本地跑通这条链路之前省下相当大的调试成本。
返回列表