咱们的目标是将schema => form
表单,schema
就是一个json对象,咱们看看阿里的form-render
库(一个低代码react
的form
表单库)的schema
长什么样子:javascript
{
"type": "object",
"properties": {
"count": {
// 基础属性
"title": "代号",
"type": "string",
"min": 6,
// rules (补充校验信息)
"rules": [
{
"pattern": "^[A-Za-z0-9]+$",
"message": "只容许填写英文字母和数字"
}
],
// props (补充antd组件props)
"props": {
"allowClear": true
}
}
}
}
复制代码
虽然官方网站说这个JSON, 遵循JSON Schema
国际规范,可是我以为这个规范太麻烦了,我是按照ant-design的使用习惯来定义schema的,主要是更符合使用习惯,相似ant是这样使用组件的,vue的elementUI好像也是相似的用法:vue
<Form 这里能够定义Form的属性>
<Form.Item name="account" 这里能够定义Form.Item的属性> <Input 这里能够定义表单组件的属性 /> </Form.Item>
<Form.Item name="password"> <Input 这里能够定义表单组件的属性 /> </Form.Item>
</Form>
复制代码
因此对应跟组件使用差很少的schema定义以下:java
{
// 至关于在上面的 Form组件上定义的属性
formOptions:{
// 当字段被删除时保留字段值
// preserve:true
},
formItems: [ // 至关于Form组件里全部Form.Item组件
{
// 这个属性过重要了,必填,至关于每个组件的标识符,能够是数组
// 数组能够是字符串或者数字,以此定义嵌套对象,嵌套数组
name: 'account',
// value: '', 这里能够定义初始值,也能够不设置
options: { // 至关于Form.Item组件属性
// hidden: xx 隐藏表单逻辑
},
// 布局属性,后续会用这些属性控制组件的布局
// 布局属性就是设置一行几列表单,表单label宽高等等ui属性
// 能够看到咱们是把ui属性和逻辑上表单属性解耦了的
// 本篇文章不会涉及到这个属性
layoutOptions: { // 留给后面拓展的布局组件属性
// label: xx
},
// 组件名,这里'input'会被转化为ant的Input组件
// 会有一个map将字符串转换为组件
Wiget: 'input',
WigetOptions: {}, // 表单组件属性
},
],
}
复制代码
上面的name由于能够定义为数组,好比['a', 'b']
,因此对应form表单的{a : { b: '更改这里' }}
react
还能够定义为[a, 1]
,会被解析为更改{ a: [ undefined, '更改这里' ] }
,typescript
经过这个name的命名设置,咱们能够看到,既能知足数组嵌套,也能知足对象嵌套,因此能够知足几乎所有表单对象值的格式要求。json
因此咱们但愿form内核大概使用的方式是:redux
// 定义schema
const schema = {
formItems: [
{
name: 'account',
value: 1,
options: {
},
Wiget: 'input'
}
]
}
const Demo = () => {
const [form] = useForm({ schema });
return <Form form={form} />;
};
ReactDOM.render(
<Demo />,
document.getElementById('app')
);
复制代码
以上配置就渲染一个Input的组件,而且form
提供一系列方法就像ant
同样,能够getFiledsValue
, setFieldsValue
等等方法,让咱们的使用跟ant
几乎是无缝链接,数组
有人会说,直接用ant就能够改装啊,可是你要知道,markdown
可是ant自己一些属性是函数,JSON
上是不能挂函数的,由于JSON.stringify
会把函数过滤掉,因此,不少ant属性须要挂函数,内部就不支持了,好比onFinish
事件,shouldUpdate
方法等等antd
还有若是咱们业务某个产品须要不少自定义的需求,可能涉及到要改底层的form库,就须要本身开发一套了,因此魔改ant
的form
不太好,还不如本身开发一套呢
废话很少说,开始编码!
咱们的大致架构以下(没有写form渲染器器(便可视化拖拽表单这块功能)后续加):
上图比较简陋,咱们先把FormStore
搭建好,毕竟它是调度组件的老大,为了省时间,就不用ts了,先js跑通。
下面是使用ramda库提供的一些工具函数以及标识符,这个不重要,看函数名就能猜到这些函数什么意思了,用到的话,会具体解释这些函数的做用
import { path, clone, assocPath, merge, type, equals } from 'ramda'
// 如下是一些标识符
// 此标识符意味着通知全部组件更新
const ALL = Symbol('*');
// 此标识符用来标识formStore
const FORM_SIGN = Symbol('formStoreSign');
// 导出内部方法的标识符
const INNER_HOOKS_SIGN = Symbol("innerHooks");
复制代码
用于存放表单数据、接受表单初始值,以及封装对表单数据的操做。
class FormStore {
// 参数是初始化的values
constructor(initialValue) {
// 后续有resetValue,也就是重置表单的方法,因此要留住它
this.initialValue = initialValue
// values存储form表单的值
// clone是ramda提供的深克隆功能
this.values = initialValue ? clone(initialValue) : {}
// 事件收集器,订阅的事件(函数)都存放在这里
this.listeners = []
}
}
复制代码
这里表单数据仓库FormStore
和每个Form.Item
(用来包裹表单好比Input
组件,把Input
注册到FormStore
里)采用的通讯方式是发布订阅模式。,在FormStore
中维护一个事件回调列表listeners
,每一个Field
建立时,经过调用FormStore.subscribe(listener)
订阅表单数据变更
// 通知的方法,通知单个或者全部组件更新表单值
notify = (name) => {
for (const listener of this.listeners) listener(name)
}
// 订阅事件的方法,返回清除事件的函数,在组件卸载的时候须要清除这个组件订阅的事件
subscribe = (listener) => {
this.listeners.push(listener)
return () => {
// 取消订阅
const index = this.listeners.indexOf(listener)
if (index > -1) this.listeners.splice(index, 1)
}
}
复制代码
上面须要注意的是:
this.notify(name)
中的的name
,能够是数组或者字符串,好比['account', 'CCB']
, ['account', 0]
再添加getFieldValues
、getFieldValue
,setFieldValue
, setFieldsValue
函数,做用分别是:
notify(name)
,通知单个表单更新notify(name)
,以保证全部的表单变更都会触发通知// 获取表单值
getFieldValues = (name) => {
return clone(this.values)
}
// 这里的name不必定是字符串,也有多是字符串数组,或者数组下标(string | string | number[])
// 例如:name = ['a', 'b']意思是获取form表单值(value)对象的value[a][b]属性值
getFieldValue = (name) => {
if (typeof name !== 'string' && !Array.isArray(name)) {
throw new Error(`参数 ${name} 须要是字符串或者数组`)
}
// strToArray定义在下面,就是转化为数组的函数
// 由于path第一个参数必须是数组,name有多是字符串
// path用法:
// path(['a', 'b'], {a: {b: 2}}) => 2
return path(strToArray(name), this.values)
}
// 设置form表单 单个值的方法
setFieldValue = (name, value) => {
const newName = strToArray(name)
// assocPath是ramda用来给对象设置值的函数
// assocPath用法:
// assocPath(['a', 'b', 'c'], 42, {a: {b: {c: 0}}})
// => {a: {b: {c: 42}}}
this.values = assocPath(newName, value, this.values)
// 发布事件,咱们的事件都是以名字字符串做为标识
this.notify(name)
}
// 设置form表单 多个值的方法
setFieldsValue = (value) => {
// 若是value不是对象({}这样的对象,其它数组这些对象不行,此函数不执行
if (R.type(value) !== 'Object') return
// pickPath方法能把对象解析为路径
// pickPaths({a: 2, c: 3 })
// => [[{path: 'a', value: 2 }], [{ path: 'c', vlaue: 3 }]]
const paths = pickPaths(value)
paths.forEach((item) => {
this.values = assocPath(item.path, item.value, this.values)
})
this.notify(ALL)
}
复制代码
而后还有一些工具函数以及导出的函数,功能和做用都写在注释里,这样FormStore组件大体完成。
// 暴露formStore的内部方法给外面,不让其直接访问FormStore
getFormExport = (schema) => {
return {
signType: FORM_SIGN,
getFieldValue: this.getFieldValue,
setFieldValue: this.setFieldValue,
setFieldsValue: this.setFieldsValue,
isSamePath: this.isSamePath,
getInnerHooks: this.getInnerHooks(schema),
}
}
// 判断两个路径是否相等,以下
// equals([1, 2, 3], [1, 2, 3]); //=> true
isSamePath = (path1, path2) => {
if (type(path1) !== 'Array' || type(path2) !== 'Array') {
throw new Error(`isSamePath函数的参数均需数组`)
}
return equals(path1, path2) //=> true
}
// 获取内部方法,只在内部组件使用
getInnerHooks = schema => sign => {
if(sign === INNER_HOOKS_SIGN) {
return {
getFieldValue: this.getFieldValue,
setFieldValue: this.setFieldValue,
setFieldsValue: this.setFieldsValue,
isSamePath: this.isSamePath,
subscribe: this.subscribe,
notify: this.notify,
schema
}
}
console.warn('外部禁止使用getInnerHooks方法');
return null;
}
// 下面是工具函数
// 此函数就是把字符串转数组的函数
const strToArray = (name) => {
if (typeof name === 'string') return [name]
if (Array.isArray(name)) return name
throw new Error(`${name} 参数必须是数组或者字符串`)
}
// 这个函数是用来提取对象的路径的好比说:
// pickPaths({a: 2, c: 3 })
// => [[{path: 'a', value: 2 }], [{ path: 'c', vlaue: 3 }]]
// pickPaths({ b:[ { a : 1 } ] )
// => [[ { path: [ "b", 0, "a"], value: 1 }]]
function pickPaths(root, collects = [], resultPaths = []) {
function dfs(root, collects) {
if (type(root) === 'Object') {
return Object.keys(root).map((item) => {
const newCollect = clone(collects)
newCollect.push(item)
return dfs(root[item], newCollect)
})
}
if (type(root) === 'Array') {
return root.map((item, index) => {
const newCollect = clone(collects)
newCollect.push(index)
return dfs(item, newCollect)
})
}
return resultPaths.push({ path: collects, value: root })
}
dfs(root, collects)
return resultPaths
}
复制代码
好了,咱们能够试试咱们刚才写的的FormStore
组件能干啥了
const formStore = new FormStore({ account: [ { name: 'CCB' } ] });
formStore.setFieldsValue({ account: [ { name: 'xiaoming' }, 123 ] });
// 打印formStore.value
// => { account: [ { name: 123 }, 123 ] }
console.log(formStore.values)
formStore.setFieldValue([ 'account', 1, 'age' ], 10)
// => { account: [ { name: 123 }, age: 10 ] }
console.log(formStore.values)
复制代码
上面能够看到,这个路径解析模块对咱们来讲很是重要,因此后续我会把它单独提取
出来做为一个服务,咱们在平时的业务代码里,也须要把这些比较重要的模块,单独提取成服务类
,或者hooks
。
其次后面会用函数式写法再重构一下具体的函数。上面的写法只是为了避免了解函数式和不会使用ramda库
的同窗看。
咱们接着再简单试一下formStore的注册函数功能
const formStore = new FormStore({ account: [{ name: "CCB" }] });
formStore.subscribe((name)=>{
if(name === ALL || formStore.isSamePath(name, [ 'account', 0, 'name' ])){
console.log('路径匹配 [ account, 0, name ]')
}
})
// formStore.setFieldsValue({ account: [{ name: "A" }] })
// => 打印 路径匹配 [ account, 0, name ]
formStore.setFieldValue([ 'account', 0, 'name' ], 'A')
复制代码
好了,这个模块按道理个人测试用例须要用测试库的,这里就不用了,欢迎过两天你们去看个人即将发布的jest入门
。(主要是为了宣传这个,不停的学习,棒棒哒😄)
上面subscribe
订阅事件和notify
发布事件是一个简单的发布订阅模型。说白了跟redux的源码差很少,订阅事件就是把订阅的函数放到一个数组,发布事件就是把数组里的函数拿出来调用一遍。
接下来咱们看看Form组件是怎样的,Form
组件至关简单,也只是为了提供一个入口和传递上下文。
props
接收一个FormStore
的实例(这个实例经过useForm({ schema })产生,后面会讲这个useForm是怎么实现的),并经过Context
传递给子组件(即Field
)中
import { INNER_HOOKS_SIGN } form './utils';
import { FormContext } from './context';
// form组件映射关系
const WigetsMap = {
input: Input
}
function Form(props) {
if (props.form.signType !== FORM_SIGN) throw new Error('form类型错误');
// 这里的form是后面useForm产生的对象
// 这个对象实际是formStore的exportForm方法导出的对象
// signType用来标示是咱们的formStore.exportForm方法导出的对象
if(form.signType !== FORM_SIGN) throw new Error('form类型错误');
// 外部传的form
const { form, ...restProps } = props;
// 获取到fromStore的getInnerHooks方法导出的内部函数
const innerForm = form.getInnerHooks(INNER_HOOKS_SIGN);
return (
<form {...restProps} onSubmit={(event) => { event.preventDefault(); event.stopPropagation(); // 调用了formInstance 提供的submit方法 // innerForm.submit(); }} > {/* formInstance 当作全局的 context 传递下去 */} <FormContext.Provider value={innerForm}> {/* useForm的时候schema会传给form */} {innerForm.schema?.formItem?.map((item, index) => { return ( {/* formItem属性在传递给下面 */} <FormItem key={index} name={item.name} {...item.options}> {/* WigetOptions属性在传递给下面 */} {WigetsMap[item.Wiget] ? <item.Wiget {...item.WigetOptions} /> : null} </FormItem> ); })} </FormContext.Provider> </form>
);
}
复制代码
Form组件主要的功能就是把innerForm传递给Form.Item组件,这个innerFrom咱们看上面的FormStore组件getInnerHooks是怎么样的:
// 获取内部方法,只在内部组件使用
getInnerHooks = schema => sign => {
if(sign === INNER_HOOKS_SIGN) {
return {
getFieldValue: this.getFieldValue,
setFieldValue: this.setFieldValue,
setFieldsValue: this.setFieldsValue,
isSamePath: this.isSamePath,
subscribe: this.subscribe,
notify: this.notify,
schema,
}
}
console.warn('外部禁止使用getInnerHooks方法');
return null;
}
复制代码
能够看到导出的对象必须传入INNER_HOOKS_SIGN
标识符才能获取,INNER_HOOKS_SIGN
是组件内部的,外面使用useForm
的开发者是拿不到的,因此道处对象只服务于组件内部。
目的就是用来获取和设置属性,已经订阅和发布事件。
上文还有FormContext这个context,咱们看下这个文件长什么样
import React from 'react'
const warningFunc: any = () => {
console.warn(
'Please make sure to call the getInternalHooks correctly'
);
};
export const FormContext = React.createContext({
getInnerHooks: () => {
return {
getFieldValue: warningFunc,
setFieldValue: warningFunc,
setFieldsValue: warningFunc,
isSamePath: warningFunc,
subscribe: warningFunc,
notify: warningFunc,
};
},
});
复制代码
默认的参数就是咱们在FormStore
定义的getInnerHooks
的方法,保证它们两个函数导出属性名字一致,这里就体现了typescript
的重要性了。
欢迎你们去个人博客里看,以一篇typescript基础入门
接下来,咱们看一下,外部的useForm是怎么使用的
const useForm = (props) => {
// 检测schema是否符合规范,不符合报错
checkSchema(props.schema);
// 保存schema的值
const schemaRef = useRef(props.schema);
// 保存form的引用对象
const formRef = useRef();
// 第一次渲染初始化formStore
if (!formRef.current) {
formRef.current = new FormStore(setSchemaToValues(props.schema)).getFormExport(props.schema);
}
// 若是schema发生变化,formStore从新生成
if (JSON.stringify(props.schema) !== JSON.stringify(schemaRef.current)) {
schemaRef.current = props.schema;
formRef.current = new FormStore(setSchemaToValues(props.schema)).getFormExport(props.schema);
}
return [formRef.current];
};
// 工具函数
function checkSchema(schema) {
ifElse(
isArrayAndNotNilArray,
forEach(checkFormItems),
() => { throw new Error('formItems property of schema need to an Array') }
)(path(['formItems'], schema));
}
function checkFormItems(item) {
if (!all(equals(true))([isObject(item), isNameType(item.name)])) {
throw new Error('please check whether formItems field of schema meet the specifications');
}
}
复制代码
上面惟一指值得一说的就是useRef的使用,能够当作单例模式来用,以下:
const a = useRef();
if(!a.current) return 1;
return a.current
复制代码
第一次赋值1,若是存在就一直是1,不会变
接着咱们看一下Form.Item
组件的代码
import React, { cloneElement, useEffect, useContext, useState } from 'react'
import { FormContext } from './context';
import { ALL } form './utils';
function FormItem(props: any) {
const { name, children } = props;
// 这个是得到store的Context,后面会有讲解
const innerForm = useContext(FormContext);
// 若是若是咱们schema初始化有值,就会传到这里
const [value, setValue] = useState(name && store ? innerForm.getFieldValue(name) : undefined);
useEffect(() => {
if (!name || !innerForm) return;
// 判断n若是是ALL表示你们都要更新
// 或者单独更新这个form表单
// 要求n和name相同
return innerForm.subscribe((n) => {
if (n === ALL || (Array.isArray(n)
&& innerForm.isSamePath(n, name))) {
setValue(store.getFieldValue(name));
}
});
}, [name, innerForm]);
return cloneElement(children, {
value,
onChange: (e) => {
innerForm.setFieldValue(name, e.target.value);
},
});
}
复制代码
这里须要注意的是,cloneElement把children包装了一下,传入了value和onChange方法,例如:
<Form.Item name="account" 这里能够定义Form.Item的属性>
<Input 这里能够定义表单组件的属性 />
</Form.Item>
复制代码
这里的Input就能自动接收到value和onChange属性和方法了
这篇文章彻底是本身感兴趣低代码的form平台表单实现原理,本身查了些资料,写了一个能跑通的demo,可是原理是没有问题的,可能里面仍是会有bug,欢迎你们评论区提出,周末还在写文章,看在辛苦的份上,大哥点个赞吧,😀
下面的代码使用ramda库重构了一版,本身跑了一下,暂时没发现问题。本文后续计划以下:
import ReactDOM from 'react-dom';
import React, { useState, useContext, useEffect, useRef, cloneElement } from 'react';
import { path, clone, assocPath, type, equals, pipe, __, all, when, ifElse, F, forEach, reduce } from 'ramda';
import { Input } from 'antd';
// 常量模块
const ALL = Symbol('*');
const FORM_SIGN = Symbol('formStoreSign');
const INNER_HOOKS_SIGN = Symbol('innerHooks');
// 工具函数模块
function isString(name) {
return type(name) === 'String';
}
function isArray(name) {
return type(name) === 'Array';
}
function isArrayAndNotNilArray(name) {
if(type(name) !== 'Array') return false;
return name.length === 0 ? false : true;
}
function isUndefined(name) {
return type(name) === 'Undefined';
}
function isObject(name) {
return type(name) === 'Object';
}
function strToArray(name) {
if (isString(name)) return [name];
if (isArray(name)) return name;
throw new Error(`${name} params need to an Array or String`);
}
function isStrOrArray(name) {
return isString(name) || isArray(name);
}
const returnNameOrTrue = returnName => name => {
return returnName ? name : true;
}
function isNameType(name, returnName = false) {
return ifElse(
isStrOrArray,
returnNameOrTrue(returnName),
F,
)(name)
}
function checkSchema(schema) {
ifElse(
isArrayAndNotNilArray,
forEach(checkFormItems),
() => { throw new Error('formItems property of schema need to an Array') }
)(path(['formItems'], schema));
}
function checkFormItems(item) {
if (!all(equals(true))([isObject(item), isNameType(item.name)])) {
throw new Error('please check whether formItems field of schema meet the specifications');
}
}
function setFormReduce(acc, item) {
if (!isUndefined(item.value)) {
acc = assocPath(strToArray(item.name), item.value, acc)
}
return acc;
}
function setSchemaToValues(initialSchema) {
return pipe(
path(['formItems']),
reduce(setFormReduce, {})
)(initialSchema)
}
const warningFunc = () => {
console.warn(
'Please make sure to call the getInternalHooks correctly'
);
};
export const FormContext = React.createContext({
getInnerHooks: () => {
return {
getFieldsValue: warningFunc,
getFieldValue: warningFunc,
setFieldValue: warningFunc,
setFieldsValue: warningFunc,
isSamePath: warningFunc,
subscribe: warningFunc,
notify: warningFunc
};
}
});
function pickPaths(root, collects = [], resultPaths = []) {
function dfs(root, collects) {
if (isObject(root)) {
return dfsObj(root)
}
if (isArray(root)) {
return dfsArr(root)
}
return resultPaths.push({ path: collects, value: root })
}
function dfsObj(root) {
Object.keys(root).map((item) => {
const newCollect = clone(collects)
newCollect.push(item)
return dfs(root[item], newCollect)
})
}
function dfsArr(root) {
root.map((item, index) => {
const newCollect = clone(collects)
newCollect.push(index)
return dfs(item, newCollect)
})
}
dfs(root, collects)
return resultPaths
}
class FormStore {
constructor(initialValue) {
this.initialValue = initialValue
this.values = initialValue ? clone(initialValue) : {}
this.listeners = []
}
getFieldsValue = () => {
return clone(this.values)
}
getFieldValue = (name) => {
return ifElse(
isNameType,
pipe(strToArray, path(__, this.values)),
F,
)(name, true)
}
setFieldValue = (name, value) => {
pipe(
strToArray,
(newName) => {
this.values = assocPath(newName, value, this.values);
this.notify(name);
},
)(name)
}
setFieldsValue = (value) => {
return when(
isObject,
pipe(pickPaths, forEach((item) => {
this.values = assocPath(item.path, item.value, this.values)
}), () => this.notify(ALL)),
)(value)
}
notify = (name) => {
for (const listener of this.listeners) listener(name)
}
subscribe = (listener) => {
this.listeners.push(listener)
return () => {
const index = this.listeners.indexOf(listener)
if (index > -1) this.listeners.splice(index, 1)
}
}
getFormExport = (schema) => {
return {
signType: FORM_SIGN,
getFieldValue: this.getFieldValue,
setFieldValue: this.setFieldValue,
setFieldsValue: this.setFieldsValue,
isSamePath: this.isSamePath,
getFieldsValue: this.getFieldsValue,
getInnerHooks: this.getInnerHooks(schema)
}
}
isSamePath = (path1, path2) => {
if (type(path1) !== 'Array' || type(path2) !== 'Array') {
throw new Error('isSamePath函数的参数均需数组')
}
return equals(path1, path2)
}
getInnerHooks = schema => sign => {
if (sign === INNER_HOOKS_SIGN) {
return {
getFieldsValue: this.getFieldsValue,
getFieldValue: this.getFieldValue,
setFieldValue: this.setFieldValue,
setFieldsValue: this.setFieldsValue,
isSamePath: this.isSamePath,
subscribe: this.subscribe,
notify: this.notify,
schema
}
}
console.warn('外部禁止使用getInnerHooks方法');
return null;
}
}
const useForm = (props) => {
checkSchema(props.schema);
const schemaRef = useRef(props.schema);
const formRef = useRef();
if (!formRef.current) {
formRef.current = new FormStore(setSchemaToValues(props.schema)).getFormExport(props.schema);
}
if (JSON.stringify(props.schema) !== JSON.stringify(schemaRef.current)) {
schemaRef.current = props.schema;
formRef.current = new FormStore(setSchemaToValues(props.schema)).getFormExport(props.schema);
}
return [formRef.current];
};
function FormItem(props) {
const { name, children } = props;
// 这个是得到store的Context,后面会有讲解
const innerForm = useContext(FormContext);
// 若是咱们new FormStore有
const [value, setValue] = useState(name && innerForm ? innerForm.getFieldValue(name) : undefined);
useEffect(() => {
if (!name || !innerForm) return;
return innerForm.subscribe((n) => {
if (n === ALL || (Array.isArray(n)
&& innerForm.isSamePath(n, strToArray(name)))) {
setValue(innerForm.getFieldValue(name));
}
});
}, [name, innerForm, innerForm]);
return cloneElement(children, {
value,
onChange: (e) => {
innerForm.setFieldValue(name, e.target.value);
}
});
}
const WigetsMap = {
input: Input
}
function Form(props) {
if (props.form.signType !== FORM_SIGN) throw new Error('form类型错误');
const { form, ...restProps } = props;
const innerForm = form.getInnerHooks(INNER_HOOKS_SIGN);
return (
<form {...restProps} onSubmit={(event) => { event.preventDefault(); event.stopPropagation(); }} > <FormContext.Provider value={innerForm}> {innerForm.schema.formItems.map((item, index) => { return ( <FormItem key={index} name={item.name} {...item.options} > {WigetsMap[item.Wiget] ? <item.Wiget {...item.WigetOptions} /> : null} </FormItem> ); })} </FormContext.Provider> </form > ); } const schema = { formItems: [ { name: 'account', value: 1, options: { }, Wiget: 'input' } ] } const Demo = () => { const [form] = useForm({ schema }); window.f = form; return <Form form={form} />; }; ReactDOM.render( <Demo />, document.getElementById('app') ); 复制代码