formily2.0探究 (一)

2,022 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第27天,点击查看活动详情

formily

中后台复杂场景的数据+协议驱动的表单框架,阿里巴巴统一表单解决方案,可以完成复杂表单需求,提供表单设计器。

优势

高性能 字段数据多也能快速相应

跨端 与框架无关,兼容reactvue

生态好 支持antdelement等组件库

协议驱动 json

架构

三部分,

@formily/core 内核:管理表单的校验和联动

@formily/react ui桥接库,接入内核数据实现表单交互效果

@formily/antd 封装了场景化的组件

比较其他产品

自定义组件成本低、性能好、支持动态渲染、开箱即用、支持跨端、开发效率高、视图代码可维护性高、场景化封装能力强,支持预览表单

但是学习成本高,api太多了,比我用过的 x-render复杂多了,写个例子要用好多库

安装

npm i @formily/reactive @formily/core @formily/reactive-react @formily/react @formily/antd ajv less --save

#vite.config.ts
 resolve: {
     // less文件 引入antd 中 有一个 ~ ,这种写法对esm构建工具不兼容,转化一下
    alias: [
      { find: /^~/, replacement: '' }
    ]
  },
// react17 jsx转换器修改 React.createElemtnt() =>jsx()
// vite默认自动用jsx去编译,可能会报错,所以要用古典的转换器
  plugins: [react(
    {
      jsxRuntime: 'classic'
    }
  )],
  css: { // less 用到js了,所以要开
    preprocessorOptions: {
      less: {//less3.0后 less的javascriptEnabled默认是false
        javascriptEnabled: true
      }
    }
  }
  • tsconfig.json

        "noImplicitAny": false, //先不用类型
    

优势策略

字段多

依赖响应式解决方案,构建响应式表单的领域模型实现精确渲染

简单说 就是 表单数据更新的时候,他只会重渲染他对应的那个组件,按需渲染

mvvm

model-view-viewmodel,逻辑和视图分离

view负责维护ui层,viewmodel负责数据视图交互,model就是数据模型

viewmodel数据变化会触发ui更新 #id.value =

ui更新触发viewmodel更新数据模型 onchange

formily提供了viewviewmodelviewformily/reactviewmodel@formily/core

响应式

文档:reactive.formilyjs.org/zh-CN/guide…

例子:reactive.formilyjs.org/zh-CN/api/r…

据说是类似mobx,看起来像vue3,参考我写的 vue3-响应式基础篇

流程

img

代理对象

tracker 执行

读取对象属性,触发拦截,将当前属性和tracker 绑定

修改对象属性时,触发拦截,执行当前属性依赖的tracker

又执行tracker, 读取对象属性,触发拦截,将当前属性和tracker 绑定。。。

observable

主要用于创建不同响应式行为的 observable 对象。

  • 例子:修改 obs的值时,会触发执行 tracker
import { observable, autorun } from './@formily/reactive';
//observable是用来创建不同的响应式行为的可观察对象的
const obs = observable({ name: '1' });
//reaction 可订阅对象的订阅者
//当tracker函数的执行的时候,如果函数内部有对observable对象的某个属性进行读操作,则会进行依赖收集
//那么当前的reaction就会与属性进行一个绑定,当属性发生了写操作,就会触发tracker重新执行
const tracker = () => {
  console.log(obs.name);
}
//autorun可以创建一个自动执行的响应器
//可以接收一个tracker函数,如果函数内闻有消费observable数据,数据发生变化的时候tracker会重新执行
autorun(tracker);
obs.name = '2';
  • 源码: import { observable, autorun } from './@formily/reactive';
// 真正的源码里 currentReaction 是数组
let currentReaction; // 当前正执行的tracker行为
const RawReactionsMap = new WeakMap(); // 收集依赖,结构和vue3的类似 {map:[]
export function observable(value) {
    // 劫持代理对象
  return new Proxy(value, baseHandler);
}
const baseHandler: any = {
  get(target, key) {
    const result = target[key];//获取属性的原始值
    //当前存在一个响应器,则把此响应器和对象以及属性进行绑定,
    //以后当此对象的这个属性发生改变后会重新执行此响应器 
      // 对属性进行取值时,将此属性和当前正执行的tracker行为 联立起来
    if (currentReaction) {
      addRawReactionsMap(target, key, currentReaction);
    }
    return result;
  },
  set(target, key, value) {
      // 写值时,触发这个属性对应的 tracker行为,重渲染相关组件
    target[key] = value;
    RawReactionsMap.get(target)?.get(key)?.forEach(reaction => {
        reaction();
    });
    return true;
  }
}
/**
 * 则把此响应器和对象以及属性进行绑定
 * @param target 目标对象
 * @param key 目标属性
 * @param reaction 响应器 
 */
function addRawReactionsMap(target, key, reaction) {
  const reactionsMap = RawReactionsMap.get(target);
  if (reactionsMap) {
    const reactions = reactionsMap.get(key);
    if (reactions) {
      reactions.push(reaction);
    } else {
      reactionsMap.set(key, [reaction])
    }
    return reactionsMap;
  } else {
    const reactionsMap = new Map();
    reactionsMap.set(key, [reaction]);
    RawReactionsMap.set(target, reactionsMap);
    return reactionsMap;
  }
}

export function autorun(tracker) {
  //reaction被 称为响应器,它也是一个函数,vue3是 currentReaction = tracker ,有点不一样,但差不多
  const reaction = () => {
    currentReaction = reaction;
    tracker();
    currentReaction = null;
  }
  reaction();
}

Observer

  • 例子:组件改时,下面的值跟着改,加 Observer只是为了精确渲染,减少多余的渲染
import React from 'react'
import { observable } from '@formily/reactive'
import { Observer } from '@formily/reactive-react'

const obs = observable({
  value: 'Hello world',
})

export default () => {
  return (
    <div>
      <div>
        <Observer>
          {() => (
            <input
              }}
              value={obs.value}
              onChange={(e) => {
                obs.value = e.target.value
              }}
            />
          )}
        </Observer>
      </div>
      <Observer>{() => <div>{obs.value}</div>}</Observer>
    </div>
  )
}
  • 源码:
import { useReducer, useRef, useState } from "react";
import { Tracker } from "../reactive";

export function Observer(props) {
  //先定义一个可以让组件强行刷新的方法
  //const [, forceUpdate] = useReducer(x => x + 1, 0);
  const [, forceUpdate] = useState({});
  //创建一跟踪的ref
  const trackerRef = useRef(null);
  if (!trackerRef.current) {
    //创建一个手动的跟踪器 下面两种都行
    // trackerRef.current = new Tracker(useReducer);
    trackerRef.current = new Tracker(() => forceUpdate({}));
  }
  return trackerRef.current.track(props.children);
}

const baseHandler: any = {
。。。// 这里改了些 优先执行 reaction._scheduler()
  set(target, key, value) {
    target[key] = value;
    RawReactionsMap.get(target)?.get(key)?.forEach(reaction => {
      if (typeof reaction._scheduler === 'function') {
        reaction._scheduler();
      } else {
        reaction();
      }
    });
    return true;
  }
}
export class Tracker {
  constructor(scheduler) {//forceUpdate
    this.track._scheduler = scheduler;//调度更新器
  }
    // 和上面的autorun差不多
  track: any = (tracker) => {
    currentReaction = this.track;
    const result = tracker();
    currentReaction = null;
    return result;
  }
}

字段关联逻辑复杂

怎么处理的?做了模型,里面自带很多属性和方法,做了formpath方便赋值

领域模型

针对表单做的领域模型

https://core.formilyjs.org/zh-CN/api/entry/create-form
import { createForm } from '@formily/core'

const form = createForm({
  initialValues: {
    say: 'hello',
  },
})
https://core.formilyjs.org/zh-CN/api/models/field#%E5%B1%9E%E6%80%A7
调用createField所返回的 Field 模型  
const field = form.createField({name:'xx'})

DDD(领域驱动)

domain-driven design 领域驱动设计。思考问题的方法论,用于对实际问题建模

以一种领域专家、设计人员、开发人员都能理解的通用语言作为相互交流的工具,把概念设计成一个领域模型。用代码实现

form:{值、是否可见、提交}	表单
field:{值、是否可见、设置值} 字段

路径 FormPath

core.formilyjs.org/zh-CN/api/e…

  • 例子
import { FormPath } from '@formily/core';
const target = { array: [] };
//点路径
FormPath.setIn(target, 'a.b.c', 'dotValue');
console.log(FormPath.getIn(target, 'a.b.c'));

//下标
FormPath.setIn(target, 'array.0.d', 'dValue');
console.log(FormPath.getIn(target, 'array.0.d'));

//解构在前后端 数据结构不一致的情况下特别方便
FormPath.setIn(target, 'parent.[f,g]', [1, 2]);
console.log(target); //{a:{b:{c:'dotValue'}},array[{d:'dValue'}],parent:{f:1,g:2}}

生命周期

响应式和路径系统是一个黑盒,怎么实现在某个过程阶段完成一些自定义逻辑

这里面有一堆:该有的都有 有表单的,有字段的

core.formilyjs.org/zh-CN/api/e…

协议驱动

json-schema,可以跨端适配。简单搞笑描述表单逻辑。

但只适合描述数据字段,所以要扩展 去描述ui

扩展

DSL、领域特定语言、domain specific language、具有受限表达性的一种计算机程序语言,受限于领域

x-*表示ui样式

{type:'string',
 'x-component':'Input', //ui 组件

三种表单渲染

jsx

不推荐

import { createForm } from '@formily/core';
import { Field } from '@formily/react';
import 'antd/dist/antd.css';
import { Form, FormItem, Input, NumberPicker } from '@formily/antd';
//纯JSX
const form = createForm();
function App() {
  return (
    <Form form={form} labelCol={6} wrapperCol={5}>
      <Field
        name="name"
        title="姓名"
        required
        component={[Input, {}]}
        decorator={[FormItem, {}]}
      />
      <Field
        name="age"
        title="年龄"
        required
        component={[NumberPicker, {}]}
        decorator={[FormItem, {}]}
      />
    </Form>
  )
}
export default App

json-schema

代码少的时候,推荐,很长的时候不是很推荐


import { createForm } from '@formily/core';
import { Field, createSchemaField } from '@formily/react';
import 'antd/dist/antd.css';
import { Form, FormItem, Input } from '@formily/antd';
//纯JSX
const form = createForm();
const SchemaField = createSchemaField({
  components: {
    FormItem, Input
  }
});
const schema = {
  type: 'object',
  properties: {
    name: {
      title: '姓名',
      type: 'string',
      required: true,
      'x-decorator': 'FormItem',
      'x-component': 'Input'
    },
    email: {
      title: '邮箱',
      type: 'string',
      required: true,
      'x-validator': 'email',
      'x-decorator': 'FormItem',
      'x-component': 'Input'
    }
  }
}
function App() {
  return (
    <Form form={form} labelCol={6} wrapperCol={5}>
      <SchemaField schema={schema} />
    </Form>
  )
}
export default App

markup schema

标注schema,其实也差不多,2.0新添加的,比较推荐

    <Form form={form} labelCol={6} wrapperCol={5}>
      <SchemaField.String
          name = 'name'
          title='xx'
          required
          x-component='Input'
          x-component-props={{}}
          x-decorator='FormItem'
          />
    </Form>

联动校验

react.formilyjs.org/zh-CN/api/s…

effects或者x-reactions里实现

主动联动

reactions包含target

type SchemaReaction<Field = any> =
  | {
      dependencies?: string[] | Record<string, string> //依赖的字段路径列表,只能以点路径描述依赖,支持相对路径,如果是数组格式,那么读的时候也是数组格式,如果是对象格式,读的时候也是对象格式,只是对象格式相当于是一个alias
      when?: string | boolean //联动条件
      target?: string //要操作的字段路径,支持FormPathPattern路径语法,注意:不支持相对路径!!
      effects?: SchemaReactionEffect[] //主动模式下的独立生命周期钩子
      fulfill?: {
        //满足条件
        state?: IGeneralFieldState //更新状态
        schema?: ISchema //更新Schema
        run?: string //执行语句
      }
      otherwise?: {
        //不满足条件
        state?: IGeneralFieldState //更新状态
        schema?: ISchema //更新Schema
        run?: string //执行语句
      }
    }
  • 例子
// 来源 输入123时下面的输入框出现,否则消失
import { createForm } from '@formily/core';
import { Field, createSchemaField } from '@formily/react';
import 'antd/dist/antd.css';
import { Form, FormItem, Input } from '@formily/antd';
//纯JSX
const form = createForm();
const SchemaField = createSchemaField({
  components: {
    FormItem, Input
  }
});
const schema = {
  type: 'object',
  properties: {
    source: {
      title: '来源',
      type: 'string',
      required: true,
      'x-decorator': 'FormItem',
      'x-component': 'Input',
      'x-component-props': {
        'placeholder': '请输入'
      },
      'x-reactions': [
        {
          'target': 'target', // 主动指向目标
          'when': "{{$self.value == '123'}}",
          "fulfill": {
            'state': {
              visible: true
            }
          },
          "otherwise": {
            'state': {
              visible: false
            }
          }
        }
      ]
    },
    target: {
      title: '目标',
      type: 'string',
      'x-decorator': 'FormItem',
      'x-component': 'Input',
      'x-component-props': {
        'placeholder': '请输入'
      }
    }
  }
}
function App() {
  return (
    <Form form={form} labelCol={6} wrapperCol={5}>
      <SchemaField schema={schema} />
    </Form>
  )
}
export default App

被动

和上面的效果一样

const schema = {
  type: 'object',
  properties: {
    source: {
      title: '来源',
      type: 'string',
      required: true,
      'x-decorator': 'FormItem',
      'x-component': 'Input',
      'x-component-props': {
        'placeholder': '请输入'
      }
    },
    target: {
      title: '目标',
      type: 'string',
      'x-decorator': 'FormItem',
      'x-component': 'Input',
      'x-component-props': {
        'placeholder': '请输入'
      },
      'x-reactions': [
        {
          'dependencies': ['source'],
          'when': "{{$deps[0] == '123'}}",
          "fulfill": {
            'state': {
              visible: true
            }
          },
          "otherwise": {
            'state': {
              visible: false
            }
          }
        }
      ]
    }
  }
}

effect

知道个大概行了,真正用再去看

挂载的时候还有更改的时候去判断。

const form = createForm({
  effects() {
    onFieldMount('target', (field: any) => {
      form.setFieldState(field.query('target'), (targetState) => {
        if (field.value === '123') {
          targetState.visible = true;
        } else {
          targetState.visible = false;
        }
      });
    });
    onFieldValueChange('source', (field: any) => {
      form.setFieldState(field.query('target'), (targetState) => {
        if (field.value === '123') {
          targetState.visible = true;
        } else {
          targetState.visible = false;
        }
      });
    });
  }
});