学习笔记二十五 —— formily React组件通信方案

8 阅读16分钟

设计一个可视化搭建平台的React组件通信方案(需考虑动态表单、跨组件联动、异步数据加载)

以下是基于 FormilyDesignable 的成熟方案,针对动态表单、跨组件联动、异步数据加载等核心场景的深度解析。结合其底层设计哲学与工程实践,我将从架构设计、通信机制、性能优化三个维度展开:


🧩 一、动态表单的 JSON 驱动与协议转换

1. JSON Schema 为核心的数据协议

  • 声明式表单描述
    Formily 使用 JSON Schema 定义表单结构、字段类型、校验规则和联动逻辑。例如:
    {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "x-component": "Input",
          "x-decorator": "FormItem",
          "x-reactions": [
            { "when": "{{$values.age > 18}}", "fulfill": { "state": { "required": true } } }
          ]
        }
      }
    }
    
    • x-component 指定渲染组件,x-reactions 定义联动逻辑 。
  • 协议转换层(Adapter)
    设计器(Designable)通过 TS 类型转 Schema 技术,自动生成组件的 JSON 配置:
    • 利用 typescript-json-schema 解析组件 Props 的 TS 类型定义,提取标题、描述、默认值等元信息;
    • 结合注释标签(如 @title@description)生成可读性强的表单配置 。
      优势:组件库升级无需手动维护 Schema,降低维护成本。

2. 设计器与渲染器解耦通信

  • iframe 隔离 + PostMessage
    • 设计器(主站):管理组件拖拽、配置面板。
    • 渲染器(iframe):嵌入 Formily 渲染引擎,按 Schema 实时渲染表单。
    • 通信协议
      // 设计器 → 渲染器(添加组件)
      postMsgToChild({ action: 'add', data: { componentName: 'Input' } });
      
      // 渲染器 → 设计器(同步状态)
      window.parent.postMessage({ type: 'fieldUpdate', data: { value } }, '*');
      
    优势:避免主站加载 Formily 资源,保证样式隔离与性能 。

🔄 二、跨组件联动的响应式体系

1. 依赖管理:有向无环图(DAG)

Formily 使用 DAG 管理字段间依赖关系,实现高效更新传播:

// 伪代码:依赖关系图
const dependencyGraph = {
  A: ['B', 'C'],  // A 变化触发 B、C 更新
  B: ['D'],
  C: ['D']
};
  • 拓扑排序:确定更新顺序,避免循环依赖。
  • 脏标记(Dirty Marking):仅更新受影响字段,跳过无关节点 。

2. 响应式引擎(@formily/reactive)

  • 依赖自动追踪
    基于 Proxy 的响应式系统自动收集字段依赖:
    import { observable, autorun } from '@formily/reactive';
    const obj = observable({ name: '', age: 0 });
    
    autorun(() => {
      console.log(`Name: ${obj.name}, Age: ${obj.age}`);
    });
    obj.name = 'Alice'; // 触发日志输出
    
  • 精准更新机制
    • 每个组件通过 observer 包裹,仅在其依赖的字段变化时重渲染;
    • 使用 位掩码(Bitmask) 快速对比字段状态变更(如 value vs visible)。

3. 联动配置:声明式 vs 命令式

方式适用场景示例
声明式(x-reactions)简单字段联动(显隐、必填){ "when": "{{$values.age > 18}}", "fulfill": { "state": { "required": true } } }
命令式(Effects)复杂异步逻辑(API 串联校验)onFieldValueChange('age', (field) => { ... })

三、异步数据加载与性能优化

1. 数据流集成(RTK Query / useSWR)

  • 统一数据层
    Formily 与数据加载库解耦,通过 Effects 层桥接
    import { useDataQuery } from '@lib/data-hooks';
    
    const form = createForm({
      effects() {
        onFieldInit('city', (field) => {
          const { data, loading } = useDataQuery('cities', { province: field.value });
          field.loading = loading;
          field.dataSource = data;
        });
      }
    });
    
    优势:避免重复请求,支持缓存、轮询、错误回退 。

2. 异步加载优化策略

策略实现方式适用场景
分页加载结合 x-component-props 动态传递 pageSize 参数长列表选项(省市区)
防抖搜索(debounce)输入框配置 x-component-props: { onChange: debounce(fetchOptions, 300) }搜索下拉框
LRU 缓存本地缓存 API 响应,减少重复请求静态选项数据(如枚举)

3. 渲染性能优化

  • 虚拟滚动:对长列表(如 10k+ 选项)使用 react-window 虚拟滚动。
  • 组件记忆化
    const MemoField = memo(({ value }) => (
      <ExpensiveComponent value={value} />
    ), (prev, next) => prev.value === next.value);
    
  • 批量更新
    import { batchUpdate } from '@formily/core';
    batchUpdate(() => {
      form.setFieldState('A', { ... });
      form.setFieldState('B', { ... });
    });
    

🧪 四、完整案例:多对多复杂联动

场景描述

  • 字段 A(多选):控制字段 B 的选项、字段 C 的显隐。
  • 字段 B(多选):影响字段 C 的校验规则、字段 D 的选项。
  • 字段 D(多选):过滤字段 A 的选项 。

实现方案

createForm({
  effects(form) {
    // A → B, C 联动
    onFieldValueChange('A', (aField) => {
      const valuesA = aField.value || [];
      form.setFieldState('B', state => {
        state.dataSource = fetchBOptions(valuesA);
      });
      form.setFieldState('C', state => {
        state.visible = !valuesA.includes('disableC');
      });
    });

    // B + D → A 联动
    onFieldValueChange(['B', 'D'], (bField, dField) => {
      const valuesB = bField.value || [];
      const valuesD = dField.value || [];
      form.setFieldState('A', state => {
        state.dataSource = filterAOptions([...valuesB, ...valuesD]);
      });
    });
  }
});

💎 五、总结:Formily 架构核心思想

  1. 协议驱动:JSON Schema 作为唯一数据源,统一设计器与运行时。
  2. 响应式通信
    • 依赖追踪 → 自动更新订阅字段;
    • 精准渲染 → 位掩码比对 + 组件级更新。
  3. 分层解耦
    graph LR
     设计器-- PostMessage -->渲染器
     Schema-- 协议转换 -->组件实例
     响应式引擎-- 监听 -->业务逻辑
    
  4. 性能优先
    • 异步分片加载
    • 批量更新 + 虚拟滚动
    • 缓存策略(LRU)

实施建议

  • 优先实现 Schema 生成器(基于 TS 类型推导);
  • 复杂联动用 Effects 命令式代码 补充声明式配置的不足;
  • 性能监控集成 Formily Devtools,追踪字段更新耗时 。

通过以上设计,Formily 在保证灵活性的同时,解决了企业级表单的复杂通信与性能瓶颈,值得低代码平台深度借鉴。


以下是对 Formily 基于 JSON Schema 实现表单系统的底层设计分析,重点剖析其协议定义、实现机制与解析过程:

🧠 一、协议设计:扩展 JSON Schema 的语义化能力

Formily 在 JSON Schema Draft-07 标准基础上扩展 x-* 属性,构建了兼具数据描述与 UI 控制能力的 DSL:

  1. 核心扩展属性
    • x-component:声明渲染组件(如 InputSelect
    • x-component-props:传递组件属性(如 { disabled: true }
    • x-reactions:定义字段联动规则(如值变化时修改其他字段状态)
    • x-validator:扩展校验规则(支持异步校验)
    • x-decorator:包裹组件的外层容器(如 FormItem 实现标签布局)
  2. 协议分层设计
    {
      "properties": {
        "username": {
          "type": "string",
          "title": "用户名",
          "x-component": "Input", // UI 绑定
          "x-decorator": "FormItem", // 布局控制
          "x-reactions": [ // 联动逻辑
            { "when": "{{ $self.value.length > 5 }}", 
              "fulfill": { "state": { "error": "用户名过长" } }
            }
          ]
        }
      }
    }
    
    通过分层扩展,数据规则(type/title)UI 描述(x-component)行为逻辑(x-reactions) 实现解耦。

⚙️ 二、实现机制:响应式状态与依赖追踪

1. 响应式状态管理(@formily/reactive)

  • 依赖自动收集
     import { observable, autorun } from '@formily/reactive';
     const formValues = observable({ name: '' });
     
     autorun(() => {
       // 自动追踪 formValues.name 依赖
       console.log(`Name changed: ${formValues.name}`); 
     });
    
    基于 Proxy 实现字段级订阅,避免全量渲染。

2. 依赖图(DAG)与脏检查优化

  • 拓扑排序更新
    • 当字段 A 变化时,根据依赖图 { A: ['B', 'C'] } 仅触发 B、C 更新
    • 使用 位掩码(Bitmask) 快速对比字段状态变更(值变化 vs 校验状态变化)

3. 联动引擎(x-reactions)

  • 声明式联动
    "x-reactions": [
      { 
        "when": "{{ $values.age > 18 }}", // 条件表达式
        "fulfill": { 
          "state": { "visible": true } // 控制显隐
        }
      }
    ]
    
    • 解析器将 JSON 中的字符串表达式转换为可执行函数
    • 执行时自动注入上下文变量($self$values

🔧 三、解析流程:协议到组件的转换

1. Schema 编译(Schema → FieldConfig)

graph LR
  A[JSON Schema] --> B(解析器)
  B --> C{是否有 x-component}
  C -- 是 --> D[创建组件节点]
  C -- 否 --> E[递归处理子属性]
  D --> F[注册字段到 FormStore]
  • 递归解析:深度优先遍历 Schema 的 properties
  • 组件映射:通过 x-component 匹配预注册的组件池

2. 组件渲染优化

  • 按需渲染:未可见字段不实例化组件(visible: false
  • 局部更新:字段更新时通过依赖图跳过无关组件渲染

3. 校验引擎实现

  • 规则合并
    • 原生规则(required: true) → 转换为 { required: true }
    • 扩展规则(x-validator: [{ phone: true }]) → 合并到校验器
  • 异步校验队列
     registerValidateRules({
       async checkUsername(value) {
         const res = await fetch('/check', { body: value });
         return res.valid ? '' : '用户名已存在';
       }
     });
    
    通过 Promise 链管理并发请求,避免重复提交 。

⚡️ 四、性能优化策略

  1. 协议解析加速
    • Web Worker 隔离:将 Schema 解析移至独立线程
    • Schema 缓存:对稳定 Schema 进行哈希缓存
  2. 渲染层优化
    • 虚拟滚动:对长列表(x-component: "Select")启用虚拟化
    • 批量更新batchUpdate(() => { ... }) 合并多次状态变更
  3. 内存管理
    • 懒加载:折叠面板内的字段延迟初始化
    • GC 策略:卸载组件时自动清理响应式监听

💎 五、设计哲学总结

  1. 协议驱动
    JSON Schema 作为唯一数据源,统一设计器与运行时,实现前后端解耦 。
  2. 分层架构
    graph TB
      A[协议层] --> B[响应式引擎]
      B --> C[组件桥接层]
      C --> D[渲染层]
    
  3. 开放扩展
    • 自定义组件(注册 x-component
    • 自定义校验规则(registerValidateRules
    • 自定义联动行为(扩展 x-reactions 语法)。

局限与改进方向

  • 深度嵌套 Schema 的递归解析性能瓶颈 → 探索 WebAssembly 编译优化
  • 声明式联动对复杂逻辑表达能力不足 → 结合低代码可视化编排

通过以上设计,Formily 实现了 “协议即界面” 的终极目标,为低代码平台提供了可扩展、高性能的动态表单解决方案。


Formily 的协议转换层(Adapter)是连接 JSON Schema 与运行时表单渲染的核心枢纽,其设计目标是将声明式协议动态转化为可交互的 UI 组件与响应式状态模型。以下是其设计原理和实现逻辑的直白解析:

🧩 一、协议转换层的核心任务

  1. 协议解析

    • 输入:用户定义的 JSON Schema(含 x-componentx-reactions 等扩展属性)。
    • 输出:生成字段模型(Field Model)UI 组件树
    • 关键动作:递归遍历 Schema 树,识别字段类型(如 stringvoid)、关联组件、联动规则。
  2. 状态与组件绑定

    • 为每个字段创建独立的 响应式状态模型(如值 value、显隐 visible、校验规则 validator)。
    • 将状态模型注入对应组件的属性(如 value 绑定到输入框,visible 控制组件渲染)。

⚙️ 二、核心设计原理

1. 组件映射:从字符串到真实组件

  • 问题:Schema 中的 "x-component": "Input" 只是一个字符串,如何变成真实的输入框?
  • 方案
    • 组件注册表:提前将组件名(如 Input)映射到具体的 UI 组件(如 Ant Design 的 Input 组件)。
    • 桥接层:通过 connect 函数将组件的属性(如 valueonChange)与字段模型自动绑定。

    直白解释
    协议层像“翻译官”,把 JSON 中的 "Input" 翻译成代码中的 <Input /> 组件,并告诉它:
    “你的值来自字段A,值变化时更新字段A的状态”

2. 动态状态管理:响应式引擎驱动

  • 问题:字段联动(如“当A=1时隐藏B”)如何高效触发?
  • 方案
    • 依赖追踪:解析 x-reactions 中的表达式(如 {{$values.A === 1}}),自动建立字段间的依赖关系。
    • 精确更新:当字段A变化时,仅触发依赖A的字段(如B)更新,而非全表单重渲染。

    直白解释
    协议层像“监控系统”,当字段A的值变化时,立刻检查谁依赖A,并精准通知B更新(而非喊醒所有字段)。

3. 容器与布局:Void类型的魔法

  • 问题:JSON 如何描述非数据字段(如卡片、标签页等容器)?
  • 方案
    • Void类型字段:声明 "type": "void",表示该节点不占用数据结构,仅用于布局。
    • 嵌套渲染:Void节点可包裹其他字段,形成UI容器(如卡片内嵌输入框)。

    直白解释
    协议层把 void 类型视为“透明盒子”,盒子本身不存数据,但能包裹其他字段,实现层级化UI布局

4. 异步逻辑融合:数据加载与校验

  • 问题:如何实现“选项依赖后端接口”或“异步校验用户名”?
  • 方案
    • 数据源绑定:在 x-component-props 中配置异步函数,协议层自动管理加载状态。
    • 校验队列:异步校验规则被封装为 Promise,按顺序执行避免冲突。

    直白解释
    协议层像“调度员”,当需要加载选项时,自动调用接口并更新下拉框;校验时排队检查避免混乱。


🚀 三、性能优化设计

  1. 惰性解析

    • 仅当字段可见(visible: true)时才初始化其模型和组件,减少不可见字段的开销。
  2. 模型复用

    • 相同路径的字段在 Schema 更新时复用已有模型,避免重复创建(如表格行数据)。
  3. 批量更新

    • 对联动触发的多次状态变更合并为一次渲染(类似 React 的批处理)。

💡 四、为何这样设计?

  • 解耦协议与实现
    JSON Schema 只声明 “要什么”,协议层负责 “怎么做”,使动态表单不依赖具体框架。
  • 性能与灵活性的平衡
    响应式更新解决性能瓶颈,扩展协议(x-*)满足复杂业务,同时避免协议过度臃肿。

🌟 总结:协议层的本质

协议转换层如同表单的“操作系统内核”

  1. 输入:JSON Schema(配置文件)
  2. 处理
    • 解析协议 → 创建状态模型 → 绑定组件 → 监听依赖 → 调度异步任务
  3. 输出高交互、高性能的动态表单

它让开发者通过 JSON 配置 就能获得 手写代码般的灵活性与性能,这正是 Formily 的核心突破。


Designable 设计器与渲染器之间的解耦通信核心是通过 iframe 隔离 + 安全消息协议 实现的,其本质是将设计器(主页面)与渲染器(iframe 子页面)视为两个独立进程,通过事件驱动机制完成数据同步。以下是其原理的深度解析:

🧩 一、架构设计:沙箱隔离与消息桥接

  1. 物理隔离(iframe 沙箱)

    • 设计器(主页面):负责组件拖拽、属性配置、Schema 生成,不运行任何渲染逻辑
    • 渲染器(iframe 子页面):仅运行 Formily 渲染引擎,接收 Schema 并实时渲染表单,不包含设计态代码
    • 优势
      • 避免设计器与渲染器的代码/样式冲突;
      • 渲染环境纯净,确保预览效果与生产环境一致。
  2. 通信桥梁(postMessage API)

    • 设计器 → 渲染器:通过 iframe.contentWindow.postMessage() 发送 Schema 更新、组件操作指令。
    • 渲染器 → 设计器:通过 window.parent.postMessage() 回传交互事件(如字段点击、校验错误)。
    • 消息格式
      // 设计器发送的消息
      {
        type: "SCHEMA_UPDATE",  // 消息类型
        payload: { ...schema }  // 完整的 JSON Schema
      }
      
      // 渲染器发送的消息
      {
        type: "FIELD_FOCUS",    // 消息类型
        payload: { path: "user.name" } // 聚焦字段路径
      }
      

🔁 二、通信协议:事件类型与数据流

核心消息类型

消息类型发起方作用
SCHEMA_UPDATE设计器同步最新 Schema 给渲染器,触发重新渲染
COMPONENT_DRAG设计器通知渲染器高亮拖拽区域的放置位置(视觉反馈)
FIELD_FOCUS渲染器用户点击表单字段时,通知设计器选中对应组件
VALIDATION_ERROR渲染器表单校验失败时,通知设计器标记错误字段
RENDER_READY渲染器iframe 加载完成后通知设计器,触发初始 Schema 发送

数据流示例

graph LR
  A[设计器] -- SCHEMA_UPDATE --> B[渲染器]
  B -- FIELD_FOCUS --> A
  B -- VALIDATION_ERROR --> A
  A -- COMPONENT_DRAG --> B

⚙️ 三、双向通信实现机制

  1. 设计器 → 渲染器(控制流)

    • 当用户在设计器拖拽组件或修改属性时,生成新 Schema。
    • 设计器通过 postMessage 发送 SCHEMA_UPDATE 事件,包含完整 Schema。
    • 渲染器监听消息,解析 Schema 并重新渲染表单。
  2. 渲染器 → 设计器(反馈流)

    • 用户在渲染后的表单中交互(如点击输入框),渲染器捕获事件。
    • 渲染器通过 window.parent.postMessage() 发送 FIELD_FOCUS 事件。
    • 设计器根据字段路径自动选中对应组件,同步高亮显示。

🛡️ 四、安全与性能优化

  1. 安全校验

    • 来源验证:渲染器校验消息来源是否为设计器域名,拒绝非法消息:
      window.addEventListener("message", (event) => {
        if (event.origin !== "https://designer.com") return; // 仅信任设计器域名
        handleMessage(event.data);
      });
      
  2. 性能优化

    • Schema 差分更新
      设计器对比新旧 Schema,仅发送变更部分(如 type: "SCHEMA_PATCH"),减少数据传输量。
    • 消息节流
      拖拽等高频率操作时,合并多次动作为单次更新(如 100ms 内只发送最终状态)。
  3. 渲染稳定性

    • 序列化安全:Schema 中的函数或复杂对象需转为字符串(如 x-reactions 逻辑),避免序列化失败。
    • 错误隔离:渲染器崩溃不影响设计器运行,通过 iframe.reload() 自动恢复。

💡 五、为何如此设计?

  1. 职责分离
    • 设计器专注 协议生成(JSON Schema),渲染器专注 协议执行(渲染表单),符合单一职责原则。
  2. 环境一致性
    • 渲染器 iframe 模拟生产环境,避免设计器 CSS/JS 污染预览效果。
  3. 扩展性
    • 新增消息类型(如 UNDO_REDO)即可支持新功能,无需修改核心架构。
  4. 安全性
    • postMessage 的源验证机制,杜绝跨站脚本攻击(XSS)。

💎 总结:设计器与渲染器的通信本质

设计器是导演,通过 Schema 剧本指挥演出;
渲染器是演员,在 iframe 舞台中表演;
postMessage 是对讲机,确保指令与反馈实时同步🎭。

这种设计以 协议为中心(Schema 为唯一真相源)、事件为纽带(消息驱动状态同步),实现了复杂动态表单的可视化搭建与实时预览的无缝协同。


以下基于 Formily 的跨组件联动响应式体系,从依赖管理(DAG)、响应式引擎(@formily/reactive)和联动配置方式(声明式 vs 命令式)三个核心维度展开深度解析:

🧩 一、依赖管理:有向无环图(DAG)

1. DAG 的核心作用

  • 依赖关系建模
    将字段间的联动关系抽象为有向边(如字段 A → 字段 B),确保依赖传播路径无环,避免循环更新导致的死循环问题。
  • 拓扑排序
    计算字段更新顺序(如 A 更新后需优先更新 B 再更新 C),保证联动逻辑正确性。

2. 实现原理

  • 脏标记(Dirty Marking)
    仅标记受影响的字段节点(如 A 变化时标记 B、C 为“脏”),跳过无关字段的更新。
  • 动态依赖图维护
    // 伪代码:DAG 依赖关系示例
    const dependencyGraph = {
      A: ['B', 'C'],  // A 变化触发 B、C 更新
      B: ['D'],
      C: ['D']         // D 依赖 B 和 C,需等待两者更新完成
    };
    
    当字段 A 变更时,通过拓扑排序生成更新序列 [B, C, D],确保 D 在 B/C 更新后执行。

3. 性能优势

  • 时间复杂度 O(1)
    高频输入场景下,单字段更新仅触发依赖链路上的节点更新(非整树渲染)。
  • 批量更新合并
    对同一批次内的多次更新合并为一次拓扑排序,减少冗余计算。

二、响应式引擎(@formily/reactive)

1. 依赖自动追踪

  • Proxy 代理机制
    通过 observable 将字段状态转化为响应式对象,自动追踪字段间的依赖关系:
    import { observable, autorun } from '@formily/reactive';
    const formState = observable({ name: '', age: 0 });
    
    // 自动追踪 name 和 age 的依赖
    autorun(() => {
      console.log(`个人信息: ${formState.name}, ${formState.age}`);
    });
    
    nameage 变化时,自动触发 autorun 回调。

2. 精准更新控制

  • 位掩码(Bitmask)
    用二进制位标识字段状态类型(如 0b001 代表值变化、0b010 代表显隐变化),快速对比状态差异:
    const UPDATE_MASK = {
      VALUE: 0b001,   // 值变化
      VISIBLE: 0b010,  // 显隐变化
      DISABLED: 0b100  // 禁用状态变化
    };
    
    仅当位掩码变化时才触发组件重渲染,避免无效更新。

3. 渲染优化

  • 组件级订阅
    每个表单组件通过 observer 包裹,仅在其依赖的字段状态变化时重渲染:
    const NameInput = observer(() => (
      <Input value={formState.name} onChange={(v) => (formState.name = v)} />
    ));
    

⚙️ 三、联动配置:声明式 vs 命令式

1. 声明式(x-reactions)

  • 适用场景
    简单字段联动(显隐、必填、选项更新),通过 JSON Schema 配置:
    {
      "name": {
        "x-component": "Input",
        "x-reactions": [
          {
            "dependencies": ["age"],
            "when": "{{$deps[0] > 18}}",
            "fulfill": {
              "state": { "required": true }  // 年龄>18时姓名必填
            }
          }
        ]
      }
    }
    
    优势:配置简洁,低代码平台友好;
    局限:复杂异步逻辑表达能力弱。

2. 命令式(Effects API)

  • 适用场景
    多字段联合校验、异步数据加载、跨表单通信等复杂逻辑:
    createForm({
      effects(form) {
        onFieldValueChange('city', async (field) => {
          const res = await fetch(`/districts?city=${field.value}`);
          form.setFieldState('district', state => {
            state.dataSource = res.data;  // 动态加载行政区选项
          });
        });
      }
    });
    
    优势:灵活处理异步/副作用逻辑;
    劣势:需手写代码,Schema 可移植性降低。

3. 混合策略实践

  • 分层架构
    • 基础联动:用 x-reactions 声明(如字段显隐);
    • 复杂逻辑:用 Effects 补充(如 API 串联校验)。
  • 性能对比
    联动类型实现方式时间复杂度适用场景
    单字段依赖x-reactionsO(1)显隐/必填/选项更新
    多字段联合计算EffectsO(n)异步校验/跨表单通信

💎 四、总结:响应式体系的核心价值

  1. 依赖管理
    DAG 拓扑排序 + 脏标记 → 确保联动顺序正确性,避免循环依赖。
  2. 响应式引擎
    Proxy 依赖追踪 + 位掩码对比 → 实现字段级 O(1) 精准更新。
  3. 联动配置
    声明式与命令式分层协作 → 兼顾简单配置与复杂逻辑灵活性。
  4. 性能天花板
    2000+ 字段的高频输入无卡顿,企业级复杂表单的终极解决方案。

通过 DAG 与响应式引擎的结合,Formily 实现了联动逻辑可预测更新性能可保障配置方式可扩展的完整闭环,成为低代码平台复杂表单场景的首选方案。


Formily 的异步数据加载设计通过 Effects 桥接层实现了与数据流库(如 RTK Query/useSWR)的解耦,其核心在于将数据加载逻辑抽象为独立的副作用管理模块,并通过响应式依赖追踪实现精准更新。以下是其设计原理的深度解析:

🧩 一、分层架构:数据流与UI的物理隔离

Formily 将数据加载逻辑从组件中剥离,形成三层结构:

  1. UI组件层:仅负责渲染,通过 x-component-props 声明数据依赖(如 dataSource 属性)。
  2. Effects 桥接层:监听字段生命周期事件(如初始化、值变化),触发数据加载逻辑。
  3. 数据流层:对接 RTK Query/useSWR 等库,管理请求、缓存与状态更新。
graph LR
  A[UI组件] -- 声明数据依赖 --> B(Effects桥接层)
  B -- 调用数据流API --> C[RTK Query/useSWR]
  C -- 返回数据 --> B
  B -- 更新字段状态 --> A

优势:UI组件无需感知数据来源,数据流库可替换(如切换为 Axios),实现技术栈无关性。


⚙️ 二、桥接层实现原理

1. 依赖注入式绑定

通过 createFormeffects 属性注入数据加载逻辑:

createForm({
  effects(form) {
    onFieldInit('city', (field) => {
      // 绑定数据加载逻辑到字段初始化事件
      loadCityOptions(field);
    });
  }
});
  • 事件驱动:利用 onFieldInitonFieldValueChange 等钩子监听字段状态变化。
  • 上下文传递:字段模型(field)作为载体,承载数据加载状态(loadingerror)和结果(dataSource)。

2. 与数据流库的协作模式

const { data, error, isLoading } = useSWR(
  `/api/cities?province=${field.value}`, 
  fetcher
);

// 将数据流状态同步到字段模型
field.loading = isLoading;
field.dataSource = data || [];
field.setErrors(error ? [error.message] : []);
  • 无侵入式集成:在 Effects 内调用任意数据流库的 API,无需修改 Formily 核心。
  • 自动资源释放:字段销毁时,Formily 自动取消未完成的请求(依赖数据流库的 AbortController)。

🔄 三、响应式更新与性能优化

1. 精准更新机制

  • 依赖追踪
    field.value 变化时,响应式引擎自动触发数据重载(因 useSWRkey 依赖 field.value)。
  • 局部渲染
    仅更新关联字段的 dataSource 状态位(位掩码 0b1000),避免父组件重渲染。

2. 性能优化策略

策略实现方式作用
请求防抖debounce(fetchData, 300)避免高频输入导致重复请求
缓存复用useSWR 的本地缓存机制 或 RTK Query 的自动去重减少重复请求
分页加载x-component-props: { loadMore: () => fetchNextPage() }长列表动态加载
懒加载结合 visible 状态,字段不可见时不初始化数据减少首屏请求量

💡 四、异步校验与数据流的协同

数据加载结果可直接用于异步校验:

onFieldValidate('username', async (field) => {
  const res = await checkUsername(field.value);
  if (res.exists) {
    field.selfErrors = ['用户名已存在'];
  }
});
  • 校验队列:Formily 自动管理并发校验的顺序与状态冲突。
  • 与数据流共享缓存checkUsername 复用 RTK Query 的缓存,避免额外请求。

🛠️ 五、实战案例:级联选择器优化

const form = createForm({
  effects(form) {
    onFieldValueChange('province', async (field) => {
      const provinceId = field.value;
      // 1. 显示加载状态
      form.setFieldState('city', state => state.loading = true);
      
      // 2. 调用数据流库
      const { data } = await getCities(provinceId);
      
      // 3. 更新选项并清除加载状态
      form.setFieldState('city', state => {
        state.dataSource = data;
        state.loading = false;
      });
    });
  }
});
  • 优化点
    • 使用 batchUpdate 合并 dataSourceloading 状态更新,避免两次渲染。
    • 请求错误时通过 state.errors 显示错误信息,提升用户体验。

💎 六、设计总结

  1. 解耦哲学
    Effects 层作为适配器模式的实践,隔离了数据逻辑与UI渲染,使 Formily 成为“协议处理器”而非“数据管家”。
  2. 性能关键
    依赖响应式引擎的细粒度更新 + 数据流库的缓存机制,实现万级字段表单的高性能异步加载。
  3. 扩展性
    通过自定义 Effects 可集成任意异步库(如 Apollo GraphQL),适配多协议后端。

最佳实践

  • 复杂异步逻辑用 Effects 命令式编程(如多接口串联)
  • 简单数据绑定用 x-reactions 声明式配置(如选项过滤)
  • 性能敏感场景启用 请求防抖 + LRU 缓存

这一设计使 Formily 在保证动态能力的同时,避免了与具体数据流方案的强绑定,为企业级低代码平台提供了可持续演进的异步数据方案。