<

1. mobx #

1.1 mobx #

mobx是一个简单可扩展的状态管理库

1.2. mobx vs redux #

mobx学习成本更低,性能更好的的状态解决方案

1.3. 核心思想 #

状态变化引起的副作用应该被自动触发

flow

1.4. mobx #

observable

1.4.1 引用类型 (observable) #

import { observable } from 'mobx';
const p1 = observable([1, 2, 3]);
p1.push(4);
p1.pop();
console.log(p1);
console.log(Array.isArray(p1));

1.4.2 基本类型(observable.box) #

类型 描述
String 字符串
Boolean 布尔值
Number 数字
Symbol 独一无二的值
import { observable } from 'mobx';
let num = observable.box(10);
let str = observable.box('hello');
let bool = observable.box(true);
console.log(num.get(), str.get(), bool.get());
num.set(100);
str.set('world');
bool.set(false);
console.log(num.get(), str.get(), bool.get());

2.formily #

2.1 核心优势 #

2.2 分层架构 #

cells

2.3 竞品对比 #

formcompare

2.4 安装 #

npm init vite@latest
npm install @formily/reactive @formily/core @formily/reactive-react @formily/react @formily/antd ajv less --save

2.5 配置 #

vite.config.ts

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
+ plugins: [react({
+   jsxRuntime: 'classic'
+ })],
+ resolve: {
+   alias: [
+     { find: /^~/, replacement: '' }
+   ]
+ },
+ css: {
+   preprocessorOptions: {
+     less: {
+       // 支持内联 JavaScript
+       javascriptEnabled: true,
+     }
+   }
+ }
})

tsconfig.json

{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
+   "strict": false,
+   "noImplicitAny": false,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

3. 字段数量多 #

3.1 问题 #

3.2 解决方案 #

Reaction

3.2.1 MVVM #

MVVM

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MVVM</title>
</head>
<body>
    <input id="bookTitle" value="红楼梦"/>
    <script>
        class Book{
            constructor(title){
                this.title=title;
            }
        }
        //Model
        let book = new Book('红楼梦');
        //ViewModel
        let viewModel = {display:'block'};
        Object.defineProperty(viewModel,'title',{
                get(){
                    return book.title;
                },
                set(newTitle){
                    bookTitle.value = book.title = newTitle;
                }
        });
        Object.defineProperty(viewModel,'display',{
                get(){
                    return bookTitle.style.display;
                },
                set(display){
                    bookTitle.style.display = display;
                }
        });
        //View(视图层)负责维护 UI 结构与样式
        //同时负责与 ViewModel(视图模型)做数据绑定
        //这里的数据绑定关系是双向的,也就是,ViewModel(视图模型)的数据发生变化,会触发 View(视图层)的更新
        viewModel.title='新红楼梦';
        setTimeout(()=>{
            viewModel.display= 'none';
        },3000);
        //同时视图层的数据变化又会触发 ViewModel(视图模型)的变化
        bookTitle.onchange = (event)=>{
            viewModel.title = event.target.value;
        }
    </script>
</body>
</html>

3.2.2 observable #

3.2.2 Reaction #

3.2.3 autorun #

3.2.4 实现observable #

3.2.4.1 src\main.tsx #

src\main.tsx

import { observable, autorun } from './@formily/reactive'
const obs = observable({
  name: 'zhu',
})
const tracker = () => {
  console.log(obs.name);
}
autorun(tracker)
obs.name = 'feng';
/**
zhu
feng
 */
import { observable, autorun } from '@formily/reactive'
const obs = observable({
  name: 'zhu',
+ age: 12
})
+let counter = 0;
const tracker = () => {
  console.log(obs.name);
+ if (counter++) {
+   console.log(obs.age);
+ }
}
autorun(tracker)
+obs.age = 13;
obs.name = 'feng';
+obs.age = 14;
/**
tracker第1次执行
zhu
tracker第2次执行
feng
13
tracker第3次执行
feng
14
 */
3.2.4.2 reactive\index.ts #

src\@formily\reactive\index.ts

const RawReactionsMap = new WeakMap()
let currentReaction;
export function observable(value) {
    return new Proxy(value, baseHandlers)
}
export const autorun = (tracker) => {
    const reaction = () => {
        currentReaction = reaction;
        tracker()
        currentReaction = null;
    }
    reaction()
}
const baseHandlers = {
    get(target, key) {
        const result = target[key]
        if (currentReaction) {
            addRawReactionsMap(target, key, currentReaction)
        }
        return result
    },
    set(target, key, value) {
        target[key] = value
        RawReactionsMap.get(target)?.get(key)?.forEach((reaction) => reaction())
        return true;
    }
}
const 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
    }
}

3.2.5 Observer #

3.2.5.1 src\main.tsx #

src\main.tsx

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App';
ReactDOM.render(<App />, document.getElementById('root')!);
3.2.5.2 src\App.tsx #

src\App.tsx

import { observable } from './@formily/reactive'
import { Observer } from './@formily/reactive-react'
const username = observable({ value: 'zhangsan' })
const age = observable({ value: 14 })
export default () => {
  return (
    <>
      <Observer>
        {() => (
          <input
            value={username.value}
            onChange={(event) => {
              username.value = event.target.value
            }}
          />
        )}
      </Observer>
      <Observer>{() => {
        console.log('username render');
        return <div>{username.value}</div>;
      }}</Observer>
      <Observer>
        {() => (
          <input
            value={age.value}
            onChange={(event) => {
              age.value = +event.target.value
            }}
          />
        )}
      </Observer>
      <Observer>{() => {
        console.log('age render');
        return <div>{age.value}</div>;
      }}</Observer>
    </>
  )
}
3.2.5.3 reactive-react\index.tsx #

src\@formily\reactive-react\index.tsx

import React, { useReducer } from 'react';
import { Tracker } from '../../@formily/reactive'
export const Observer = (props) => {
    const [, forceUpdate] = useReducer(x => x + 1, 0)
    const trackerRef = React.useRef(null)
    if (!trackerRef.current)
        trackerRef.current = new Tracker(forceUpdate)
    return trackerRef.current.track(props.children)
}
3.2.5.4 reactive\index.ts #

src\@formily\reactive\index.ts

const RawReactionsMap = new WeakMap()
let currentReaction;
export function observable(value) {
    return new Proxy(value, baseHandlers)
}
export const autorun = (tracker) => {
    const reaction = () => {
        currentReaction = reaction;
        tracker()
        currentReaction = null;
    }
    reaction()
}
const baseHandlers = {
    get(target, key) {
        const result = target[key]
        if (currentReaction) {
            addRawReactionsMap(target, key, currentReaction)
        }
        return result
    },
    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;
    }
}
const 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 class Tracker {
+    constructor(scheduler) {
+        this.track._scheduler = scheduler
+    }
+    track: any = (tracker) => {
+        currentReaction = this.track;
+        return tracker()
+    }
+}

4. 字段关联逻辑复杂 #

4.1 问题 #

4.2 领域模型 #

fields

src\main.tsx

import { createForm } from '@formily/core'
const form = createForm()
const field = form.createField({ name: 'target' })

4.3 DDD(领域驱动) #

4.3.1 表单 #

interface Form {
   values,//值
   visible, //是否可见
   submit() //提交
}

4.3.2 字段 #

interface Field {
   value,     //值
   visible,   //是否可见
   setValue() //设置值
}

4.4 路径系统 #

src\main.tsx

import { FormPath } from '@formily/core'
const target = { array: [] }
//点路径 就是我们最常用的a.b.c格式,用点符号来分割每个路径节点,主要用来读写数据
FormPath.setIn(target, 'a.b.c', 'dotValue')
console.log(FormPath.getIn(target, 'a.b.c')) //'dotValue'

//下标路径 对于数组路径,都会有下标,我们的下标可以用点语法,也可以用中括号
FormPath.setIn(target, 'array.0.d', 'arrayValue')
console.log(FormPath.getIn(target, 'array.0.d')) //arrayValue

//解构表达式 解构表达式类似于 ES6 的解构语法,在前后端数据不一致的场景非常适用,解构表达式会作为点路径的某个节点
FormPath.setIn(target, 'parent.[f,g]', [1, 2])

console.log(JSON.stringify(target))
//{"array":[{"d":"arrayValue"}],"a":{"b":{"c":"dotValue"}},"parent":{"f":1,"g":2}}

5. 生命周期 #

5.1 问题 #

5.2 解决方案 #

import { useMemo, useState } from 'react'
import { createForm, onFormInit, onFormReact } from '@formily/core'
export default () => {
  const [state, setState] = useState('init')
  const form = useMemo(
    () =>
      createForm({
        effects() {
          onFormInit(() => {
            setState('表单已初始化')
          })
          onFormReact((form) => {
            if (form.values.input == 'Hello') {
              setState('响应Hello')
            } else if (form.values.input == 'World') {
              setState('响应World')
            }
          })
        },

      }),
    []
  )
  return (
    <div>
      <p>{state}</p>
      <button
        onClick={() => {
          form.setValuesIn('input', 'Hello')
        }}
      >
        Hello
      </button>
      <button
        onClick={() => {
          form.setValuesIn('input', 'World')
        }}
      >
        World
      </button>
    </div>
  )
}

6 协议驱动 #

6.1 问题 #

6.2 解决方案 #

6.3 JSON-Schema #

import Ajv from 'ajv';
const ajv = new Ajv()

const schema = {
  type: "object",
  properties: {
    foo: { type: "integer" },
    bar: { type: "string" }
  },
  required: ["foo"],
  additionalProperties: false
}

const validate = ajv.compile(schema)

const data = {
  foo: 1,
  bar: "abc",
  age: 1
}

const valid = validate(data)
if (!valid)
  console.log(validate.errors)

6.4 扩展的JSON-Schema #

{
  "type": "string",
  "title": "字符串",
  "description": "这是一个字符串",
  "x-component": "Input",//字段 UI 组件属性
  "x-component-props": {//字段 UI 组件属性
    "placeholder": "请输入"
  }
}
{
  "type": "void",
  "title": "卡片",
  "description": "这是一个卡片",
  "x-component": "Card",//字段 UI 组件属性
  "properties": {
    "name": {
      "type": "string",
      "title": "字符串",
      "description": "这是一个字符串",
      "x-component": "Input",//字段 UI 组件属性
      "x-component-props": {//字段 UI 组件属性
        "placeholder": "请输入"
      }
    }
  }
}

6.5 API #

6.5 表单渲染 #

6.5.1 JSX 案例 #

src\App.tsx

import { createForm } from '@formily/core'
import { Field } from '@formily/react'
import 'antd/dist/antd.css'
import { Form, FormItem, Input, NumberPicker } from '@formily/antd'
//createForm创建一个 Form 实例,作为 ViewModel 给 UI 框架层消费
const form = createForm()
function App() {
  return (
    <Form form={form} labelCol={6} wrapperCol={10}>
      <Field
        name="name"
        title="姓名"
        required
        component={[Input]}
        decorator={[FormItem]}
      />
      <Field
        name="age"
        title="年龄"
        validator={{ maximum: 5 }}
        component={[NumberPicker]}
        decorator={[FormItem]}
      />
    </Form>
  )
}
export default App;

6.5.2 JSON Schema案例 #

src\App.tsx

import { createForm } from '@formily/core'
import { createSchemaField } from '@formily/react'
import 'antd/dist/antd.css'
import { Form, FormItem, Input } from '@formily/antd'
const form = createForm()
const SchemaField = createSchemaField({
  components: {
    FormItem,
    Input
  },
})
const schema = {
  type: 'object',
  properties: {
    name: {
      title: `姓名`,
      type: 'string',
      required: true,
      'x-decorator': 'FormItem',//字段 UI 包装器组件
      'x-component': 'Input',//字段 UI 组件属性
    },
    age: {
      title: `邮箱`,
      type: 'string',
      required: true,
      'x-validator': 'email',
      'x-decorator': 'FormItem',//字段 UI 包装器组件
      'x-component': 'Input',//字段 UI 组件属性
    },
  },
}
function App() {
  return (
    <Form form={form} labelCol={6} wrapperCol={10}>
      <SchemaField schema={schema} />
    </Form>
  )
}
export default App;

6.5.3 Markup Schema 案例 #

src\App.tsx

import { createForm } from '@formily/core'
import { createSchemaField } from '@formily/react'
import 'antd/dist/antd.css'
import { Form, FormItem, Input, NumberPicker } from '@formily/antd'
const form = createForm()
const SchemaField = createSchemaField({
  components: {
    Input,
    FormItem,
    NumberPicker
  },
})

function App() {
  return (
    <Form form={form} labelCol={6} wrapperCol={10}>
      <SchemaField>
        <SchemaField.String
          name="name"
          title="姓名"
          required
          x-component="Input"//字段 UI 组件属性
          x-decorator="FormItem"//字段 UI 包装器组件
        />
        <SchemaField.Number
          name="age"
          title="年龄"
          maximum={120}
          x-component="NumberPicker"//字段 UI 组件属性
          x-decorator="FormItem"//字段 UI 包装器组件
        />
      </SchemaField>
    </Form>
  )
}
export default App;

6.6 联动校验 #

6.6.1 主动联动 #

src\App.tsx

import { createForm } from '@formily/core'
import { createSchemaField } from '@formily/react'
import 'antd/dist/antd.css'
import { Form, FormItem, Input } from '@formily/antd'
const form = createForm()
const SchemaField = createSchemaField({
  components: {
    FormItem,
    Input
  },
})
const schema = {
  type: 'object',
  properties: {
    source: {
      title: `来源`,
      type: 'string',
      required: true,
      'x-decorator': 'FormItem',//字段 UI 包装器组件
      'x-component': 'Input',//字段 UI 组件属性
      "x-component-props": {//字段 UI 组件属性
        "placeholder": "请输入"
      },
      "x-reactions": [//字段联动协议
        {
          "target": "target",///要操作的字段路径,支持FormPathPattern路径语法
          //代表当前字段实例,可以在普通属性表达式中使用,也能在 x-reactions 中使用
          "when": "{{$self.value == '123'}}",//联动条件
          "fulfill": { //满足条件
            "state": {//更新状态
              "visible": true
            }
          },
          "otherwise": { //不满足条件
            "state": {//更新状态
              "visible": false
            }
          }
        }
      ]
    },
    target: {
      "title": "目标",
      "type": "string",
      "x-component": "Input",//字段 UI 组件属性
      "x-component-props": {//字段 UI 组件属性
        "placeholder": "请输入"
      },
      'x-decorator': 'FormItem'//字段 UI 包装器组件
    }
  },
}
function App() {
  return (
    <Form form={form} labelCol={6} wrapperCol={10}>
      <SchemaField schema={schema} />
    </Form>
  )
}
export default App;

6.6.2 被动联动 #

src\App.tsx

import { createForm } from '@formily/core'
import { createSchemaField } from '@formily/react'
import 'antd/dist/antd.css'
import { Form, FormItem, Input } from '@formily/antd'
const form = createForm()
const SchemaField = createSchemaField({
  components: {
    FormItem,
    Input
  },
})
const schema = {
  type: 'object',
  properties: {
    source: {
      title: `来源`,
      type: 'string',
      required: true,
      'x-decorator': 'FormItem',//字段 UI 包装器组件
      'x-component': 'Input',//字段 UI 组件属性
      "x-component-props": {//字段 UI 组件属性
        "placeholder": "请输入"
      }
    },
    target: {
      "title": "目标",
      "type": "string",
      "x-component": "Input",//字段 UI 组件属性
      "x-component-props": {//字段 UI 组件属性
        "placeholder": "请输入"
      },
      'x-decorator': 'FormItem',//字段 UI 包装器组件
      "x-reactions": [//字段联动协议
        {
          "dependencies": ["source"],
          //只能在x-reactions中的表达式消费,与 x-reactions 定义的 dependencies 对应,数组顺序一致
          "when": "{{$deps[0] == '123'}}",
          "fulfill": {
            "state": {
              "visible": true
            }
          },
          "otherwise": {
            "state": {
              "visible": false
            }
          }
        }
      ]
    }
  },
}
function App() {
  return (
    <Form form={form} labelCol={6} wrapperCol={10}>
      <SchemaField schema={schema} />
    </Form>
  )
}
export default App;

6.6.3 effects #

src\App.tsx

import { createForm, onFieldMount, onFieldValueChange } from '@formily/core'
import { createSchemaField } from '@formily/react'
import 'antd/dist/antd.css'
import { Form, FormItem, Input } from '@formily/antd'
const form = createForm({
  effects() {//effects 副作用逻辑,用于实现各种联动逻辑
    //用于监听某个字段已挂载的副作用钩子
    onFieldMount('target', (field: any) => {
      //可以设置字段状态
      form.setFieldState(field.query('target'), (state) => {
        if (field.value === '123') {
          state.visible = true;
        } else {
          state.visible = false;
        }
      })
    })
    //用于监听某个字段值变化的副作用钩子
    onFieldValueChange('source', (field: any) => {
      form.setFieldState(field.query('target'), (state) => {
        if (field.value === '123') {
          state.visible = true;
        } else {
          state.visible = false;
        }
      })
    })
  },
})
const SchemaField = createSchemaField({
  components: {
    FormItem,
    Input
  },
})
const schema = {
  type: 'object',
  properties: {
    source: {
      title: `来源`,
      type: 'string',
      required: true,
      'x-decorator': 'FormItem',//字段 UI 包装器组件
      'x-component': 'Input',//字段 UI 组件属性
      "x-component-props": {//字段 UI 组件属性
        "placeholder": "请输入"
      }
    },
    target: {
      "title": "目标",
      "type": "string",
      'x-decorator': 'FormItem',//字段 UI 包装器组件
      "x-component": "Input",//字段 UI 组件属性
      "x-component-props": {//字段 UI 组件属性
        "placeholder": "请输入"
      }
    }
  }
}
function App() {
  return (
    <Form form={form} labelCol={6} wrapperCol={10}>
      <SchemaField schema={schema} />
    </Form>
  )
}
export default App;

7.案例 #

7.1.注册 #

import React from 'react'
import { createForm } from '@formily/core'
import { createSchemaField } from '@formily/react'
import {
  Form,
  FormItem,
  FormLayout,
  Input,
  Select,
  Password,
  Cascader,
  DatePicker,
  Submit,
  Space,
  FormGrid,
  Upload,
  ArrayItems,
  Editable,
  FormButtonGroup,
} from '@formily/antd'
import { action } from '@formily/reactive'
import { Card, Button } from 'antd'
import { UploadOutlined } from '@ant-design/icons'

const form = createForm({
  validateFirst: true,
})

const IDUpload = (props) => {
  return (
    <Upload
      {...props}
      action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
      headers={{
        authorization: 'authorization-text',
      }}
    >
      <Button icon={<UploadOutlined />}>上传复印件</Button>
    </Upload>
  )
}

const SchemaField = createSchemaField({
  components: {
    FormItem,
    FormGrid,
    FormLayout,
    Input,
    DatePicker,
    Cascader,
    Select,
    Password,
    IDUpload,
    Space,
    ArrayItems,
    Editable,
  },
  scope: {
    fetchAddress: (field) => {
      const transform = (data = {}) => {
        return Object.entries(data).reduce((buf, [key, value]) => {
          if (typeof value === 'string')
            return buf.concat({
              label: value,
              value: key,
            })
          const { name, code, cities, districts } = value
          const _cities = transform(cities)
          const _districts = transform(districts)
          return buf.concat({
            label: name,
            value: code,
            children: _cities.length
              ? _cities
              : _districts.length
              ? _districts
              : undefined,
          })
        }, [])
      }

      field.loading = true
      fetch('//unpkg.com/china-location/dist/location.json')
        .then((res) => res.json())
        .then(
          action.bound((data) => {
            field.dataSource = transform(data)
            field.loading = false
          })
        )
    },
  },
})

export default () => {
  return (
    <div
      style={{
        display: 'flex',
        justifyContent: 'center',
        background: '#eee',
        padding: '40px 0',
      }}
    >
      <Card title="新用户注册" style={{ width: 620 }}>
        <Form
          form={form}
          labelCol={5}
          wrapperCol={16}
          onAutoSubmit={console.log}
        >
          <SchemaField>
            <SchemaField.String
              name="username"
              title="用户名"
              required
              x-decorator="FormItem"
              x-component="Input"
            />
            <SchemaField.String
              name="password"
              title="密码"
              required
              x-decorator="FormItem"
              x-component="Password"
              x-component-props={{
                checkStrength: true,
              }}
              x-reactions={[
                {
                  dependencies: ['.confirm_password'],
                  fulfill: {
                    state: {
                      selfErrors:
                        '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}',
                    },
                  },
                },
              ]}
            />
            <SchemaField.String
              name="confirm_password"
              title="确认密码"
              required
              x-decorator="FormItem"
              x-component="Password"
              x-component-props={{
                checkStrength: true,
              }}
              x-reactions={[
                {
                  dependencies: ['.password'],
                  fulfill: {
                    state: {
                      selfErrors:
                        '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}',
                    },
                  },
                },
              ]}
            />
            <SchemaField.Void
              title="姓名"
              x-decorator="FormItem"
              x-decorator-props={{
                asterisk: true,
                feedbackLayout: 'none',
              }}
              x-component="FormGrid"
            >
              <SchemaField.String
                name="firstName"
                x-decorator="FormItem"
                x-component="Input"
                x-component-props={{
                  placeholder: '姓',
                }}
                required
              />
              <SchemaField.String
                name="lastName"
                x-decorator="FormItem"
                x-component="Input"
                x-component-props={{
                  placeholder: '名',
                }}
                required
              />
            </SchemaField.Void>
            <SchemaField.String
              name="email"
              title="邮箱"
              required
              x-validator="email"
              x-decorator="FormItem"
              x-component="Input"
            />
            <SchemaField.String
              name="gender"
              title="性别"
              x-decorator="FormItem"
              x-component="Select"
              enum={[
                {
                  label: '男',
                  value: 1,
                },
                {
                  label: '女',
                  value: 2,
                },
                {
                  label: '第三性别',
                  value: 3,
                },
              ]}
              required
            />
            <SchemaField.String
              name="birthday"
              title="生日"
              required
              x-decorator="FormItem"
              x-component="DatePicker"
            />
            <SchemaField.String
              name="address"
              title="地址"
              required
              x-decorator="FormItem"
              x-component="Cascader"
              x-reactions="{{fetchAddress}}"
            />
            <SchemaField.String
              name="idCard"
              title="身份证复印件"
              required
              x-decorator="FormItem"
              x-component="IDUpload"
            />
            <SchemaField.Array
              name="contacts"
              title="联系人信息"
              required
              x-decorator="FormItem"
              x-component="ArrayItems"
            >
              <SchemaField.Object x-component="ArrayItems.Item">
                <SchemaField.Void
                  x-decorator="FormItem"
                  x-component="ArrayItems.SortHandle"
                />
                <SchemaField.Void
                  name="popover"
                  title="维护联系人信息"
                  x-decorator="Editable.Popover"
                  x-component="FormLayout"
                  x-component-props={{
                    layout: 'vertical',
                  }}
                  x-reactions={[
                    {
                      dependencies: ['.popover.name'],
                      fulfill: {
                        schema: {
                          title: '{{$deps[0]}}',
                        },
                      },
                    },
                  ]}
                >
                  <SchemaField.String
                    name="name"
                    required
                    title="姓名"
                    x-decorator="FormItem"
                    x-component="Input"
                    x-component-props={{
                      style: {
                        width: 300,
                      },
                    }}
                  />
                  <SchemaField.String
                    name="email"
                    title="邮箱"
                    x-validator={[{ required: true }, 'email']}
                    x-decorator="FormItem"
                    x-component="Input"
                    x-component-props={{
                      style: {
                        width: 300,
                      },
                    }}
                  />
                  <SchemaField.String
                    name="phone"
                    required
                    title="手机号"
                    x-validator="phone"
                    x-decorator="FormItem"
                    x-component="Input"
                    x-component-props={{
                      style: {
                        width: 300,
                      },
                    }}
                  />
                </SchemaField.Void>
                <SchemaField.Void
                  x-decorator="FormItem"
                  x-component="ArrayItems.Remove"
                />
              </SchemaField.Object>
              <SchemaField.Void
                x-component="ArrayItems.Addition"
                title="新增联系人"
              />
            </SchemaField.Array>
          </SchemaField>
          <FormButtonGroup.FormItem>
            <Submit block size="large">
              注册
            </Submit>
          </FormButtonGroup.FormItem>
        </Form>
      </Card>
    </div>
  )
}

7.2 登录 #

import React from 'react'
import { createForm } from '@formily/core'
import { createSchemaField } from '@formily/react'
import { Form, FormItem, Input, Password, Submit } from '@formily/antd'
import { Tabs, Card } from 'antd'
import * as ICONS from '@ant-design/icons'
import { VerifyCode } from './VerifyCode'

const normalForm = createForm({
  validateFirst: true,
})

const phoneForm = createForm({
  validateFirst: true,
})

const SchemaField = createSchemaField({
  components: {
    FormItem,
    Input,
    Password,
    VerifyCode,
  },
  scope: {
    icon(name) {
      return React.createElement(ICONS[name])
    },
  },
})

export default () => {
  return (
    <div
      style={{
        display: 'flex',
        justifyContent: 'center',
        background: '#eee',
        padding: '40px 0',
      }}
    >
      <Card style={{ width: 400 }}>
        <Tabs style={{ overflow: 'visible', marginTop: -10 }}>
          <Tabs.TabPane key="1" tab="账密登录">
            <Form
              form={normalForm}
              layout="vertical"
              size="large"
              onAutoSubmit={console.log}
            >
              <SchemaField>
                <SchemaField.String
                  name="username"
                  title="用户名"
                  required
                  x-decorator="FormItem"
                  x-component="Input"
                  x-validator={{
                    required: true,
                  }}
                  x-component-props={{
                    prefix: "{{icon('UserOutlined')}}",
                  }}
                />
                <SchemaField.String
                  name="password"
                  title="密码"
                  required
                  x-decorator="FormItem"
                  x-component="Input"
                  x-component-props={{
                    prefix: "{{icon('LockOutlined')}}",
                  }}
                />
              </SchemaField>
              <Submit block size="large">
                登录
              </Submit>
            </Form>
          </Tabs.TabPane>
          <Tabs.TabPane key="2" tab="手机登录">
            <Form
              form={phoneForm}
              layout="vertical"
              size="large"
              onAutoSubmit={console.log}
            >
              <SchemaField>
                <SchemaField.String
                  name="phone"
                  title="手机号"
                  required
                  x-validator="phone"
                  x-decorator="FormItem"
                  x-component="Input"
                  x-component-props={{
                    prefix: "{{icon('PhoneOutlined')}}",
                  }}
                />
                <SchemaField.String
                  name="verifyCode"
                  title="验证码"
                  required
                  x-decorator="FormItem"
                  x-component="VerifyCode"
                  x-component-props={{
                    prefix: "{{icon('LockOutlined')}}",
                  }}
                  x-reactions={[
                    {
                      dependencies: ['.phone#value', '.phone#valid'],
                      fulfill: {
                        state: {
                          'component[1].readyPost': '{{$deps[0] && $deps[1]}}',
                          'component[1].phoneNumber': '{{$deps[0]}}',
                        },
                      },
                    },
                  ]}
                />
              </SchemaField>
              <Submit block size="large">
                登录
              </Submit>
            </Form>
          </Tabs.TabPane>
        </Tabs>
        <div
          style={{
            display: 'flex',
            justifyContent: 'space-between',
          }}
        >
          <a href="#新用户注册">新用户注册</a>
          <a href="#忘记密码">忘记密码?</a>
        </div>
      </Card>
    </div>
  )
}
import React, { useState } from 'react'
import { Input, Button } from 'antd'

interface IVerifyCodeProps {
  value?: any
  onChange?: (value: any) => void
  readyPost?: boolean
  phoneNumber?: number
  style?: React.CSSProperties
}

export const VerifyCode: React.FC<React.PropsWithChildren<IVerifyCodeProps>> =
  ({ value, onChange, readyPost, phoneNumber, ...props }) => {
    const [lastTime, setLastTime] = useState(0)

    const counting = (time = 20) => {
      if (time < 0) return
      setLastTime(time)
      setTimeout(() => {
        counting(time - 1)
      }, 1000)
    }

    return (
      <div
        style={{ display: 'inline-flex', width: '100%', alignItems: 'center' }}
      >
        <Input
          {...props}
          style={{ marginRight: 5, ...props.style }}
          value={value}
          onChange={onChange}
        />
        <div
          style={{
            flexShrink: 0,
            color: '#999',
            width: 100,
            height: 35,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          {lastTime === 0 && (
            <Button
              disabled={!readyPost}
              block
              onClick={() => {
                if (phoneNumber) {
                  console.log(`post code by phone number ${phoneNumber}`)
                }
                counting()
              }}
            >
              发送验证码
            </Button>
          )}
          {lastTime > 0 && <span>剩余{lastTime}秒</span>}
        </div>
      </div>
    )
  }