formily 调研和使用

8,765 阅读4分钟

我正在参加「掘金·启航计划」

前言

最近了解了一下 formily 这个库,formily 是一个支持 react 和 vue 项目的表单管理的一个库,非常强大。底层是对表单数据用 es6 的 Proxy 代理,对数据进行劫持来做发布订阅,来达到更好的性能,正因为用了 Proxy 这个 api,所以是不支持 IE 浏览器的,作者也表示放弃 IE 了,作者演讲formily的一部分视频

四个核心库:

组件生态:

支持目前主流的 UI 组件库,对组件库里的表单组件做了桥接和 props 映射,使用的话需要去看 formily 的这些组件文档。

formily 创建表单的三种方式

  • Markup Schema

    • 标注schema 和 JSON Schema 相似,只是把 JSON 格式转换成类似 JSX 的方式,可能比 JSON Schema 书写起来更好扩展,还不太了解这种写法的使用场景
  • JSON Schema

    • 使用 JSON-Schema 协议来描述一个表单的字段,组件以及联动等,还在 JSON-Schema 的基础上加上了一些扩展属性,以 x-*格式来表达。
  • 纯 JSX

    • 和 react JSX 写法完全一样

表单联动:

表单中最复杂的可能就是一些联动逻辑,可以看看在 fromily 中如何实现表单联动。

  • 在 JSON schema 中实现表单联动逻辑,通过 x-reactions 属性配置

image.png

  • 在 createForm 的时候做联动,通过传入 effects 方法

image.png

简单的联动案例

import React from 'react';
import { createForm, onFieldReact, onFieldChange, registerValidateLocale, setValidateLanguage } from '@formily/core';
import { createSchemaField } from '@formily/react';

const DemoForm = createForm({
  validateFirst: true, // 只展示第一个校验错误提示
  validateLanguage: 'en-US', // 校验提示展示的语言
  initialValues, // 表单初始值
  effects(form) {// 做一些表单的复杂逻辑
    //主动联动模式
    // onFieldChange('size', ['items.*.url'], (field, form) => {
    // 	// field 和 props 中有很多熟悉和 api,可以看官网文档去做一些联动逻辑
    //   console.log(123, field)
    // })
    //被动联动模式
    // onFieldReact('items.*.url', (field) => {
    //   // field.query('size').get('value') 表示查询表单中 size 字段的值
    //   // 还可以通过 FormPath 的方式获取, 参考文档 https://core.formilyjs.org/zh-CN/api/entry/form-path
    //   // field.query('.aa').value() //直接读取同级别aa字段值
    //   // field.query('..aa').value() //读取父级aa字段值
    //   // field.query('..[+].aa') //读取跨级相邻aa字段值
    //   const isSmall = field.query('size').get('value') == 2;
    //   const width = isSmall ? 250 : 720;
    //   const height = isSmall ? 300 : 240;
    // 	// 设置组件的 props
    //   field.setComponentProps({
    //     outputImgSize: {
    //       width,
    //       height,
    //       maxFileSize: 4,
    //     },
    //     cropPreview: isSmall ? 'imgPreview' : '',
    //     cropper: {
    //       aspectRatio: width / height,
    //       background: false,
    //       responsive: true,
    //       autoCropArea: 1,
    //       zoomable: false,
    //     }
    //   })
    // })
  }
});

const formItemLayout = {
  labelCol: 5,
  wrapperCol: 19,
  labelAlign: 'left',
};

const schema = {
  type: 'object', // 对象类型
  properties: { // 表单中的字段
    size: { // 表单字段key
      type: 'number', // 字段值的类型
      title: 'Image Size', // 字段的 label
      required: true, // 内置校验,必填
      enum: [ // 该组件的属性,radio 的选项
        {
          label: 'Big (720px*240px)',
          value: 1,
        },
        {
          label: 'Small (250px*300px)',
          value: 2,
        },
      ],
      'x-decorator': 'FormItem', // 组件包裹的组件,一般为 formItem
      'x-component': 'Radio.Group', // 渲染的组件
    },
    items: {
      type: 'array',
      'x-component': 'ArrayItems',
      'x-decorator': 'FormItem',
      // 自定义校验,return 出去的表示校验不通过的提示
      'x-validator': `{{(value)=> {
        if(value.length > 10) {
          return 'Configure up to 10';
        }
        if(value.length < 1) {
          return 'Configure at least one';
        }
      }}}`,
      items: {
        type: 'object',
        properties: {
          url: {
            type: 'string',
            title: 'Image',
            required: true,
            'x-decorator': 'FormItem',
            'x-component': 'CropImageUploader',
            'x-component-props': { // 组件 props
              outputImgSize: {
                width: 720,
                height: 240,
                maxFileSize: 4,
              },
              cropPreview: '',
              cropper: {
                aspectRatio: 720 / 240,
                background: false,
                responsive: true,
                autoCropArea: 1,
                zoomable: false,
              },
            },
            'x-decorator-props': { // 组件包裹的组件 props
              ...formItemLayout,
            },
            'x-reactions': [ // 组件联动
              {
                dependencies: ['size'], // 依赖项
                // $deps[0] 表示依赖数组中的第一项,双花括号中的是表达式
                when: '{{$deps[0] != 2}}',
                fulfill: { // 满足 when 表达式做 fulfill 中的逻辑
                  // state: { // 该组件的 state
                  //	// state 中有很多熟悉,不止组件的 props,还有控制组件的现实和隐藏,具体看官网的文档
                  //   'component[1].outputImgSize': { // 修改组件props属性的值
                  //     width: 720,
                  //     height: 240,
                  //     maxFileSize: 4,
                  //   },
                  //   'component[1].cropPreview': '',
                  //   'component[1].cropper.aspectRatio': 720 / 240,
                  // },
                  
                  // 上面的写法和下面的效果是一样的
                  schema: {
                    'x-component-props.outputImgSize': {
                      width: 720,
                      height: 240,
                      maxFileSize: 4,
                    },
                    'x-component-props.cropPreview': '',
                    'x-component-props.cropper.aspectRatio': 720 / 240,
                  },
                },
                otherwise: { // 不满足 when 表达式的做 otherwise 中的逻辑
                  // state: {
                  //   'component[1].outputImgSize': {
                  //     width: 250,
                  //     height: 300,
                  //     maxFileSize: 4,
                  //   },
                  //   'component[1].cropPreview': 'imgPreview',
                  //   'component[1].cropper.aspectRatio': 250 / 300,
                  // },
                  schema: {
                  	'x-component-props.outputImgSize': {
                  		width: 250,
                  		height: 300,
                  		maxFileSize: 4,
                  	},
                    'x-component-props.cropPreview': 'imgPreview',
                    'x-component-props.cropper.aspectRatio': 250 / 300,
                  },
                },
              },
            ],
          },
          sort: {
            type: 'string',
            title: 'Sorting',
            required: true,
            'x-decorator': 'FormItem',
            'x-component': 'Input',
            'x-component-props': {
            placeholder: 'please enter sorting',
            },
            'x-decorator-props': {
            ...formItemLayout,
            },
            // 自定义校验中也可以写正则表达式
            'x-validator': `{{(value)=> {
              const reg = new RegExp(/^(0|([1-9]+\\d*))$/, 'g');
              if(!reg.test(value)) {
                return 'Please enter Resource ID and must be integer';
              }
            }}}`,
          },
          remove: {
            type: 'void', // 表示该组件为空值,只做UI展示,这里是对 ArrayItems 的删除按钮
              'x-decorator': 'FormItem',
              'x-component': 'ArrayItems.Remove',
            },
          },
      },
      properties: {
        add: { // 新增 ArrayItem 的按钮
          type: 'void',
          title: 'add new Type',
          'x-component': 'ArrayItems.Addition',
        },
      },
    },
  },
};

表单校验

  • 通过 api 配置校验

image.png

  • JSON schema 通过 x-validator 设置自定义校验,也可以通过 x-reactions 配置去实现联动校验,具体看官网文档

image.png

表单布局

布局可以使用 FormLayout FormItem FormGrid Space 这四个组件自由搭配使用,配置 type 为 void 表示空值字段,下面配置是简单实现了表单中一行显示两个 formItem 布局。

const schema = {
  type: 'object',
  properties: {
    grid: {
      type: 'void', // 表示空字段
      title: 'Brand Title', // formItem 的 label
      'x-component': 'Space',
      'x-decorator': 'FormItem',
      'x-decorator-props': {
        asterisk: true, // label 上显示必填的 * 号
        feedbackLayout: 'none',
        ...formItemLayout,
      },
      properties: {
        nameEn: {
          type: 'string',
          title: '',
          required: true,
          'x-decorator': 'FormItem',
          'x-component': 'Input',
          'x-component-props': {
            placeholder: 'In English',
            addonTextBefore: 'En: ',
            maxLength: 32,
            showLimitHint: true,
          },
        },
        nameLocal: {
          type: 'string',
          title: '',
          required: true,
          'x-decorator': 'FormItem',
          'x-component': 'Input',
          'x-component-props': {
            placeholder: 'In local language',
            addonTextBefore: 'Local: ',
            maxLength: 32,
            showLimitHint: true,
          },
        },
      }
    }
  },
};

效果:

image.png

总结

本次使用 formily 只了解到了这个库一部分的使用方法,个人感觉基本可以满足百分之八九十的表单交互使用,很多功能需要通过官网慢慢去了解,参考官网的一些案例,因为它的字段熟悉和 api 太多了,所以需要一点时间。配置过程中遇到个问题感觉比较坑,使用 FormGrid 组件,type 类型本应该是 void,手快打错成了 viod,导致这个UI组件中的内容也没有显示,页面也没有报错,排查起来比较费时。

参考:

formily 官网
可能是你见过最专业的表单方案---解密Formily2.0