0 概述
Formily的React库经验汇总,官方文档在这里
我们在Core库中看到了,Formily是如何实现一个ViewModel层的。我们也看到了我们使用自定义的UI如何接入的Core库,在接入的过程中,我们也发现了一点问题:
- Field组件重复,每次自定义UI都需要用到
- Context组件重复,总是需要建立Form与Field的Context,这点也麻烦
- 自定义UI组件暂时无法实现嵌套的组件,例如一个ObjectField组件下面优雅地嵌入一个Field组件
Formily的React库的目标就是解决以上的这些问题,让我们自定义的UI组件能更快地接入Formily体系之中,它将自己定义为ViewModel层与View层之间的胶水层,它包括的内容有:
- 胶水组件,它定义好了Field,ArrayField,ObjectField与VoidField,能快速地让我们嵌入自己的自定义UI组件。
- Schema组件,Formily漂亮的地方在于,它更进一步地扩展胶水组件的含义。它允许开发者动态传入一个JSON Schema或者Markup Schema,然后由它来统一渲染组件。聪明的你已经知道,这其实是在为后续的低代码开发做了铺垫。而且,这种方法带来的代码可维护性与可读性也更好。
1 胶水组件
代码在这里
1.1 Field组件
import React, { ReactChild } from 'react';
import { createForm, Form } from '@formily/core';
import { FormProvider, Field, FormConsumer } from '@formily/react';
import { Input } from 'antd';
const form = createForm({
effects: () => {},
});
export default () => {
return (
<FormProvider form={form}>
<Field
name="input"
component={[Input, { placeholder: 'Please Input' }]}
/>
<FormConsumer>
{(form: Form) => {
return JSON.stringify(form.values) as ReactChild;
}}
</FormConsumer>
</FormProvider>
);
};
Field组件就像我们原来的用法,在component属性写入组件,就能实现自定绑定组件了。
1.2 ObjectField组件
import { Field, ObjectField } from '@formily/core';
import { useField } from '@formily/react';
import React, { ReactNode, useContext } from 'react';
export default (props: { children: ReactNode }) => {
const field = useField();
return (
<div
style={{
border: '2px solid rgb(186 203 255)',
}}
>
<h2>{field.title}</h2>
<div style={{ padding: '10px' }}>{props.children}</div>
</div>
);
};
我们先定义一个Card组件,在于ObjectField的容器。useField是react自带的属性,它会拉取当前父节点中最接近的Field属性(Core库里面的Field,不是React库的Field)。
import React, { ReactChild } from 'react';
import { createForm, Form } from '@formily/core';
import {
FormProvider,
Field,
FormConsumer,
ObjectField,
VoidField,
} from '@formily/react';
import { Input } from 'antd';
import Card from './Card';
import { FormItem, NumberPicker } from '@formily/antd';
const form = createForm({
effects: () => {},
});
export default () => {
return (
<FormProvider form={form}>
<ObjectField name="person" title="个人信息" component={[Card, {}]}>
<Field
name="name"
title="姓名"
required={true}
component={[Input, {}]}
decorator={[FormItem, {}]}
/>
<Field
name="age"
title="年龄"
required={true}
component={[NumberPicker, {}]}
decorator={[FormItem, {}]}
/>
</ObjectField>
<FormConsumer>
{(form: Form) => {
return JSON.stringify(form.values) as ReactChild;
}}
</FormConsumer>
</FormProvider>
);
};
然后我们建立一个ObjectField组件,compnent是Card就可以了。最后,在ObjectField组件里面嵌套Field组件就可以了。另外,Formily在ObjectField组件的实现中,它会自动将ObjectField组件嵌套的children组件传递给Card这个组件,所以,你看到了Card组件中用到了props.children这个变量。
{
"person":{
"name":"123",
"age":12
}
}
注意,最后生成表单的格式是以上的这种格式,数据嵌套在person的子属性里面。而且,我们的Field组件只需要写name,而不是person.name就可以了。Field组件自己会知道自己嵌套在什么容器组件的下面。
1.3 VoidField组件
import React, { ReactChild } from 'react';
import { createForm, Form } from '@formily/core';
import {
FormProvider,
Field,
FormConsumer,
ObjectField,
VoidField,
} from '@formily/react';
import { Input } from 'antd';
import Card from './Card';
import { FormItem, FormLayout, NumberPicker } from '@formily/antd';
const form = createForm({
effects: () => {},
});
export default () => {
return (
<FormProvider form={form}>
<VoidField
name="layout"
component={[FormLayout, { labelCol: 6, wrapperCol: 10 }]}
>
<ObjectField
name="person"
title="个人信息"
component={[Card, {}]}
>
<Field
name="name"
title="姓名"
required={true}
component={[Input, {}]}
decorator={[FormItem, {}]}
/>
<Field
name="age"
title="年龄"
required={true}
component={[NumberPicker, {}]}
decorator={[FormItem, {}]}
/>
</ObjectField>
</VoidField>
<FormConsumer>
{(form: Form) => {
return JSON.stringify(form.values) as ReactChild;
}}
</FormConsumer>
</FormProvider>
);
};
VoidField组件的用法与ObjectField是类似的,只不过它是没有value的而已。我们这里在VoidField组件中使用了FormLayout作为组件。
1.4 ArrayField组件
ArrayField组件是最为复杂的组件,一方面,它想ObjectField一样,作为容器组件可以嵌套其他的基础组件。另外一方面,ArrayField都是像表格与TabPane的组件,除了展示底层的基础组件,还需要自己显示列头,列宽,添加按钮等的组件。因此,在Formily里面,ArrayField组件它不会自动解析children组件再交给ArrayField组件来渲染,它只会直接将整个schema交给ArrayField,由ArrayField来确定怎样渲染。
import { ArrayField, Field } from '@formily/core';
import { useField } from '@formily/react';
import { observer } from '@formily/reactive-react';
import React, { ReactNode, useContext } from 'react';
import { ReactElement } from 'react';
type PropsType = Field & {
children: (index: number) => ReactElement;
};
export default observer((props: PropsType) => {
const field = useField<ArrayField>();
return (
<div
style={{
border: '2px solid rgb(186 203 255)',
}}
>
<div style={{ padding: '10px' }}>
{field.value?.map((item, index) => {
return (
<div key={index}>
<div>
{field.componentProps.childrenRender(index)}
</div>
<button
onClick={() => {
field.remove(index);
}}
>
删除
</button>
</div>
);
})}
</div>
<button
onClick={() => {
field.push({});
}}
>
添加一行
</button>
</div>
);
});
我们先定义一个ArrayItems组件,它使用componentProps里面的childrenRender来渲染子组件。注意,childrenRender是一个Render Props,不是ReactNode。
import React, { ReactChild } from 'react';
import { createForm, Form } from '@formily/core';
import {
FormProvider,
Field,
FormConsumer,
ObjectField,
VoidField,
ArrayField,
} from '@formily/react';
import { Input } from 'antd';
import Card from './Card';
import { FormItem, FormLayout, NumberPicker } from '@formily/antd';
import ArrayItems from './ArrayItems';
const form = createForm({
effects: () => {},
});
export default () => {
//使用ArrayField传递描述数组数据,注意不能用props.children来传递
return (
<FormProvider form={form}>
<VoidField
name="layout"
component={[FormLayout, { labelCol: 6, wrapperCol: 10 }]}
>
<ObjectField
name="person"
title="个人信息"
component={[Card, {}]}
>
<Field
name="name"
title="姓名"
required={true}
component={[Input, {}]}
decorator={[FormItem, {}]}
/>
<Field
name="age"
title="年龄"
required={true}
component={[NumberPicker, {}]}
decorator={[FormItem, {}]}
/>
</ObjectField>
<ArrayField
name="contact"
title="联系信息"
component={[
ArrayItems,
{
childrenRender: (index: number) => {
return (
<ObjectField
name={index + ''}
title="信息"
component={[Card, {}]}
>
<Field
name="phone"
title="电话"
required={true}
validator={{ format: 'phone' }}
component={[Input, {}]}
decorator={[FormItem, {}]}
/>
<Field
name="email"
title="电子邮件"
required={true}
validator={{ format: 'email' }}
component={[Input, {}]}
decorator={[FormItem, {}]}
/>
</ObjectField>
);
},
},
]}
></ArrayField>
</VoidField>
<FormConsumer>
{(form: Form) => {
return JSON.stringify(form.values) as ReactChild;
}}
</FormConsumer>
</FormProvider>
);
};
最后我们用ArrayField与ArrayItems来渲染了这个允许自增的表单列表组件。
2 Schema组件
schema组件是整个React库中最为漂亮的部分,在看示例代码的时候,不妨思考一下,Formily是怎样实现这个组件的。代码在这里。Formily为ant design包装的组件,全部都是使用Schema的方式包装,所以,这一部分是必须要被掌握的。
2.1 Json Schema
import { ArrayField, Field } from '@formily/core';
import { RecursionField, useField, useFieldSchema } from '@formily/react';
import { observer } from '@formily/reactive-react';
import React, { ReactNode, useContext } from 'react';
import { ReactElement } from 'react';
type PropsType = Field & {
children: (index: number) => ReactElement;
};
export default observer((props: PropsType) => {
const field = useField<ArrayField>();
const fieldSchema = useFieldSchema();
return (
<div
style={{
border: '2px solid rgb(186 203 255)',
}}
>
<div style={{ padding: '10px' }}>
{field.value?.map((item, index) => {
return (
<div key={index}>
<div>
<RecursionField
name={index}
schema={fieldSchema.items!}
/>
</div>
<button
onClick={() => {
field.remove(index);
}}
>
删除
</button>
</div>
);
})}
</div>
<button
onClick={() => {
field.push({});
}}
>
添加一行
</button>
</div>
);
});
首先,我们重写ArrayItems组件,这个时候,它不是通过componentProps来选择子组件,而是通过useFieldSchema来获取自身的Schema,然后交给RecursionField来渲染。(这里刚开始我也看不懂,直到后面的源代码才知道啥意思。)
import React, { ReactChild } from 'react';
import { createForm, Form } from '@formily/core';
import {
FormProvider,
Field,
FormConsumer,
ObjectField,
VoidField,
ArrayField,
createSchemaField,
ISchema,
} from '@formily/react';
import { Input } from 'antd';
import Card from './Card';
import { FormItem, FormLayout, NumberPicker } from '@formily/antd';
import ArrayItems from './ArrayItems';
const form = createForm({
effects: () => {},
});
//创建SchemaField的时候,就已经有options
const SchemaField = createSchemaField({
components: {
Input,
NumberPicker,
Card,
FormLayout,
FormItem,
ArrayItems,
},
});
const schema: ISchema = {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': { labelCol: 6, wrapperCol: 10 },
properties: {
person: {
type: 'object',
title: '个人信息',
'x-component': 'Card',
'x-decorator': 'FormItem',
properties: {
name: {
type: 'string',
title: '姓名',
required: true,
'x-component': 'Input',
'x-component-props': {},
'x-decorator': 'FormItem',
},
age: {
type: 'number',
title: '年龄',
required: true,
'x-component': 'NumberPicker',
'x-component-props': {},
'x-decorator': 'FormItem',
},
},
},
contact: {
type: 'array',
title: '联系信息',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
items: {
type: 'object',
title: '信息',
'x-component': 'Card',
properties: {
phone: {
type: 'string',
title: '电话',
format: 'phone',
required: true,
'x-component': 'Input',
'x-component-props': {},
'x-decorator': 'FormItem',
},
email: {
type: 'string',
title: '电子邮件',
format: 'email',
required: true,
'x-component': 'Input',
'x-component-props': {},
'x-decorator': 'FormItem',
},
},
},
},
},
};
export default () => {
//使用schema
return (
<FormProvider form={form}>
<SchemaField schema={schema} />
<FormConsumer>
{(form: Form) => {
return JSON.stringify(form.values) as ReactChild;
}}
</FormConsumer>
</FormProvider>
);
};
最后,我们用SchemaField组件与json schema就能渲染这个一样的页面。没有闭包,没有if语句,就是简单的json就能表达整个页面,注意这里与直接用ArrayField组件的不同。
另外,Object Schema的对象,总是含有properties属性。而Array Schema的对象,不仅含有properties属性(描述有哪些额外的按钮),还有items属性(描述数组中的每个元素应该怎么渲染)。
2.2 Markup Schema
import React, { ReactChild } from 'react';
import { createForm, Form } from '@formily/core';
import {
FormProvider,
Field,
FormConsumer,
ObjectField,
VoidField,
ArrayField,
createSchemaField,
ISchema,
} from '@formily/react';
import { Input } from 'antd';
import Card from './Card';
import { FormItem, FormLayout, NumberPicker } from '@formily/antd';
import ArrayItems from './ArrayItems';
const form = createForm({
effects: () => {},
});
//创建SchemaField的时候,就已经有options
const SchemaField = createSchemaField({
components: {
Input,
NumberPicker,
Card,
FormLayout,
FormItem,
ArrayItems,
},
});
const schema: ISchema = {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': { labelCol: 6, wrapperCol: 10 },
properties: {
person: {
type: 'object',
title: '个人信息',
'x-component': 'Card',
'x-decorator': 'FormItem',
properties: {
name: {
type: 'string',
title: '姓名',
required: true,
'x-component': 'Input',
'x-component-props': {},
'x-decorator': 'FormItem',
},
age: {
type: 'number',
title: '年龄',
required: true,
'x-component': 'NumberPicker',
'x-component-props': {},
'x-decorator': 'FormItem',
},
},
},
contact: {
type: 'array',
title: '联系信息',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
items: {
type: 'object',
title: '信息',
'x-component': 'Card',
properties: {
phone: {
type: 'string',
title: '电话',
format: 'phone',
required: true,
'x-component': 'Input',
'x-component-props': {},
'x-decorator': 'FormItem',
},
email: {
type: 'string',
title: '电子邮件',
format: 'email',
required: true,
'x-component': 'Input',
'x-component-props': {},
'x-decorator': 'FormItem',
},
},
},
},
},
};
export default () => {
//使用schema
return (
<FormProvider form={form}>
<SchemaField>
<SchemaField.Void
x-component="FormLayout"
x-component-props={{ labelCol: 6, wrapperCol: 10 }}
>
<SchemaField.Object
title="个人信息"
name="person"
x-component={'Card'}
x-decorator={'FormItem'}
>
<SchemaField.String
title="姓名"
name="name"
required={true}
x-component={'Input'}
x-decorator={'FormItem'}
/>
<SchemaField.Number
title="年龄"
name="age"
required={true}
x-component={'NumberPicker'}
x-decorator={'FormItem'}
/>
</SchemaField.Object>
<SchemaField.Array
title="个人信息"
name="contact"
x-component={'ArrayItems'}
x-decorator={'FormItem'}
>
<SchemaField.Object
title="信息"
x-component={'Card'}
x-decorator={'FormItem'}
>
<SchemaField.String
title="电话"
name="phone"
required={true}
format="phone"
x-component={'Input'}
x-decorator={'FormItem'}
/>
<SchemaField.Number
title="电子邮件2"
name="email"
required={true}
format="email"
x-component={'Input'}
x-decorator={'FormItem'}
/>
</SchemaField.Object>
</SchemaField.Array>
</SchemaField.Void>
</SchemaField>
<FormConsumer>
{(form: Form) => {
return JSON.stringify(form.values) as ReactChild;
}}
</FormConsumer>
</FormProvider>
);
};
JSON Schema的特点是,容易被后端服务器二次处理,也容易被其他编辑器自动生成,但是代码的可读性不好。因此,Formily提供了不需要动态化支持的Markup Schema,它的思路是,以ReactElement的语法,在SchemaField组件中写组件,这部分组件会先转换为json代码,然后再被SchemaField根据json渲染出来。这种Markup Schema的写法可读性更好,而且在typescript环境中有更好的提示。
3 胶水组件原理
我们来尝试一下,只有core库如何实现ObjectField与ArrayField组件。
3.1 ObjectField
代码在这里
import React, {
createContext,
ReactElement,
useContext,
FunctionComponent,
ReactNode,
Component,
} from 'react';
import { observer } from '@formily/reactive-react';
import { Field, Form, IFieldFactoryProps } from '@formily/core';
//创建上下文,方便Field消费
const FormContext = createContext<Form>({} as Form);
//创建上下文,方便FormItem消费
const FieldContext = createContext<Field>({} as Field);
export { FormContext };
export { FieldContext };
//表单管理入口
type FormProviderProps = {
form: Form;
children: ReactNode;
};
export const FormProvider = (props: FormProviderProps) => {
return (
<FormContext.Provider value={props.form}>
{props.children}
</FormContext.Provider>
);
};
let nameId = 0;
function randomName():string{
let id = nameId++;
return "random_"+id;
}
type FormConsumerProps = {
children:(form:Form)=>ReactElement
}
export const FormConsumer = observer((props:FormConsumerProps)=>{
const form = useContext(FormContext);
return props.children(form);
})
//状态桥接器组件
const ReactiveField =
(props: {field:Field}&{otherProps?:object} ) => {
let field = props.field;
console.log('Child Component Field: ' + field.address + ' Render');
if (!field.visible) return null;
//渲染字段,将字段状态与UI组件关联
//传入children
const component = React.createElement(
(field.component[0] as unknown) as string,
{
...field.componentProps,
...props.otherProps,
} as React.Attributes,
props.children,
);
//渲染字段包装器
const decorator = React.createElement(
(field.decorator[0] as unknown) as string,
field.decoratorProps,
component,
);
return (
<FieldContext.Provider value={field}>
{decorator}
</FieldContext.Provider>
);
};
export const MyObjectField = observer((props:IFieldFactoryProps<any, any, any, any>&{children?:ReactNode[]})=>{
const form = useContext(FormContext);
const parent = useContext(FieldContext);
const name = props.name ? props.name:randomName();
const field = form.createObjectField({
...props,
name:name,
basePath:parent?.address,
});
return <ReactiveField field={field}>{props.children}</ReactiveField>
})
export const MyField = observer((props:IFieldFactoryProps<any, any, any, any>)=>{
const form = useContext(FormContext);
const parent = useContext(FieldContext);
const name = props.name ? props.name:randomName();
const field = form.createField({
...props,
name:name,
basePath:parent?.address,
});
return <ReactiveField field={field} otherProps={{value:field.value,onChange:field.onInput}}/>
})
这里的代码与Core库的实现很相似,注意点如下:
- MyObjectField使用createObjectField,而不是createField来创建Field,并且向component透传了children属性。这点实现了MyObjectField的children自动渲染。
- MyField组件总是先用useContext(FieldContext)来获取上级的Field组件,然后在createField的时候,将上级的field的address填入basePath属性中。这点实现了Field在多层嵌套以后依然能知道自己在哪一级的Field下面。
import { autorun, observable } from '@formily/reactive';
import { observer } from '@formily/reactive-react';
import { FormConsumer, FormProvider, MyField, MyObjectField } from './Context';
import Input from './Input';
import InputDigit from './InputDigit';
import Password from './Password';
import Label from './Label';
import FormItem from './FormItem';
import RequireValidator from './RequireValidator';
import { useMemo } from 'react';
import { createForm, Field, onFieldReact } from '@formily/core';
import Card from './Card';
export default () => {
console.log('Top Render');
const form = useMemo(() => {
return createForm({
effects: () => {
onFieldReact('nameLength', (field) => {
let field2 = field as Field;
field2.value = field2.query('.name').value()?.length;
});
},
});
}, []);
return (
<FormProvider form={form}>
<MyObjectField
title="个人信息"
name="person"
component={[Card,{}]}
decorator={[FormItem]}>
<MyField
title="名称"
name="name"
required
component={[Input, {}]}
decorator={[FormItem]}
/>
<MyField
title="年龄"
name="age"
required
component={[InputDigit, {}]}
decorator={[FormItem, { style: { height: 30 } }]}
/>
</MyObjectField>
<MyObjectField
title="联系信息"
name="contact"
component={[Card,{}]}
decorator={[FormItem]}>
<MyField
title="电话"
name="phone"
validator={{
format:'phone'
}}
required
component={[Input, {}]}
decorator={[FormItem]}
/>
<MyField
title="邮件"
name="email"
validator={{
format:'email'
}}
required
component={[Input, {}]}
decorator={[FormItem]}
/>
</MyObjectField>
<FormConsumer>
{(form)=>{
return (<div>{JSON.stringify(form.values)}</div>);
}}
</FormConsumer>
</FormProvider>
);
};
这是测试代码
3.2 ArrayField
代码在这里
export const MyArrayField = observer(
(
props: IFieldFactoryProps<any, any, any, any> & {
children?: (index: number) => ReactNode;
},
) => {
const form = useContext(FormContext);
const parent = useContext(FieldContext);
const name = props.name;
const field = form.createArrayField({
...props,
name: name,
basePath: parent?.address,
});
return (
<ReactiveField
field={field}
otherProps={{
value: field.value,
onChange: field.onInput,
}}
>
{props.children}
</ReactiveField>
);
},
);
MyArrayField的实现与Formily的稍有不同,MyArrayField会透传children字段。MyArrayField的实现也简单,用createArrayField创建Field就可以了
import { ArrayField, Field } from '@formily/core';
import { observer } from '@formily/reactive-react';
import React, { ReactNode, useContext } from 'react';
import { ReactElement } from 'react';
import { FieldContext } from './Context';
// Input UI组件
type PropsType = Field & {
children: (index: number) => ReactElement;
};
export default observer((props: PropsType) => {
const field = useContext(FieldContext) as ArrayField;
console.log('render arrayitem ', field.value);
return (
<div
style={{
border: '2px solid rgb(186 203 255)',
}}
>
<div style={{ padding: '10px' }}>
{field.value.map((item, index) => {
console.log('render array ' + index);
return (
<div key={index}>
<div>{props.children(index)}</div>
<button
onClick={() => {
field.remove(index);
}}
>
删除
</button>
</div>
);
})}
</div>
<button
onClick={() => {
field.push({});
}}
>
添加一行
</button>
</div>
);
});
注意,ArrayField里面的组件要用observer包围,否则array发生变动以后,容器组件不会自动渲染。这里,我们直接用props.children来渲染就可以了。
import { autorun, observable } from '@formily/reactive';
import { observer } from '@formily/reactive-react';
import {
FormConsumer,
FormProvider,
MyArrayField,
MyField,
MyObjectField,
} from './Context';
import Input from './Input';
import InputDigit from './InputDigit';
import Password from './Password';
import Label from './Label';
import FormItem from './FormItem';
import RequireValidator from './RequireValidator';
import { useMemo } from 'react';
import { createForm, Field, onFieldReact } from '@formily/core';
import Card from './Card';
import ArrayItems from './ArrayItems';
export default () => {
console.log('Top Render');
const form = useMemo(() => {
return createForm({
effects: () => {
onFieldReact('nameLength', (field) => {
let field2 = field as Field;
field2.value = field2.query('.name').value()?.length;
});
},
});
}, []);
return (
<FormProvider form={form}>
<MyObjectField
title="个人信息"
name="person"
component={[Card, {}]}
decorator={[FormItem]}
>
<MyField
title="名称"
name="name"
required
component={[Input, {}]}
decorator={[FormItem]}
/>
<MyField
title="年龄"
name="age"
required
component={[InputDigit, {}]}
decorator={[FormItem, { style: { height: 30 } }]}
/>
</MyObjectField>
<MyArrayField
title="联系信息"
name="contact"
component={[ArrayItems, {}]}
decorator={[FormItem, { style: { height: 30 } }]}
>
{(index) => {
return (
<MyObjectField
title="信息"
name={index}
component={[Card, {}]}
decorator={[FormItem]}
>
<MyField
title="电话"
name="phone"
validator={{
format: 'phone',
}}
required
component={[Input, {}]}
decorator={[FormItem]}
/>
<MyField
title="邮件"
name="email"
validator={{
format: 'email',
}}
required
component={[Input, {}]}
decorator={[FormItem]}
/>
</MyObjectField>
);
}}
</MyArrayField>
<FormConsumer>
{(form) => {
return <div>{JSON.stringify(form.values)}</div>;
}}
</FormConsumer>
</FormProvider>
);
};
这是测试代码,写法已经和Formily的很相似了
4 Schema组件原理
Schema组件实现原理中,最关键的一点是,使用RecursionField进行某个Schema子树的渲染,并且它也会不断递归自身,将自身Schema渲染完毕以后,计算得到子Schema,然后交给子的RecursionField来渲染。
4.1 JSON Schema
代码在这里
export type JsonSchema =
| {
type: 'object';
title?: string;
name?: string;
required?: boolean;
format?: string;
properties: {
[name in string]: JsonSchema;
};
'x-component': string;
'x-component-props': any;
'x-decorator': string;
'x-decorator-props': any;
}
| {
type: 'array';
title?: string;
name?: string;
required?: boolean;
format?: string;
items: JsonSchema;
properties: {
[name in string]: JsonSchema;
};
'x-component': string;
'x-component-props': any;
'x-decorator': string;
'x-decorator-props': any;
}
| {
type: 'number';
title?: string;
name?: string;
required?: boolean;
format?: string;
'x-component': string;
'x-component-props': any;
'x-decorator': string;
'x-decorator-props': any;
}
| {
type: 'string';
title?: string;
name?: string;
required?: boolean;
format?: string;
'x-component': string;
'x-component-props': any;
'x-decorator': string;
'x-decorator-props': any;
};
首先,定义JSON Schema的格式
import { MyField, MyObjectField, MyArrayField } from './Context';
import { Fragment, ReactElement } from 'react';
import { useContext, ReactNode } from 'react';
import { createContext } from 'react';
import { JsonSchema } from './JsonSchema';
//创建上下文,方便Schema获取到Component字符串的实际指向
export type SchemaOptions = {
[name in string]:
| React.FunctionComponent<any>
| React.Component<any, any, any>;
};
export const SchemaOptionsContext = createContext<SchemaOptions>(
{} as SchemaOptions,
);
//创建上下文,方便RecusrionField获取到当前的子Schema
export const FieldSchemaContext = createContext<JsonSchema>({} as JsonSchema);
type RecursionFieldProps = {
name: string;
schema: JsonSchema;
onlyRenderProperties: boolean;
};
export const RecursionField: React.FC<RecursionFieldProps> = (props) => {
const fieldSchema = props.schema;
//当fieldSchema的name为空的时候,使用props上面的name
let name = fieldSchema.name ? fieldSchema.name : props.name;
if (name === undefined) {
name = '';
}
let validator: { format: string } | undefined = undefined;
if (fieldSchema.format) {
validator = { format: fieldSchema.format };
}
const options = useContext(SchemaOptionsContext);
const renderProperties = (
schemas: { [name in string]: JsonSchema },
): ReactElement => {
let result = [];
for (var key in schemas) {
let subSchema = schemas[key];
result.push(
<RecursionField
key={key}
onlyRenderProperties={false}
schema={subSchema}
name=""
/>,
);
}
return <Fragment>{result}</Fragment>;
};
const render = (): ReactNode => {
if (fieldSchema.type == 'object') {
if (props.onlyRenderProperties) {
return renderProperties(fieldSchema.properties);
}
return (
<MyObjectField
title={fieldSchema.title}
name={name}
required={fieldSchema.required}
validator={validator}
component={[
options[fieldSchema['x-component']],
fieldSchema['x-component-props'],
]}
decorator={[
options[fieldSchema['x-decorator']],
fieldSchema['x-decorator-props'],
]}
>
{renderProperties(fieldSchema.properties)}
</MyObjectField>
);
} else if (fieldSchema.type == 'array') {
//array不渲染children,因为array的业务方案太多了
if (props.onlyRenderProperties) {
return renderProperties(fieldSchema.properties);
}
return (
<MyArrayField
title={fieldSchema.title}
name={name}
required={fieldSchema.required}
validator={validator}
component={[
options[fieldSchema['x-component']],
fieldSchema['x-component-props'],
]}
decorator={[
options[fieldSchema['x-decorator']],
fieldSchema['x-decorator-props'],
]}
/>
);
} else if (
fieldSchema.type == 'number' ||
fieldSchema.type == 'string'
) {
return (
<MyField
title={fieldSchema.title}
name={name}
required={fieldSchema.required}
validator={validator}
component={[
options[fieldSchema['x-component']],
fieldSchema['x-component-props'],
]}
decorator={[
options[fieldSchema['x-decorator']],
fieldSchema['x-decorator-props'],
]}
/>
);
}
};
return (
<FieldSchemaContext.Provider value={fieldSchema}>
{render()}
</FieldSchemaContext.Provider>
);
};
type SchemaProps = {
options: SchemaOptions;
schema: JsonSchema;
};
export function Schema(props: SchemaProps) {
return (
<SchemaOptionsContext.Provider value={props.options}>
<RecursionField
onlyRenderProperties={true}
schema={props.schema}
name=""
/>
</SchemaOptionsContext.Provider>
);
}
然后,我们建立SchemaOptionsContext,传递component字符串映射。以及建立FieldSchemaContext,它负责传递下级的Schema。最后,Schema组件的渲染其实就是将schema交给了RecursionField来渲染,RecursionField主要做两件事:
- 根据schema的当前节点,渲染出schema。
- 只有VoidField与ObjectField,需要进一步渲染子schema,并填充到children字段里面。
注意,ArrayField组件,不会去渲染它的children字段。那么ArrayField是如何知道怎样渲染自身的元素呢?
import { ArrayField, Field } from '@formily/core';
import { observer } from '@formily/reactive-react';
import React, { ReactNode, useContext } from 'react';
import { ReactElement } from 'react';
import { FieldContext } from './Context';
import { FieldSchemaContext, RecursionField } from './Schema';
export default observer(() => {
const fieldSchema = useContext(FieldSchemaContext);
const field = useContext(FieldContext) as ArrayField;
console.log('render arrayitem ', field.value);
//在下面的RecursionField传入name,因为定义jsonSchema的时候,无法知道当前是在哪个index
return (
<div
style={{
border: '2px solid rgb(186 203 255)',
}}
>
<div style={{ padding: '10px' }}>
{field.value.map((item, index) => {
return (
<div key={index}>
<div>
<RecursionField
onlyRenderProperties={false}
schema={(fieldSchema as any).items}
name={index + ''}
/>
</div>
<button
onClick={() => {
field.remove(index);
}}
>
删除
</button>
</div>
);
})}
</div>
<button
onClick={() => {
field.push({});
}}
>
添加一行
</button>
</div>
);
});
答案是使用useContext(FieldSchemaContext),拿出自己的schema,然后对着Schema的items字段,调用RecursionField来渲染。这为ArrayField的实现提供了最大的灵活度,你试试思考一下,如果RecursionField帮助了ArrayField渲染了children字段,那么ArrayField如何知道怎样渲染列头,列行等信息呢。
import { autorun, observable } from '@formily/reactive';
import { observer } from '@formily/reactive-react';
import { FormConsumer, FormProvider } from './Context';
import Input from './Input';
import InputDigit from './InputDigit';
import Password from './Password';
import Label from './Label';
import FormItem from './FormItem';
import { useMemo } from 'react';
import { createForm } from '@formily/core';
import Card from './Card';
import ArrayItems from './ArrayItemsSchema';
import { Schema, SchemaOptions } from './Schema';
import { JsonSchema } from './JsonSchema';
let options: SchemaOptions = {
Input: Input,
InputDigit: InputDigit,
Password: Password,
Label: Label,
Card: Card,
ArrayItems: ArrayItems,
FormItem: FormItem,
};
let schema: JsonSchema = {
type: 'object',
'x-component': 'FormItem',
'x-component-props': {},
'x-decorator': 'FormItem',
'x-decorator-props': {},
properties: {
person: {
type: 'object',
name: 'person',
title: '个人信息',
'x-component': 'Card',
'x-component-props': {},
'x-decorator': 'FormItem',
'x-decorator-props': {},
properties: {
name: {
type: 'string',
name: 'name',
title: '名称',
required: true,
'x-component': 'Input',
'x-component-props': {},
'x-decorator': 'FormItem',
'x-decorator-props': {},
},
age: {
type: 'number',
name: 'age',
title: '年龄',
required: true,
'x-component': 'InputDigit',
'x-component-props': {},
'x-decorator': 'FormItem',
'x-decorator-props': {},
},
},
},
contact: {
type: 'array',
name: 'contact',
title: '联系信息',
'x-component': 'ArrayItems',
'x-component-props': {},
'x-decorator': 'FormItem',
'x-decorator-props': {},
items: {
//这里的name要保持为空,因为array下有多个行,每个行的index都是不同的,不能在定义schema的时候确定
//这里只能由ArrayItems自身来确定下一级的name
type: 'object',
title: '信息',
'x-component': 'Card',
'x-component-props': {},
'x-decorator': 'FormItem',
'x-decorator-props': {},
properties: {
name: {
type: 'string',
name: 'phone',
title: '电话',
required: true,
format: 'phone',
'x-component': 'Input',
'x-component-props': {},
'x-decorator': 'FormItem',
'x-decorator-props': {},
},
age: {
type: 'string',
name: 'email',
title: '电子邮件',
required: true,
format: 'email',
'x-component': 'Input',
'x-component-props': {},
'x-decorator': 'FormItem',
'x-decorator-props': {},
},
},
},
properties: {},
},
},
};
export default () => {
const form = useMemo(() => {
return createForm({
effects: () => {},
});
}, []);
return (
<FormProvider form={form}>
<Schema options={options} schema={schema} />
<FormConsumer>
{(form) => {
return <div>{JSON.stringify(form.values)}</div>;
}}
</FormConsumer>
</FormProvider>
);
};
测试代码,与Formily的几乎是一样的了
4.2 Markup Schema
Markup Schema其实就是JSON Schema的另外一种写法而已。所以,在实现上,是先将Markup Schema转换为JSON Schema,最后再交给Schema来渲染。一个错误的思路是,将Markup Schema看成是React Node然后逐层递归来渲染。这个方法的错误在于,在ArrayField组件里面,你不能用相同引用的React Node来渲染多个行,这样会被React报错。你只能用相同的Schema,生成出不同引用但是相同内容的React Node来渲染多个行。
从另外一个角度看,RecursionField组件就是将Json转换为React Node的过程,那么Markup Schema组件就是反过来,它将React Node转换为Json,我们来看看是怎样做到的。
代码在这里
import { FunctionComponent, useContext } from 'react';
import { ReactNode } from 'react';
import { createContext } from 'react';
import { JsonSchema } from './JsonSchema';
type BasicJsxSchemaProps = {
title?: string;
name?: string;
required?: boolean;
format?: string;
component: [string, any];
decorator: [string, any];
};
type ObjectJsxSchemaProps = {
children: ReactNode;
} & BasicJsxSchemaProps;
type ArrayJsxSchemaProps = {
children: ReactNode;
} & BasicJsxSchemaProps;
export const JsxSchemaContext = createContext<JsonSchema>({} as JsonSchema);
export function JsxSchema() {
const Common: FunctionComponent<
BasicJsxSchemaProps & {
type: 'string' | 'number' | 'object' | 'array';
}
> = (props) => {
let parent = useContext(JsxSchemaContext);
let data: JsonSchema = {
type: props.type,
title: props.title,
name: props.name,
required: props.required,
format: props.format,
'x-component': props.component[0],
'x-component-props': props.component[1],
'x-decorator': props.decorator[0],
'x-decorator-props': props.decorator[1],
};
//添加上级的schema
if (parent.type == 'array') {
if (!parent.items) {
//首次添加进入array
parent.items = data;
} else {
//而后进入array
if (!parent.properties) {
parent.properties = {};
}
parent.properties[data.name!] = data;
}
} else if (parent.type == 'object') {
if (!parent.properties) {
parent.properties = {};
}
parent.properties[data.name!] = data;
} else {
throw new Error('unknown component!');
}
//让子schema递归下去
if (data.type == 'array' || data.type == 'object') {
return (
<JsxSchemaContext.Provider value={data}>
{props.children}
</JsxSchemaContext.Provider>
);
} else {
return null;
}
};
const StringJsx = (props: BasicJsxSchemaProps) => {
return <Common type="string" {...props} />;
};
const NumberJsx = (props: BasicJsxSchemaProps) => {
return <Common type="number" {...props} />;
};
const ObjectJsx = (props: ObjectJsxSchemaProps) => {
return <Common type="object" {...props} />;
};
const ArrayJsx = (props: ArrayJsxSchemaProps) => {
return <Common type="array" {...props} />;
};
return {
String: StringJsx,
Number: NumberJsx,
Object: ObjectJsx,
Array: ArrayJsx,
};
}
首先建立一个JsxSchemaContext,它获取的是当前正在生成的JSON Schema子树。然后在每层子树render的时候,把当前schema添加进去properties或者items上。注意,由于Markup Schema缺乏像JSON这样显式的指定properties与items属性,Markup Schema将所有的children组件全部放在xml的子树上。因此,我们约定,在Markup Schema中,首个Children组件放入items属性,之后的Children组件放入properties属性,Formily也是这样实现的。
export const Schema: FunctionComponent<SchemaProps> = (props) => {
let schema: JsonSchema = props.schema
? props.schema
: {
type: 'object',
'x-component': '',
'x-component-props': {},
'x-decorator': '',
'x-decorator-props': {},
properties: {},
};
return (
<SchemaOptionsContext.Provider value={props.options}>
<JsxSchemaContext.Provider value={schema}>
{props.children}
</JsxSchemaContext.Provider>
<RecursionField
onlyRenderProperties={true}
schema={schema}
name=""
/>
</SchemaOptionsContext.Provider>
);
};
最后,我们稍微更改一下Schema组件,添加JsxSchemaContext.Provider,并且将子树渲染出来。
import { autorun, observable } from '@formily/reactive';
import { observer } from '@formily/reactive-react';
import { FormConsumer, FormProvider } from './Context';
import Input from './Input';
import InputDigit from './InputDigit';
import Password from './Password';
import Label from './Label';
import FormItem from './FormItem';
import { useMemo } from 'react';
import { createForm } from '@formily/core';
import Card from './Card';
import ArrayItems from './ArrayItemsSchema';
import { Schema, SchemaOptions } from './Schema';
import { JsonSchema } from './JsonSchema';
import { JsxSchema, JsxSchemaContext } from './JsxSchema';
let options: SchemaOptions = {
Input: Input,
InputDigit: InputDigit,
Password: Password,
Label: Label,
Card: Card,
ArrayItems: ArrayItems,
FormItem: FormItem,
};
const MyJsxSchma = JsxSchema();
export default () => {
const form = useMemo(() => {
return createForm({
effects: () => {},
});
}, []);
return (
<FormProvider form={form}>
<Schema options={options}>
<MyJsxSchma.Object
name={'person'}
title={'个人信息'}
component={['Card', {}]}
decorator={['FormItem', {}]}
>
<MyJsxSchma.String
name={'name'}
title={'名称'}
required={true}
component={['Input', {}]}
decorator={['FormItem', {}]}
/>
<MyJsxSchma.Number
name={'age'}
title={'年龄'}
required={true}
component={['InputDigit', {}]}
decorator={['FormItem', {}]}
/>
</MyJsxSchma.Object>
<MyJsxSchma.Array
name={'contact'}
title={'联系方式'}
component={['ArrayItems', {}]}
decorator={['FormItem', {}]}
>
<MyJsxSchma.Object
title={'信息'}
component={['Card', {}]}
decorator={['FormItem', {}]}
>
<MyJsxSchma.String
name={'phone'}
title={'电话'}
required={true}
format={'phone'}
component={['Input', {}]}
decorator={['FormItem', {}]}
/>
<MyJsxSchma.String
name={'email'}
title={'电子邮件'}
required={true}
format={'email'}
component={['Input', {}]}
decorator={['FormItem', {}]}
/>
</MyJsxSchma.Object>
</MyJsxSchma.Array>
</Schema>
<FormConsumer>
{(form) => {
return <div>{JSON.stringify(form.values)}</div>;
}}
</FormConsumer>
</FormProvider>
);
};
测试代码,这个时候与Formily的实现也几乎一致
5 Schema联动表达式
我们在Core库中描述过,在form写联动表达式是通过在effects里面调用onFieldReact,或者onFieldValueChange方法。但是,在JSON Schema中无法将所有联动表达式都转换为js代码放到JSON节点中,这样不方便在编辑器中写入联动表达式。
因此,在Schema中描述组件的联动逻辑,有另外的一套语法。原来在Form表单的effects属性中写入联动逻辑是可以的,这是原来的方法不方便在组件编辑器中进行读取而已。
代码在这里
5.1 主动与被动联动
5.1.1 主动联动
import React from 'react';
import { createForm } from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';
const form = createForm();
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
Select,
},
});
export default () => (
<Form form={form}>
<SchemaField>
<SchemaField.String
name="input"
title="输入者"
x-component="Input"
x-decorator="FormItem"
x-reactions={{
//主动受控,target加fulfill,只有当前value为123的时候才会展示input2
//注意,可以修改受控者的哪个value
target: 'input2',
fulfill: {
state: {
visible: "{{$self.value=='123'}}",
},
},
}}
/>
<SchemaField.String
name="input2"
title="受控者"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField>
<FormConsumer>
{() => (
<code>
<pre>{JSON.stringify(form.values)}</pre>
</code>
)}
</FormConsumer>
</Form>
);
主动联动的写法,target加上fulfill,$self可以取出当前字段的值
5.1.2 被动联动
import React from 'react';
import { createForm } from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';
const form = createForm();
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
Select,
},
});
export default () => (
<Form form={form}>
<SchemaField>
<SchemaField.String
name="input"
title="输入者"
x-component="Input"
x-decorator="FormItem"
/>
<SchemaField.String
name="input2"
title="受控者"
x-component="Input"
x-decorator="FormItem"
x-reactions={{
//被动受控,dependencies加fulfill
dependencies: ['input'],
fulfill: {
state: {
value: '{{$deps[0]}}',
},
},
}}
/>
</SchemaField>
<FormConsumer>
{() => (
<code>
<pre>{JSON.stringify(form.values)}</pre>
</code>
)}
</FormConsumer>
</Form>
);
被动联动,dependencies加上fulfill,\(deps可以取到对应的依赖项的数值,这种方式只能拿到\)deps的value值,不能拿其他值
5.2 scope联动
import React from 'react';
import { createForm } from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';
const form = createForm();
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
Select,
},
scope: {
asyncVisible(field) {
field.loading = true;
setTimeout(() => {
field.loading = false;
form.setFieldState('input', (state) => {
//对于初始联动,如果字段找不到,setFieldState会将更新推入更新队列,直到字段出现再执行操作
state.display = field.value;
});
}, 1000);
},
},
});
export default () => (
<Form form={form}>
<SchemaField>
<SchemaField.String
name="select"
title="控制者"
default="visible"
enum={[
{ label: '显示', value: 'visible' },
{ label: '隐藏', value: 'none' },
{ label: '隐藏-保留值', value: 'hidden' },
]}
x-component="Select"
x-decorator="FormItem"
x-reactions={{
//主动联动,但是当value发生变化的时候,触发asyncVisible方法
//注意asyncVisible需要先放在scope环境里面,触发方法用run
target: 'input',
effects: ['onFieldValueChange'],
fulfill: {
run: 'asyncVisible($self,$target)',
},
}}
/>
<SchemaField.String
name="input"
title="受控者"
x-component="Input"
x-decorator="FormItem"
x-visible={false}
/>
</SchemaField>
<FormConsumer>
{() => (
<code>
<pre>{JSON.stringify(form.values, null, 2)}</pre>
</code>
)}
</FormConsumer>
</Form>
);
fulfill可以用run方法,这样就会调用对应scope上的方法了。触发方式上我们可以加上onFieldValueChange,或者onFieldInputValueChange
5.3 批量联动
5.3.1 批量主动联动
import React from 'react';
import { createForm } from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';
const form = createForm();
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
Select,
},
});
export default () => (
<Form form={form}>
<SchemaField>
<SchemaField.String
name="input"
title="输入者"
x-component="Input"
x-decorator="FormItem"
x-reactions={[
//同时操控多个受控者
{
//主动受控,target加fulfill,只有当前value为123的时候才会展示input2
target: 'input2',
fulfill: {
state: {
value:
"{{'['+($self.value?$self.value:'')+']'}}",
},
},
},
{
//主动受控,target加fulfill,只有当前value为123的时候才会展示input2
target: 'input3',
fulfill: {
state: {
value:
"{{'$'+($self.value?$self.value:'')+'$'}}",
},
},
},
]}
/>
<SchemaField.String
name="input2"
title="受控者"
x-component="Input"
x-decorator="FormItem"
/>
<SchemaField.String
name="input3"
title="受控者2"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField>
<FormConsumer>
{() => (
<code>
<pre>{JSON.stringify(form.values)}</pre>
</code>
)}
</FormConsumer>
</Form>
);
x-reactions可以传入一个数组,这样就能实现一对多的主动联动
5.3.2 批量被动联动
import React from 'react';
import { createForm } from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';
const form = createForm();
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
Select,
},
});
export default () => (
<Form form={form}>
<SchemaField>
<SchemaField.String
name="price"
title="单价"
x-component="Input"
x-decorator="FormItem"
/>
<SchemaField.String
name="count"
title="数量"
x-component="Input"
x-decorator="FormItem"
/>
<SchemaField.String
name="total"
title="总额"
x-editable={false}
x-component="Input"
x-decorator="FormItem"
x-reactions={{
//被动受控,dependencies可以是一个数组
//注意加入了when操作,当两者的乘积不是合法数值的时候,不进行更新,这个是可选操作
dependencies: ['price', 'count'],
when: '{{$deps[0] && $deps[1]}}',
fulfill: {
state: {
value: '{{$deps[0]*$deps[1]}}',
},
},
}}
/>
</SchemaField>
<FormConsumer>
{() => (
<code>
<pre>{JSON.stringify(form.values)}</pre>
</code>
)}
</FormConsumer>
</Form>
);
我们在依赖项的时候也可以传入一个数组,这样可以让一个字段同时受多个字段的被动联动。注意when的写法,主要可以指定特定情况下才去触发联动。
5.3.3 批量path联动
import React from 'react';
import { createForm } from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';
const form = createForm();
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
Select,
},
});
export default () => (
<Form form={form}>
<SchemaField>
<SchemaField.String
name="input"
title="输入者"
x-component="Input"
x-decorator="FormItem"
x-reactions={
//同时操控多个受控者
{
//使用path的方式,同时指定多个受控者
//注意加入了onFieldInputValueChange的操作,仅当输入产生的value变化时才触发,开发者修改的value不触发
target: '*(input2,input3)',
effects: ['onFieldInputValueChange'],
fulfill: {
state: {
value:
"{{'['+($self.value?$self.value:'')+']'}}",
},
},
}
}
/>
<SchemaField.String
name="input2"
title="受控者"
x-component="Input"
x-decorator="FormItem"
/>
<SchemaField.String
name="input3"
title="受控者2"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField>
<FormConsumer>
{() => (
<code>
<pre>{JSON.stringify(form.values)}</pre>
</code>
)}
</FormConsumer>
</Form>
);
批量主动联动的另外一种写法是,使用path表达式的*号,可以同时指定多个target。
5.4 list条目联动
5.4.1 同级path联动
import React from 'react';
import {
Form,
FormItem,
NumberPicker,
ArrayTable,
Editable,
Input,
FormButtonGroup,
Submit,
} from '@formily/antd';
import { createForm } from '@formily/core';
import { createSchemaField } from '@formily/react';
const SchemaField = createSchemaField({
components: {
FormItem,
Editable,
Input,
NumberPicker,
ArrayTable,
},
});
const form = createForm();
export default () => {
return (
<Form form={form} layout="vertical">
<SchemaField>
<SchemaField.Array
name="projects"
title="Projects"
x-decorator="FormItem"
x-component="ArrayTable"
>
<SchemaField.Object>
<SchemaField.Void
x-component="ArrayTable.Column"
x-component-props={{
width: 50,
title: 'Sort',
align: 'center',
}}
>
<SchemaField.Void
x-decorator="FormItem"
x-component="ArrayTable.SortHandle"
/>
</SchemaField.Void>
<SchemaField.Void
x-component="ArrayTable.Column"
x-component-props={{
width: 80,
title: 'Index',
align: 'center',
}}
>
<SchemaField.String
x-decorator="FormItem"
x-component="ArrayTable.Index"
/>
</SchemaField.Void>
<SchemaField.Void
x-component="ArrayTable.Column"
x-component-props={{ title: 'Price' }}
>
<SchemaField.Number
name="price"
x-decorator="FormItem"
required
x-component="NumberPicker"
x-component-props={{}}
default={0}
x-reactions={{
//FIXME,同级使用主动联动会失败
//https://github.com/alibaba/formily/discussions/1874
//主动联动,不支持相对路径,这是官方的说法
target: '.count',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
}}
/>
</SchemaField.Void>
<SchemaField.Void
x-component="ArrayTable.Column"
x-component-props={{ title: 'Count' }}
>
<SchemaField.Number
name="count"
x-decorator="FormItem"
required
x-component="NumberPicker"
default={0}
/>
</SchemaField.Void>
<SchemaField.Void
x-component="ArrayTable.Column"
x-component-props={{ title: 'Total' }}
>
<SchemaField.Number
x-decorator="FormItem"
name="total"
x-component="NumberPicker"
x-pattern="readPretty"
x-component-props={{}}
x-reactions={{
//拿出同级的price与count数据,取乘积
dependencies: ['.price', '.count'],
when: '{{$deps[0] && $deps[1]}}',
fulfill: {
state: {
value: '{{$deps[0] * $deps[1]}}',
},
},
}}
/>
</SchemaField.Void>
<SchemaField.Void
x-component="ArrayTable.Column"
x-component-props={{
title: 'Operations',
dataIndex: 'operations',
width: 200,
fixed: 'right',
}}
>
<SchemaField.Void x-component="FormItem">
<SchemaField.Void x-component="ArrayTable.Remove" />
<SchemaField.Void x-component="ArrayTable.MoveDown" />
<SchemaField.Void x-component="ArrayTable.MoveUp" />
</SchemaField.Void>
</SchemaField.Void>
</SchemaField.Object>
<SchemaField.Void
x-component="ArrayTable.Addition"
title="Add"
/>
</SchemaField.Array>
<SchemaField.Number
name="total"
title="Total"
x-decorator="FormItem"
x-component="NumberPicker"
x-component-props={{
addonAfter: '$',
}}
x-pattern="readPretty"
x-reactions={{
//被动联动,拿出同级的.projects数据
dependencies: ['.projects'],
when: '{{$deps.length > 0}}',
fulfill: {
state: {
value:
'{{$deps[0].reduce((total,item)=>item.total ? total+item.total : total,0)}}',
},
},
}}
/>
</SchemaField>
<FormButtonGroup>
<Submit onSubmit={console.log}>提交</Submit>
</FormButtonGroup>
</Form>
);
};
使用path表达式的.号语法,我们可以轻松指定同级的其他字段。但是,注意,在目前Formily的80版本下,只支持同级path表达式的被动联动,不支持同级path表达式的主动联动,这种情况下只能使用Form的effects来做。具体看这里。
5.4.2 子级path联动
import React from 'react';
import {
FormItem,
Input,
ArrayCards,
FormButtonGroup,
Submit,
} from '@formily/antd';
import { createForm, onFieldInputValueChange } from '@formily/core';
import { FormProvider, createSchemaField, FormConsumer } from '@formily/react';
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
ArrayCards,
},
});
const form = createForm({
effects: () => {
/*
//effect方式的主动联动
onFieldInputValueChange('array.*.input', (field) => {
field.query('..').take().setTitle(field.value);
});
*/
},
});
export default () => {
return (
<FormProvider form={form}>
<SchemaField>
<SchemaField.Array
name="array"
x-component="ArrayCards"
x-component-props={{}}
>
<SchemaField.Object
//path表达式取出子级的数据
x-reactions={{
dependencies: ['.[].input'],
fulfill: {
state: {
visible: "{{$deps[0]!='123'}}",
},
},
}}
>
<SchemaField.String
name="title"
x-decorator="FormItem"
title="标题"
x-component="Input"
/>
<SchemaField.String
name="input"
x-decorator="FormItem"
title="输入框"
required
x-component="Input"
/>
<SchemaField.Void x-component="ArrayCards.Remove" />
<SchemaField.Void x-component="ArrayCards.MoveUp" />
<SchemaField.Void x-component="ArrayCards.MoveDown" />
</SchemaField.Object>
<SchemaField.Void
x-component="ArrayCards.Addition"
x-reactions={{
//被动联动
dependencies: ['array'],
fulfill: {
state: {
visible: '{{$deps[0].length<3}}',
},
},
}}
title="添加条目"
/>
</SchemaField.Array>
</SchemaField>
<FormButtonGroup>
<Submit onSubmit={console.log}>提交</Submit>
</FormButtonGroup>
<FormConsumer>
{(form) => {
return <div>{JSON.stringify(form.values)}</div>;
}}
</FormConsumer>
</FormProvider>
);
};
子级的联动,我们可以用.[].xx表达式,这种方式比较少用。
6 高级特性
有些时候我们需要自己定制的UI组件与Formily体系接入,这个时候我们就需要深入去理解Formily的内部的运行机制了
学完高级特性,我们尝试实现一个简陋版的ArrayTable
代码在这里
6.1 ObjectField的component属性为空
import { createForm } from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';
const form = createForm({
effects: () => {},
});
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
Select,
},
});
export default () => {
//ObjectField和VoidField的Component是可以为空的,不影响运行
//https://github.com/alibaba/formily/blob/formily_next/packages/react/src/components/ReactiveField.tsx
/*
const renderComponent = () => {
if (!field.component[0]) return <Fragment>{children}</Fragment>
....
}
* 当component为空的时候,直接返回一个children包围的fragment组件
* VoidField与ObjectField的执行原理是类似的,只是FieldProvider提供的是VoidField,不是ObjectField,这样Void组件会导致values中没有void的名称字段。
* ArrayField是没有children字段的,因为ArrayField接管了全部的渲染过程,Array组件没有Component是无法渲染任何组件出来的
*/
return (
<Form form={form}>
<SchemaField>
<SchemaField.Object name="object">
<SchemaField.String
name="input"
title="Object输入框"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField.Object>
<SchemaField.Void name="void">
<SchemaField.String
name="input2"
title="Void输入框"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField.Void>
<SchemaField.Array name="array">
<SchemaField.String
name="input3"
title="Array输入框"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField.Array>
</SchemaField>
<FormConsumer>
{() => (
<code>
<pre>{JSON.stringify(form.values)}</pre>
</code>
)}
</FormConsumer>
</Form>
);
};
从实验中,我们得到:
- 当ObjectField与VoidField的component为空的时候,Formily会用一个Fragment包围返回出去,渲染依然是正常的。
- 当ArrayField的component为空的时候,Formily是无法渲染的,因为Formily是不会自动渲染ArrayField的Children,它直接将整个渲染过程丢给了ArrayField来做。
另外一点,即使ObjectField与VoidField的component为空,但是它依然能给下级组件提供了FieldProvider,产生字段树,这是这种做法最大的意义。
6.2 ObjectField的相同name
import { createForm } from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';
const form = createForm({
effects: () => {},
});
const form2 = createForm({
effects: () => {},
});
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
Select,
},
});
export default () => {
//同一级的SchemaField不能有两个相同的name,否则因为properties相同而撞在一起了
//可以用Void来包围一下放在下一级使用两个相同的name
return (
<div>
<Form form={form}>
<SchemaField>
<SchemaField.Object name="object">
<SchemaField.String
name="input"
title="Object输入框"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField.Object>
<SchemaField.Object name="object">
<SchemaField.String
name="input2"
title="Object输入框"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField.Object>
</SchemaField>
<FormConsumer>
{() => (
<code>
<pre>{JSON.stringify(form.values)}</pre>
</code>
)}
</FormConsumer>
</Form>
<Form form={form2}>
<SchemaField>
<SchemaField.Object name="object">
<SchemaField.String
name="input"
title="Object输入框"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField.Object>
<SchemaField.Void name="void">
<SchemaField.Object name="object">
<SchemaField.String
name="input2"
title="Object输入框"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField.Object>
</SchemaField.Void>
</SchemaField>
<FormConsumer>
{() => (
<code>
<pre>{JSON.stringify(form2.values)}</pre>
</code>
)}
</FormConsumer>
</Form>
</div>
);
};
在Markup Schema写代码的时候,如果同一级别的两个组件name是相同的,就是会产生异常。因为Markup Schema内部会转换为JSON Schema,两个组件的name是相同的,那么他们会合并在同一个properties的key上,导致前一个组件丢失了。当然,不同组件的name不同是没问题的。
6.3 Field的basePath属性
import { createForm } from '@formily/core';
import {
createSchemaField,
Field,
FormConsumer,
ObjectField,
} from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';
const form = createForm({
effects: () => {},
});
const form2 = createForm({
effects: () => {},
});
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
Select,
},
});
export default () => {
//Field字段下面的BasePath可以改写Field的父级路径
//https://github.com/alibaba/formily/blob/formily_next/packages/react/src/components/Field.tsx
/*
const form = useForm()
const parent = useField()
const field = useAttach(
form.createField({ basePath: parent?.address, ...props })
)
//以上是Field的源代码,默认是取上级Field的address作为basePath,我们可以在props中传递basePath来改写这个属性
//另外,我们也能看到,父级没有ObjectField组件,也能使用Form的createField来创建跨下级的Field。Formily会自动补充中间缺失的Field
//注意,SchemaField上的x-basePath属性是没用的,不要使用。
*/
return (
<div>
<Form form={form}>
<Field
name="input"
basePath="mm"
title="Object输入框"
component={[Input, {}]}
decorator={[FormItem, {}]}
/>
<Field
name="input"
basePath="kk"
title="Object输入框"
component={[Input, {}]}
decorator={[FormItem, {}]}
/>
<FormConsumer>
{() => (
<code>
<pre>{JSON.stringify(form.values)}</pre>
</code>
)}
</FormConsumer>
</Form>
</div>
);
};
在Field组件的默认实现中,它会以父级Field的address作为basePath。我们可以通过改写它的basePath属性来覆盖这种默认设定。
6.4 RecursionField的name属性
import { createForm } from '@formily/core';
import {
createSchemaField,
Field,
FormConsumer,
ObjectField,
} from '@formily/react';
import ArrayList from './ArrayList';
import { Form, FormItem, Input, Select } from '@formily/antd';
const form = createForm({
effects: () => {},
});
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
Select,
ArrayList,
},
});
export default () => {
return (
<div>
<Form form={form}>
<SchemaField>
<SchemaField.Array name="data" x-component="ArrayList">
<SchemaField.Void>
<SchemaField.String
name="input"
title="输入框A"
x-component="Input"
x-decorator="FormItem"
/>
<SchemaField.String
name="input2"
title="输入框B"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField.Void>
</SchemaField.Array>
</SchemaField>
<FormConsumer>
{() => (
<code>
<pre>{JSON.stringify(form.values)}</pre>
</code>
)}
</FormConsumer>
</Form>
</div>
);
};
我们先假设,我们设计一个ArrayList的组件,它的items组件下面,不是ObjectField,而是VoidField。那么每一个行之间的数据是如何知道它在哪个index的呢?例如,input字段,怎么知道自己是在data.0.input字段,还是在data.1.input字段呢?因为没有ObjectField,不能在input字段上建立一个父字段,来沿用Field的basePath的默认生成机制。
import { ArrayField, Field } from '@formily/core';
import { RecursionField, useField, useFieldSchema } from '@formily/react';
import { observer } from '@formily/reactive-react';
import React, { ReactNode, useContext } from 'react';
import { ReactElement } from 'react';
type PropsType = Field & {
children: (index: number) => ReactElement;
};
export default observer((props: PropsType) => {
const field = useField<ArrayField>();
const fieldSchema = useFieldSchema();
//https://github.com/alibaba/formily/blob/formily_next/packages/react/src/components/RecursionField.tsx
//https://github.com/alibaba/formily/blob/formily_next/packages/react/src/components/ReactiveField.tsx
//RecursionField在渲染VoidField与ObjectField的时候,会将name字段传递给它们properties的Field的RecursionField的objectName+name。也各个Field的BasePath字段。
//换句话说,RecursionField会自动覆盖本Field以及各个子Field下的basePath
//RecursionField的name字段是必填的,basePath字段是选填的
//而且,RecursionField自身还有basePath字段,它们的优先级是,优先使用RecursionField的basePath字段,否则就使用parentField.address字段。结果为XBasePath字段。
//默认情况下,RecursionField渲染自身与子节点,这时候自身basePath被强行指定XBasePath,name字段就是为props.name字段,子节点通过ReactiveField来触发renderProperties,传入第一个参数是ObjectField或者VoidField本身,会自行继承父级的address作为basePath
//当RecursionField使用onlyRenderProperties的时候,自身缺少节点,子节点被直接指定basePath为(XBasePath+name字段)。
//无论如何,RecursionField总是保证以下:
//* 自身为XBasePath+RecursionField的name字段
//* 子节点为XBasePath+RecursionField的name字段+子节点自身的name字段
return (
<div
style={{
border: '2px solid rgb(186 203 255)',
}}
>
<div style={{ padding: '10px' }}>
{field.value?.map((item, index) => {
return (
<div key={index}>
<div>
<RecursionField
name={index}
schema={fieldSchema.items!}
/>
</div>
<button
onClick={() => {
field.remove(index);
}}
>
删除
</button>
</div>
);
})}
</div>
<button
onClick={() => {
field.push({});
}}
>
添加一行
</button>
</div>
);
});
RecursionField在设计的时候已经考虑了这种特殊情况,它的解决办法在于name字段。需要仔细看一下RecursionField的实现。
const getBasePath = () => {
if (props.onlyRenderProperties) {
return props.basePath || parent?.address.concat(props.name)
}
return props.basePath || parent?.address
}
const basePath = getBasePath()
const children =
fieldSchema['x-content'] || fieldSchema['x-component-props']?.['children']
const renderProperties = (field?: GeneralField) => {
if (props.onlyRenderSelf) return
return (
<Fragment>
{fieldSchema.mapProperties((item, name, index) => {
const base = field?.address || basePath
let schema: Schema = item
return (
<RecursionField
schema={schema}
key={`${index}-${name}`}
name={name}
basePath={base}
/>
)
})}
{children}
</Fragment>
)
}
const render = () => {
if (!isValid(props.name)) return renderProperties()
if (fieldSchema.type === 'object') {
if (props.onlyRenderProperties) return renderProperties()
return (
<ObjectField {...fieldProps} name={props.name} basePath={basePath}>
{renderProperties}
</ObjectField>
)
} ...
从实现中,我们可以看到:
- RecursionField自身还有basePath字段,它们的优先级是,优先使用RecursionField的basePath字段,否则就使用parentField.address字段。结果为XBasePath字段。
- 默认情况下,RecursionField渲染自身与子节点,这时候自身basePath被强行指定XBasePath,name字段就是为props.name字段,子节点通过ReactiveField来触发renderProperties,传入第一个参数是ObjectField或者VoidField本身,会自行继承父级的address作为basePath
- 当RecursionField使用onlyRenderProperties的时候,自身缺少节点,子节点被直接指定basePath为(XBasePath+name字段)。
6.5 ArrayTable的列组件的占位实现
import { createForm, onFieldInputValueChange } from '@formily/core';
import {
createSchemaField,
Field,
FormConsumer,
ObjectField,
} from '@formily/react';
import MyTable from './MyTable';
import { Form, FormItem, Input, Select } from '@formily/antd';
const form = createForm({
effects: () => {
onFieldInputValueChange('data', (field) => {
form.setFieldState('data.firstColumn', (state) => {
let compontProps = state.componentProps;
if (compontProps) {
console.log('change title');
compontProps.name = '名字:' + field.value.length + '行';
}
});
});
},
});
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
Select,
MyTable,
},
});
export default () => {
//MyTable实现了自增,但是Column组件并没有支持effects
return (
<div>
<Form form={form} feedbackLayout="terse">
<SchemaField>
<SchemaField.Array name="data" x-component="MyTable">
<SchemaField.Void>
<SchemaField.Void
name="firstColumn"
x-component="MyTable.Column"
x-component-props={{
title: '名字',
style: {
width: '100px',
},
}}
>
<SchemaField.String
name="name"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField.Void>
<SchemaField.Void
x-component="MyTable.Column"
x-component-props={{
title: '年龄',
}}
>
<SchemaField.String
name="age"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField.Void>
</SchemaField.Void>
</SchemaField.Array>
</SchemaField>
<FormConsumer>
{() => (
<code>
<pre>{JSON.stringify(form.values)}</pre>
</code>
)}
</FormConsumer>
</Form>
</div>
);
};
我们来模拟实现以下ArrayTable,初次实现中,我们看到ArrayTable的Schema中,每个字段都被一个Column包围住了。
但是,实际的渲染中,是以行为渲染的,每个单元格并不是都用Column来包围展示的。在这里,Column仅仅是作为一种Schema的占位符组件而已。
import { ArrayField, Field } from '@formily/core';
import {
RecursionField,
Schema,
useField,
useFieldSchema,
} from '@formily/react';
import { observer } from '@formily/reactive-react';
import React, { Fragment, ReactNode, useContext } from 'react';
import { ReactElement } from 'react';
import TableStyle from './style.css';
type Column = {
style: object;
name: string;
schema: Schema;
};
function getColumn(schema: Schema): Column[] {
//在当前实现中,Column层只是作为一种Schema来用,没有使用它的Field特性
let itemsSchema: Schema['items'] = schema.items;
const items = Array.isArray(itemsSchema) ? itemsSchema : [itemsSchema];
const parseSource = (schema: Schema): Column[] => {
let component = schema['x-component'];
if (component?.indexOf('Column') != -1) {
//获取该列的信息
return [
{
style: schema['x-component-props']?.style,
name: schema['x-component-props']?.title,
schema: schema,
},
];
}
return [];
};
const reduceProperties = (schema: Schema): Column[] => {
//对于items里面的每个schema,遍历它的Properties
if (schema.properties) {
return schema.reduceProperties((current, schema) => {
return current.concat(parseSource(schema));
}, [] as Column[]);
} else {
return [];
}
};
return items.reduce((current, schema) => {
//遍历每个items里面的schema
if (schema) {
return current.concat(reduceProperties(schema));
}
return current;
}, [] as Column[]);
}
type PropsType = Field & {
children: (index: number) => ReactElement;
};
type MyTableType = React.FC<PropsType> & {
Column?: React.FC<any>;
};
const MyTable: MyTableType = observer((props: PropsType) => {
const field = useField<ArrayField>();
const fieldSchema = useFieldSchema();
const tableColumns = getColumn(fieldSchema);
const renderHeader = () => {
let row = tableColumns.map((column) => {
return (
<td
style={column.style}
className={TableStyle.td}
key={column.name}
>
{column.name}
</td>
);
});
return <tr>{row}</tr>;
};
const renderRow = (field: any, index: number) => {
//注意这里的写法RecusionField是使用onlyRenderProperties,只渲染它的子节点
//但是因为RecursionField传入了index作为name,所以每个Property的name为parent.address+index+field name
let row = tableColumns.map((column) => {
return (
<td className={TableStyle.td} key={column.name}>
{
<RecursionField
name={index}
schema={column.schema}
onlyRenderProperties
/>
}
</td>
);
});
return (
<tr className={TableStyle.tr} key={index}>
{row}
</tr>
);
};
return (
<div
style={{
border: '2px solid rgb(186 203 255)',
}}
>
<table className={TableStyle.table}>
<thead>{renderHeader()}</thead>
<tbody>
{field.value?.map((row, index) => {
return renderRow(row, index);
})}
</tbody>
</table>
<button
onClick={() => {
field.push({});
}}
>
添加一行
</button>
</div>
);
});
MyTable.Column = () => {
return <Fragment></Fragment>;
};
export default MyTable;
因此,我们实现中,先分析Schema所有的Column描述,并保存下来对应的Schema。
- 对于每个Column组件,放到header来渲染
- 对于每个单元格,我们用RecursionField的onlyRenderProperties来渲染,避免渲染Column组件本身。另外,我们设置RecursionField的name为index。
6.6 ArrayTable的列组件的联动实现
import { createForm, onFieldInputValueChange } from '@formily/core';
import {
createSchemaField,
Field,
FormConsumer,
ObjectField,
} from '@formily/react';
import MyTable from './MyTable2';
import { Form, FormItem, Input, Select } from '@formily/antd';
const form = createForm({
effects: () => {
onFieldInputValueChange('data', (field) => {
form.setFieldState('data.firstColumn', (state) => {
let compontProps = state.componentProps;
if (compontProps) {
console.log('change title');
compontProps.title = '名字:' + field.value.length + '行';
}
});
});
},
});
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
Select,
MyTable,
},
});
export default () => {
//MyTable实现了自增,Column支持effects了,但没有自增列
return (
<div>
<Form form={form} feedbackLayout="terse">
<SchemaField>
<SchemaField.Array name="data" x-component="MyTable">
<SchemaField.Void>
<SchemaField.Void
name="firstColumn"
x-component="MyTable.Column"
x-component-props={{
title: '名字',
style: {
width: '100px',
},
}}
>
<SchemaField.String
name="name"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField.Void>
<SchemaField.Void
x-component="MyTable.Column"
x-component-props={{
title: '年龄',
}}
>
<SchemaField.String
name="age"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField.Void>
</SchemaField.Void>
</SchemaField.Array>
</SchemaField>
<FormConsumer>
{() => <div>{JSON.stringify(form.values)}</div>}
</FormConsumer>
</Form>
</div>
);
};
在6.5列实现的Table的一个问题是,Column组件仅仅作为Schema描述,而不是一个实际的VoidField。当我们在effects中动态修改Column组件的component-props的时候,UI没有反应。
import { ArrayField, Field } from '@formily/core';
import {
RecursionField,
Schema,
useField,
useFieldSchema,
useForm,
} from '@formily/react';
import { observer } from '@formily/reactive-react';
import React, { Fragment, ReactNode, useContext } from 'react';
import { ReactElement } from 'react';
import TableStyle from './style.css';
type Column = {
style: object;
title: string;
name: string;
schema: Schema;
};
function getColumn(schema: Schema): Column[] {
//在当前实现中,Column层看成是Field
let itemsSchema: Schema['items'] = schema.items;
const items = Array.isArray(itemsSchema) ? itemsSchema : [itemsSchema];
//获取当前array的field
let form = useForm();
let field = useField();
const parseSource = (schema: Schema): Column[] => {
//在渲染的时候,手动拿出每个Column的Field,并且将Schema作为保底逻辑
//这里的写法,其实是先取field数据,再去createField
//当第一次render的时候,Field不存在时,返回值为undefined
let columnField = form.query(field.address + '.' + schema.name).take();
console.log('field:', columnField);
let component = schema['x-component'];
if (component?.indexOf('Column') != -1) {
//获取该列的信息
return [
{
name: schema.name + '',
style:
columnField?.componentProps?.stype ||
schema['x-component-props']?.style,
title:
columnField?.componentProps?.title ||
schema['x-component-props']?.title,
schema: schema,
},
];
}
return [];
};
const reduceProperties = (schema: Schema): Column[] => {
//对于items里面的每个schema,遍历它的Properties
if (schema.properties) {
return schema.reduceProperties((current, schema) => {
return current.concat(parseSource(schema));
}, [] as Column[]);
} else {
return [];
}
};
return items.reduce((current, schema) => {
//遍历每个items里面的schema
if (schema) {
return current.concat(reduceProperties(schema));
}
return current;
}, [] as Column[]);
}
type PropsType = Field & {
children: (index: number) => ReactElement;
};
type MyTableType = React.FC<PropsType> & {
Column?: React.FC<any>;
};
const MyTable: MyTableType = observer((props: PropsType) => {
const field = useField<ArrayField>();
const fieldSchema = useFieldSchema();
const tableColumns = getColumn(fieldSchema);
console.log('Render Column', tableColumns);
const renderHeader = () => {
let row = tableColumns.map((column) => {
return (
<td
style={column.style}
className={TableStyle.td}
key={column.name}
>
{column.title}
</td>
);
});
return <tr>{row}</tr>;
};
const renderRow = (field: any, index: number) => {
//注意这里的写法RecusionField是使用onlyRenderProperties,只渲染它的子节点
//但是因为RecursionField传入了index作为name,所以每个Property的name为parent.address+index+field name
let row = tableColumns.map((column) => {
return (
<td className={TableStyle.td} key={column.name}>
{
<RecursionField
name={index}
schema={column.schema}
onlyRenderProperties
/>
}
</td>
);
});
return (
<tr className={TableStyle.tr} key={index}>
{row}
</tr>
);
};
return (
<div
style={{
border: '2px solid rgb(186 203 255)',
}}
>
<table className={TableStyle.table}>
<thead>{renderHeader()}</thead>
<tbody>
{field.value?.map((row, index) => {
return renderRow(row, index);
})}
</tbody>
</table>
{tableColumns.map((column) => {
//这里实际渲染每个Column,以保证Column能接收到Reaction
//注意要使用onlyRenderSelf
return (
<RecursionField
key={column.name}
name={column.name}
schema={column.schema}
onlyRenderSelf
/>
);
})}
<button
onClick={() => {
field.push({});
}}
>
添加一行
</button>
</div>
);
});
MyTable.Column = () => {
return <Fragment></Fragment>;
};
export default MyTable;
解决方法就是,将读取Column的Schema的时候,不仅要考虑Schema本身,还要将Column的这个Field读取出来分析。而且,在Table的底部,我们需要用RecursionField将Column组件实际渲染出来,这是为了利用RecursionField调用它的createVoidField来创建Void字段。
这个时候,Column组件的title会自动随着Table的行数来自动变化了
6.7 ArrayTable的序号与删除组件的实现
import { createForm, onFieldInputValueChange } from '@formily/core';
import {
createSchemaField,
Field,
FormConsumer,
ObjectField,
} from '@formily/react';
import MyTable from './MyTable3';
import { Form, FormItem, Input, Select } from '@formily/antd';
const form = createForm({
effects: () => {
onFieldInputValueChange('data', (field) => {
form.setFieldState('data.firstColumn', (state) => {
let compontProps = state.componentProps;
if (compontProps) {
console.log('change title');
compontProps.title = '名字:' + field.value.length + '行';
}
});
});
},
});
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
Select,
MyTable,
},
});
export default () => {
//MyTable实现了自增,Column支持effects了,但没有自增列
return (
<div>
<Form form={form} feedbackLayout="terse">
<SchemaField>
<SchemaField.Array name="data" x-component="MyTable">
<SchemaField.Void>
<SchemaField.Void
x-component="MyTable.Column"
x-component-props={{
title: '序号',
}}
>
<SchemaField.Void x-component="MyTable.Index" />
</SchemaField.Void>
<SchemaField.Void
name="firstColumn"
x-component="MyTable.Column"
x-component-props={{
title: '名字',
style: {
width: '100px',
},
}}
>
<SchemaField.String
name="name"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField.Void>
<SchemaField.Void
x-component="MyTable.Column"
x-component-props={{
title: '年龄',
}}
>
<SchemaField.String
name="age"
x-component="Input"
x-decorator="FormItem"
/>
</SchemaField.Void>
<SchemaField.Void
x-component="MyTable.Column"
x-component-props={{
title: '操作',
}}
>
<SchemaField.Void x-component="MyTable.Remove" />
</SchemaField.Void>
</SchemaField.Void>
</SchemaField.Array>
</SchemaField>
<FormConsumer>
{() => <div>{JSON.stringify(form.values)}</div>}
</FormConsumer>
</Form>
</div>
);
};
最后,我们来模拟实现一下ArrayTable的Index组件与Remove组件。
Index组件与Remove组件的特点是,它们不是一个字段组件,但是又既可以操作父级字段,以及知道自己在哪一个行。
import { ArrayField, Field } from '@formily/core';
import {
RecursionField,
Schema,
useField,
useFieldSchema,
useForm,
} from '@formily/react';
import { observer } from '@formily/reactive-react';
import React, { Fragment, ReactNode, useContext } from 'react';
import { createContext } from 'react';
import { ReactElement } from 'react';
import TableStyle from './style.css';
type Column = {
style: object;
title: string;
name: string;
schema: Schema;
};
const ArrayContext = createContext({} as ArrayField);
const ArrayIndexContext = createContext(0);
function getColumn(schema: Schema): Column[] {
//在当前实现中,Column层看成是Field
let itemsSchema: Schema['items'] = schema.items;
const items = Array.isArray(itemsSchema) ? itemsSchema : [itemsSchema];
//获取当前array的field
let form = useForm();
let field = useField();
const parseSource = (schema: Schema): Column[] => {
//在渲染的时候,手动拿出每个Column的Field,并且将Schema作为保底逻辑
//这里的写法,其实是先取field数据,再去createField
//当第一次render的时候,Field不存在时,返回值为undefined
let columnField = form.query(field.address + '.' + schema.name).take();
console.log('field:', columnField);
let component = schema['x-component'];
if (component?.indexOf('Column') != -1) {
//获取该列的信息
return [
{
name: schema.name + '',
style:
columnField?.componentProps?.stype ||
schema['x-component-props']?.style,
title:
columnField?.componentProps?.title ||
schema['x-component-props']?.title,
schema: schema,
},
];
}
return [];
};
const reduceProperties = (schema: Schema): Column[] => {
//对于items里面的每个schema,遍历它的Properties
if (schema.properties) {
return schema.reduceProperties((current, schema) => {
return current.concat(parseSource(schema));
}, [] as Column[]);
} else {
return [];
}
};
return items.reduce((current, schema) => {
//遍历每个items里面的schema
if (schema) {
return current.concat(reduceProperties(schema));
}
return current;
}, [] as Column[]);
}
type PropsType = Field & {
children: (index: number) => ReactElement;
};
type MyTableType = React.FC<PropsType> & {
Column?: React.FC<any>;
Index?: React.FC<any>;
Remove?: React.FC<any>;
};
const MyTable: MyTableType = observer((props: PropsType) => {
const field = useField<ArrayField>();
const fieldSchema = useFieldSchema();
const tableColumns = getColumn(fieldSchema);
const renderHeader = () => {
let row = tableColumns.map((column) => {
return (
<td
style={column.style}
className={TableStyle.td}
key={column.name}
>
{column.title}
</td>
);
});
return <tr>{row}</tr>;
};
const renderRow = (field: any, index: number) => {
//注意这里的写法RecusionField是使用onlyRenderProperties,只渲染它的子节点
//但是因为RecursionField传入了index作为name,所以每个Property的name为parent.address+index+field name
let row = tableColumns.map((column) => {
return (
<td className={TableStyle.td} key={column.name}>
{
<RecursionField
name={index}
schema={column.schema}
onlyRenderProperties
/>
}
</td>
);
});
//在这里注入index
return (
<ArrayIndexContext.Provider value={index}>
<tr className={TableStyle.tr} key={index}>
{row}
</tr>
</ArrayIndexContext.Provider>
);
};
return (
<div
style={{
border: '2px solid rgb(186 203 255)',
}}
>
<ArrayContext.Provider value={field}>
<table className={TableStyle.table}>
<thead>{renderHeader()}</thead>
<tbody>
{field.value?.map((row, index) => {
return renderRow(row, index);
})}
</tbody>
</table>
</ArrayContext.Provider>
{tableColumns.map((column) => {
//这里实际渲染每个Column,以保证Column能接收到Reaction
//注意要使用onlyRenderSelf
return (
<RecursionField
key={column.name}
name={column.name}
schema={column.schema}
onlyRenderSelf
/>
);
})}
<button
onClick={() => {
field.push({});
}}
>
添加一行
</button>
</div>
);
});
MyTable.Column = () => {
return <Fragment></Fragment>;
};
MyTable.Index = () => {
const indexContext = useContext(ArrayIndexContext);
return <span>{indexContext + 1}</span>;
};
MyTable.Remove = () => {
const arrayContext = useContext(ArrayContext);
const indexContext = useContext(ArrayIndexContext);
return (
<a
onClick={() => {
arrayContext.remove(indexContext);
}}
>
{'删除'}
</a>
);
};
export default MyTable;
解决办法就是,创建两个Context,ArrayContext来传递Array本身,ArrayIndexContext来传递哪一个行这个数据。
7 FAQ
7.1 value与onChange的隐式传递
具体看这里
Formily对于Object Field,Array Field,Void Field默认都会传递value与onChange方法。同时React默认会对onChange事件进行冒泡(这点真的恶心),所以,如果一个input组件发生了onChange操作,它上层的div组件也会收到onChange事件。同时,我们将该div组件放入Object Field的话就会出问题。
解决方法也很简单,按照语义的方式写代码,放入x-component的组件必须是输入组件,非输入组件就放入x-decorator里面就可以了
7.2 onClick方法指向scope
这点是很不推荐的,因为scope是放在SchemaField里面的,取不到组件的data数据,将onClick的方法指向到scope里面是不推荐的,还不如用onFieldReact来写。
7.3 effects里面的报错没有控制台输出
代码在这里
import {
createForm,
Field,
onFieldChange,
onFieldInputValueChange,
} from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';
import React, { useMemo } from 'react';
import { observable } from '@formily/reactive';
import 'antd/dist/antd.compact.css';
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
Select,
},
});
export default () => {
const form = useMemo(() => {
return createForm({
values: {},
effects: () => {
onFieldInputValueChange('title', (field) => {
const field2 = field as Field;
console.log('title change to ',field2.value);
//这里故意写错,抛出了异常,但是控制台没有输出
field.doSomeErrorThing();
console.log('title change2',field2.value);
});
},
});
}, []);
return (
<Form form={form} feedbackLayout="terse">
<SchemaField>
<SchemaField.String
name="title"
x-component={'Input'}
/>
</SchemaField>
<FormConsumer>
{() => <div>{JSON.stringify(form.values)}</div>}
</FormConsumer>
</Form>
);
};
在onFieldInputValueChange或者onFieldValueChange里面的代码如果写错了,控制台是没有错误输出的,这点要注意一下,体验并不太好。
这个问题后来被修复了,看这里
7.4 dataSource等Field属性只能看成是首次赋值有效
代码看这里
import {
createForm,
Field,
onFieldChange,
onFieldInputValueChange,
} from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';
import React, { useMemo } from 'react';
import { Button } from 'antd';
import { observable } from '@formily/reactive';
import 'antd/dist/antd.compact.css';
import { useState } from 'react';
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
Select,
},
});
type SelectType = {
label:string;
value:number;
}
export default () => {
let [select,setSelect] = useState<SelectType[]>([{
label:'啊',
value:1,
}]);
const form = useMemo(() => {
return createForm({
values: {},
effects: () => {
onFieldInputValueChange('title', (field) => {
const field2 = field as Field;
console.log('title change to ',field2.value);
});
},
});
}, []);
const toggleSelect = ()=>{
if( select.length == 0 || select.length == 1 ){
setSelect([{
label:'你',
value:2,
},{
label:'好',
value:3,
}]);
}else{
setSelect([{
label:'啊',
value:1,
}]);
}
}
console.log(' currentSelect ',select);
return (
<div>
<Button onClick={toggleSelect}>切换Select</Button>
<Form form={form} feedbackLayout="terse">
<SchemaField>
<SchemaField.String
name="title"
enum={select}
x-decorator={'FormItem'}
x-component={'Select'}
/>
</SchemaField>
<FormConsumer>
{() => <div>{JSON.stringify(form.values)}</div>}
</FormConsumer>
</Form>
</div>
);
};
在点击按钮以后,Field里面的dataSource属性是不会改动的。因为传入Field的属性仅仅是在该PATH下面的Field首次赋值的时候有效,第二次传入的时候,它会直接拿Form里面的state来操作。
这点跟React的UI = f(state)的方式是不太相同的,formily应该看成是类似Vue的框架,你要修改Form的state本身,而不是修改Field传入的属性来修改UI的。但是,formily的Field的存在与否依然是通过React的Field创建来确定的。
import {
createForm,
Field,
onFieldChange,
onFieldInputValueChange,
} from '@formily/core';
import { createSchemaField, FormConsumer } from '@formily/react';
import { Form, FormItem, Input, Select } from '@formily/antd';
import React, { useMemo } from 'react';
import { Button } from 'antd';
import { observable } from '@formily/reactive';
import 'antd/dist/antd.compact.css';
import { useState } from 'react';
import { useRef } from 'react';
import {observer} from '@formily/reactive-react';
const SchemaField = createSchemaField({
components: {
FormItem,
Input,
Select,
},
});
type SelectType = {
label:string;
value:number;
}
export default observer(() => {
let ref = useRef<SelectType[]>([]);
const form = useMemo(() => {
return createForm({
values: {},
effects: () => {
onFieldInputValueChange('title', (field) => {
const field2 = field as Field;
console.log('title change to ',field2.value);
});
},
});
}, []);
const toggleSelect = ()=>{
if( ref.current.length == 0 || ref.current.length == 1 ){
ref.current = [{
label:'你',
value:2,
},{
label:'好',
value:3,
}];
}else{
ref.current =[{
label:'啊',
value:1,
}];
}
form.setFieldState('title',(state)=>{
state.dataSource = ref.current;
})
}
console.log('value',form.values.title);
return (
<div>
<Button onClick={toggleSelect}>切换Select</Button>
<Form form={form} feedbackLayout="terse">
<SchemaField>
<SchemaField.String
name="title"
x-decorator={'FormItem'}
x-component={'Select'}
/>
</SchemaField>
<FormConsumer>
{() => <div>{JSON.stringify(form.values)}</div>}
</FormConsumer>
</Form>
</div>
);
});
要解决上面的问题,我们需要在点击按钮以后,显式地更改form的dataSource属性才能生效。
小结一下:
- Field的动态数量显示,可以通过React的方式setState来修改。
- Field的属性,dataSource,editable,visible,只能通过formily的方式,通过setFieldState来修改。
7.5 setFieldState与onValueChange不要用
import { createSchemaField, observer } from "@formily/react";
import { Button } from "antd";
import {
Input,
Select,
FormItem,
Submit,
Form,
Space,
FormLayout,
FormButtonGroup,
Reset,
NumberPicker,
ArrayTable,
} from "@formily/antd";
import {
Field,
onFieldInputValueChange,
onFieldChange,
createForm,
} from "@formily/core";
import { useEffect, useMemo } from "react";
const SchemaField = createSchemaField({
components: {
Input,
Select,
FormItem,
FormLayout,
Space,
Button,
Submit,
ArrayTable,
NumberPicker,
},
});
const UsetDetail: React.FC<any> = observer((props) => {
const form = useMemo(() => {
return createForm({
values: {
detail: {} as any,
},
effects: () => {
onFieldChange("detail.type", (f) => {
//初次type启动时,以及ajax数据返回后type的变化
const typeField = f as Field;
const typeValue = typeField.value;
refreshTableColumn(typeValue);
});
},
});
}, []);
const refreshTableColumn = (typeValue: string) => {
form.setFieldState("detail.selectItems", (state) => {
if (typeValue == "COMBO") {
state.visible = true;
} else {
state.visible = false;
}
});
};
useEffect(() => {
setTimeout(() => {
//模拟ajax请求
form.values.detail = {
type: "COMBO",
selectItems: [
{
amount: "123",
},
{
amount: "456",
},
],
};
}, 2000);
}, []);
const subItemSchema = (
<SchemaField.Array
name="selectItems"
title="子物料"
x-component="ArrayTable"
x-component-props={{
bordered: true,
}}
x-decorator="FormItem"
>
<SchemaField.Void>
<SchemaField.Void
name="AmountColumn"
title="默认配量"
x-component="Table.Column"
>
<SchemaField.String
name="amount"
default={1}
x-component="NumberPicker"
x-decorator="FormItem"
required={true}
/>
</SchemaField.Void>
</SchemaField.Void>
</SchemaField.Array>
);
const formSchema = (
<SchemaField>
<SchemaField.Object name="detail">
<SchemaField.String
name="type"
title="类型"
enum={[
{ label: "普通物料", value: "NORMAL" },
{ label: "属性物料", value: "PROPERTY" },
{ label: "选项物料", value: "SELECT" },
{ label: "组合物料", value: "COMBO" },
]}
required={true}
x-decorator="FormItem"
x-component="Select"
x-component-props={{}}
/>
{subItemSchema}
</SchemaField.Object>
</SchemaField>
);
return (
<Form form={form} feedbackLayout={"terse"}>
{formSchema}
<FormButtonGroup gutter={10}>
<Submit onSubmit={() => {}}>提交</Submit>
<Reset>重置</Reset>
</FormButtonGroup>
</Form>
);
});
export default UsetDetail;
看这里
在rc17版本下,以上的代码会出现问题,根本原因是,setFieldState会将写入请求进入堆栈,在field挂载以后,拉出来运行。这个实现对于Formily开发者以及Formily的使用者来说,都是不少的心智负担。因为,setFieldState的闭包什么时候触发是完全无概念的,它可能在ajax结果返回前,也可能在ajax结果返回后执行的。在加上,js是无栈异步模型,这种异步触发事件不确定的问题,出了问题超级难排查。
更好的方法,应该是直接用form.query查询字段模型,存在的时候就直接赋值,不存在的时候就不进行赋值。让赋值操作称为普通的同步操作,而不是带异步的操作。这样代码的执行时机更清晰,对开发者心智要求要低得多。
另外,不要使用onFieldChange和onFieldValueChange,因为它们的触发时机也是不确定的,它们会在开发者修改value,或者首次挂载组件,但其他组件仍没有挂载的时候触发。因为,onFieldValueChange会在其他组件没有挂载的时候会触发,所以Formily提供了setFieldState这样的API接口来弥补这个问题。
个人认为,更好的方法应该是:
- Formily的开发者与使用者都不应该使用onFieldChange和onFieldValueChange方法来编写业务代码,仅仅在组件库代码中可以使用。在业务代码中,应该只使用onFieldInputValueChange来编写用户改变值后的触发操作。至于开发者代码变更值的操作,应该显式调用相关代码,而不是依赖onFieldValueChange来做隐式触发。
- Formily的开发者与使用者都不应该使用setFieldState方法,而仅仅使用form.query,或者field.query的方法来同步修改组件状态。业务流程更加清晰,而且更少歧义。
在业务开发中,我们应该尽可能倾向于编写更难出错的,一眼就能看清的代码。而不是编写省事(行数更少,看起来优雅)但是难以理解的代码。
7.6 query.take()未创建组件的问题
import { createSchemaField, observer } from "@formily/react";
import { Button } from "antd";
import {
Input,
Select,
FormItem,
Submit,
Form,
Space,
FormLayout,
FormButtonGroup,
Reset,
NumberPicker,
ArrayTable,
} from "@formily/antd";
import {
Field,
onFieldInputValueChange,
onFieldChange,
createForm,
} from "@formily/core";
import { useEffect, useMemo, useState } from "react";
const SchemaField = createSchemaField({
components: {
Input,
Select,
FormItem,
FormLayout,
Space,
Button,
Submit,
ArrayTable,
NumberPicker,
},
});
const UsetDetail: React.FC<any> = observer((props) => {
const [stateRefresh, setStateRefresh] = useState(1);
const form = useMemo(() => {
return createForm({
values: {
detail: {} as any,
},
});
}, []);
const clickMe = () => {
const field1 = form.query("detail.items.0.amount").take();
console.log("field1 ", field1);
form.values.detail = {
items: [
{
amount: "123",
},
],
};
//只创建value的话,是拿不到这个Field的,只有这个Field被render出来才能拿到
//const field2 = form.query("detail.items.0.amount").take();
//console.log("field2 ", field2);
//手动setState的方法并不能马上render页面
setStateRefresh(stateRefresh + 1);
console.log("go");
//你需要用timeout来延迟设置,在render以后再触发后面的代码
setTimeout(() => {
const field2 = form.query("detail.items.0.amount").take();
console.log("field2 ", field2);
}, 100);
//这个问题是Formily的弱点,因为Formily设置在field上的dataSource只能是首次有效
//后续的都需要经过设置dataSource都需要经过formily的query再set的方法。
//而formily的set方法又需要组件创建以后才能使用,组件的创建时需要等待React触发后创建的,这点Formily是无法控制的生命周期
//这里只能寄望setTimeout之后,组件已经被React render出来了
//form.setFieldState的方法能解决这个问题,有一定的其他使用风险。
};
console.log("pageRefresh");
const subItemSchema = (
<SchemaField.Array
name="items"
title="子物料"
x-component="ArrayTable"
x-component-props={{
bordered: true,
}}
x-decorator="FormItem"
>
<SchemaField.Void>
<SchemaField.Void
name="AmountColumn"
title="默认配量"
x-component="ArrayTable.Column"
>
<SchemaField.String
name="amount"
default={1}
x-component="NumberPicker"
x-decorator="FormItem"
required={true}
/>
</SchemaField.Void>
</SchemaField.Void>
</SchemaField.Array>
);
const formSchema = (
<SchemaField>
<SchemaField.Object name="detail">{subItemSchema}</SchemaField.Object>
</SchemaField>
);
return (
<Form form={form} feedbackLayout={"terse"}>
{formSchema}
<Button onClick={clickMe}>{"点我"}</Button>
</Form>
);
});
export default UsetDetail;
这个问题的关键在于,Formily的属性是命令式的设置,但是Formily的组件是由React的声明式的创建造成。解决这个问题有两种方法:
- 使用setTimeout,延迟命令式设置的时机。
- 使用createField,而不是query.take,这样的话要注意将所有默认数据的配置好。
7.7 React方式组合Formily
代码看这里
<div>
<Button onClick={toggleSelect}>切换Select</Button>
<Button onClick={toggleTitle}>切换Title</Button>
<Form form={form} feedbackLayout="terse">
<SchemaField>
<SchemaField.String
title={argv.title}
x-decorator={'FormItem'}
x-component={'Input'}
x-component-props={{
placeholder: argv.placeholder,
}}
/>
</SchemaField>
<FormConsumer>
{() => <div>{JSON.stringify(form.values)}</div>}
</FormConsumer>
</Form>
</div>
无论是使用SchemaField
<div>
<Button onClick={toggleSelect}>切换Select</Button>
<Button onClick={toggleTitle}>切换Title</Button>
<Form form={form} feedbackLayout="terse">
<Field
name="title"
component={[Input, { placeholder: argv.placeholder }]}
decorator={[FormItem, { title: '123' }]}
/>
<FormConsumer>
{() => <div>{JSON.stringify(form.values)}</div>}
</FormConsumer>
</Form>
</div>
还是使用Field字段,都无法直接修改本地数据,你总是需要使用form.query来获取field,然后修改它的data来触发变更,而不是直接修改赋值属性来修改它。
<div>
<Button onClick={toggleSelect}>切换Select</Button>
<Button onClick={toggleTitle}>切换Title</Button>
<Form form={form} feedbackLayout="terse">
<FormItem
label={argv.title}
asterisk={true}
feedbackText={'错误'}
feedbackStatus="error"
>
<Input placeholder={argv.placeholder} />
</FormItem>
<FormConsumer>
{() => <div>{JSON.stringify(form.values)}</div>}
</FormConsumer>
</Form>
</div>
一个简单的方法就是仅仅使用Formily的antd与reactive组件,不使用core组件。这样就不再需要使用setFieldState,也不需要使用query方法。
8 总结
ArrayField组件,Schema的设计与实现,这两点都很漂亮,值得学习。
- 本文作者: fishedee
- 版权声明: 本博客所有文章均采用 CC BY-NC-SA 3.0 CN 许可协议,转载必须注明出处!