前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

Formily 核心概念解析与构建复杂表单的最佳实践总结

首页2023-01-17 11:40:12Front-End
FormilyReact表单状态管理前端工程化Ant DesignJSON Schema

接手过一个后台的「商品发布」页面,一屏四十多个字段,规则大概是这样的:选了「实物商品」才出现物流那一组,选了「虚拟商品」出现兑换码那一组;SKU 是个可增删的表格,每行的价格要参与总价计算;优惠券字段要拿另外两个字段的值去后端校验一次。这页用 antd 的 Form 写了八百多行,改一个需求要在 useState、useEffect、onValuesChange 三个地方同时动手,谁改谁头疼。

后来这类页面我改用 Formily 重写,代码量掉到三分之一,更重要的是联动逻辑终于集中在一个地方了。这篇把 Formily 的核心概念、校验体系、联动模式,以及我在实际项目里踩出来的一些取舍写清楚。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 传统表单开发到底卡在哪,Formily 是冲着哪几个问题去的
  • Formily 的 MVVM 领域模型,createForm / FormProvider / Field 各管什么
  • 内置规则、格式校验、自定义规则、异步校验、联动校验五种校验方式
  • 主动模式和被动模式两种联动写法,分别适合什么场景
  • Schema 抽离、复杂联动集中管理、性能优化这些落地经验
  • 什么项目适合上 Formily,什么项目上了反而更麻烦

# 一、Formily 想解决的到底是什么

# 1.1 传统表单开发的四个坑

代码重复。每个表单页都要写一遍状态管理、校验逻辑、联动处理。这些代码在不同表单之间长得极像,但字段名不一样、规则不一样,抽不成公共组件,只能复制粘贴改改。

联动逻辑散得到处都是。「选 A 之后显示 B」「C 变了 D 自动重算」这类需求,在命令式写法里会散落到 onChange 回调、useEffect、渲染函数的三元表达式里。字段一多,你根本说不清某个字段是被谁改的。

校验规则分散。有的写在组件 state 里,有的挂在 Form 的 rules 上,有的塞在 blur 回调里。想统一改一下「所有必填项的提示文案」,得全局搜一遍。

动态表单几乎写不了。字段结构由后端配置下发,前端要根据一份 JSON 动态渲染出整个表单,命令式写法在这种场景下基本抓瞎。

# 1.2 Formily 的定位

Formily 是阿里开源的表单解决方案,一句话概括就是一个抽象了表单领域模型的 MVVM 表单方案。它把表单开发从命令式改成了声明式:你不再是「监听某个事件然后手动改另一个状态」,而是「声明字段之间的关系」,剩下的交给框架。

它的几个核心能力:

  • 完善的领域模型。把表单拆成 Form、Field、Validator 等概念,每一层的职责边界清楚,状态管理不再是一团 useState。
  • 联动能力。支持主动和被动两种模式,一对一、一对多、多对一的联动都能写得很短。
  • 校验体系。内置 @formily/validator 校验引擎,支持 JSON Schema 协议校验、自定义规则、异步校验。
  • 多框架支持。@formily/react 和 @formily/vue 两个桥接包,核心逻辑在 @formily/core 里,跟视图层无关。

# 二、核心概念与架构

# 2.1 MVVM 是怎么落到表单上的

Formily 采用 MVVM 架构,对应关系是这样的:

  • Model:@formily/core 里的 Form 实例,管整个表单的状态、数据、校验规则
  • View:Input、Select 这些 UI 组件
  • ViewModel:Field 实例,连接 Model 和 View 的桥梁

这套拆分带来的直接好处是表单状态和 UI 渲染彻底解耦。Form 实例是一个独立于 React 的普通对象,你可以在任何地方拿到它、读它、改它,不用非得在组件树里。写单元测试的时候这一点尤其舒服,直接构造 form 实例跑逻辑,不用挂载组件。

底层撑起这套响应式的是 @formily/reactive,思路和 MobX 是一路的,都是靠 observable 加自动依赖收集来做精准更新。如果你对这套响应式的原理感兴趣,可以顺带看看 MobX 在 React 项目里的最佳实践,两者的心智模型基本相通。

# 2.2 包结构

Formily 的分层很清楚,装包之前先搞明白你需要哪几层:

核心层 @formily/core,负责表单状态、校验、联动。跟框架无关,Vue 项目用的也是它。

桥接层 @formily/react 和 @formily/vue,把核心库接到具体框架的渲染体系里。

组件层 @formily/antd 和 @formily/next,分别基于 Ant Design 和 Alibaba Fusion 封装,提供开箱即用的表单组件。这里有个版本对应要注意:@formily/antd 对应的是 antd v4,用 antd v5 的项目要装 @formily/antd-v5,装错了组件会各种样式错乱。

# 2.3 三个必须搞懂的 API

createForm 用来创建表单核心领域模型,它就是 MVVM 里那个标准的 ViewModel。

import { createForm } from '@formily/core'

const form = createForm({
  // 表单初始值
  initialValues: {
    username: '',
    password: ''
  },
  // 表单变化回调
  onChange: (values) => {
    console.log('表单值变化:', values)
  },
  // 副作用处理
  effects: ($, { onFieldValueChange }) => {
    onFieldValueChange('username', (field) => {
      console.log('username 变化:', field.value)
    })
  }
})
@前端进阶之旅: 代码已经复制到剪贴板

这段代码表达的意思是对的(初始值 + 值变化监听 + 副作用),但写法上有两个地方跟 Formily 2 的实际 API 对不上,得说明一下。一是监听整体值变化,标准做法是在 effects 里用 onFormValuesChange 生命周期钩子,createForm 本身没有 onChange 选项;二是 effects 的签名是 effects(form),只接收一个 form 参数,onFieldValueChange 这些钩子是从 @formily/core 里 import 进来的,不是从第二个参数解构出来的。写成上面那样在新版本里会直接报「不是函数」。正确的形态是这样:

import { createForm, onFieldValueChange, onFormValuesChange } from '@formily/core'

const form = createForm({
  initialValues: { username: '', password: '' },
  effects() {
    onFormValuesChange((form) => {
      console.log('表单值变化:', form.values)
    })
    onFieldValueChange('username', (field) => {
      console.log('username 变化:', field.value)
    })
  }
})
@前端进阶之旅: 代码已经复制到剪贴板

FormProvider 是视图层桥接表单模型的入口。它接收 createForm 创建出来的 Form 实例,用 Context 把实例传给子组件,Field 才知道自己该往哪个 form 上挂。

Field 用来承接普通字段,是表单数据绑定的最小单位。

createSchemaField 用来创建基于 JSON Schema 的字段组件,走的是声明式配置那条路,动态表单靠它。

# 三、跑通第一个表单

# 3.1 装包

用 Formily 一定要装 @formily/core,它管状态、校验、联动这些核心逻辑:

# 安装内核库
npm install --save @formily/core

# 安装 React 桥接库
npm install --save @formily/react

# 安装 Ant Design 组件库
npm install --save antd moment @formily/antd
@前端进阶之旅: 代码已经复制到剪贴板

顺带说一句,moment 这个依赖是 antd v4 时代的产物,antd v5 已经换成了 dayjs,新项目照着装 moment 会白白多一个几百 K 的包,按你的 antd 版本来。

# 3.2 一个完整例子

下面这个例子把上面提到的几个 API 全串起来了:

import React from 'react'
import { createForm } from '@formily/core'
import { FormProvider, FormConsumer, Field } from '@formily/react'
import {
  FormItem,
  FormLayout,
  Input,
  FormButtonGroup,
  Submit,
} from '@formily/antd'

// 创建表单实例
const form = createForm()

export default () => {
  return (
    <FormProvider form={form}>
      <FormLayout layout="vertical">
        <Field
          name="username"
          title="用户名"
          required
          initialValue="Hello world"
          decorator={[FormItem]}
          component={[Input]}
        />
        <Field
          name="email"
          title="邮箱"
          required
          decorator={[FormItem]}
          component={[Input]}
        />
      </FormLayout>
      <FormConsumer>
        {() => (
          <div
            style={{
              marginBottom: 20,
              padding: 5,
              border: '1px dashed #666',
            }}
          >
            实时响应:{form.values.username} - {form.values.email}
          </div>
        )}
      </FormConsumer>
      <FormButtonGroup>
        <Submit onSubmit={console.log}>提交</Submit>
      </FormButtonGroup>
    </FormProvider>
  )
}
@前端进阶之旅: 代码已经复制到剪贴板

这段代码里有几个关键点值得逐个拆开看:

  • createForm 创建表单核心领域模型,注意它写在了组件外面。写在组件里的话每次渲染都会重建一个新实例,状态全丢,正确做法是用 useMemo(() => createForm(), []) 包一层。
  • FormProvider 是视图层桥接表单模型的入口。
  • FormLayout 批量控制表单布局,layout、labelCol 这些配一次,底下所有字段都生效。
  • Field 承接字段,它的几个属性各有分工:name 标识字段在最终提交数据里的路径(支持 a.b.c 这种嵌套路径),title 是标题,required 标记必填,initialValue 是默认值,decorator 是 UI 装饰器(通常给 FormItem,负责 label、错误提示、必填星号),component 是真正的输入控件。
  • FormConsumer 是响应式模型的响应器,用 render props 模式。上面例子里它包住的那块内容,会在 username 或 email 变化时自动重渲。
  • Submit 是提交动作触发器,自动处理 loading 状态和校验失败时的拦截。
fe
  • 一、Formily 想解决的到底是什么
    • 1.1 传统表单开发的四个坑
    • 1.2 Formily 的定位
  • 二、核心概念与架构
    • 2.1 MVVM 是怎么落到表单上的
    • 2.2 包结构
    • 2.3 三个必须搞懂的 API
  • 三、跑通第一个表单
    • 3.1 装包
    • 3.2 一个完整例子
  • 四、表单校验详解
    • 4.1 两种校验场景
    • 4.2 内置规则校验
    • 4.3 内置格式校验
    • 4.4 自定义规则校验
    • 4.5 异步校验
    • 4.6 联动校验
  • 五、表单联动详解
    • 5.1 两种模式
    • 5.2 主动模式
    • 5.3 被动模式
    • 5.4 能联动的状态有哪些
  • 六、复杂表单的落地经验
    • 6.1 Schema 抽到单独文件
    • 6.2 复杂联动统一放进 effects
    • 6.3 性能优化
    • 6.4 和 Ant Design 结合
  • 总结
  • 参考

← 拯救磁盘空间 为什么 pnpm 是更好的包管理工具前端异常监控平台之Sentry落地 →