本文是对阿里FormRender的分析。
FormRender 1.0 是下一代的 React.js 表单解决方案。项目从内核级别进行了重写,为了能切实承接日益复杂的表单场景需求。我们的目标是以强大的扩展能力对表单场景 100%的覆盖支持,同时保持开发者能快速上手,并以表单编辑器、插件、自定义组件等一系列周边产品带来极致的开发体验。
FormRender配套有在线拖拉拽的工具,见fr-generator。
FormRender和fr-generator两套工具配合起来,基本满足了项目的初始需求,因需要扩展,或许还会对源码进行改造,在此先深入研究了下FormRender@1.5.8的源码,后面再研究fr-generator。
1 使用举例
1.1 schema
{
"type": "object",
"properties": {
"input_xnNHW9": {
"title": "输入框",
"type": "string",
"props": {}
},
"textarea_Ski8kS": {
"title": "编辑框",
"type": "string",
"format": "textarea",
"props": {}
}
},
"column": 1, // 整体控制一行的列数,默认为1
"labelWidth": 120, // 整体控制标签宽度
"displayType": "row" // 整体控制表单元素与 label 同行 or 分两行展示, inline 则整个展示自然顺排,有'column'、'row'和'inline'三个选项
}
1.2 渲染
2 整体架构
3 核心
3.1 Core
<Core />
组件经过简单预处理后将schema和表单布局等信息传给<MCore/>
,而MCore = React.memo(CoreRender, areEqual),会根据表单值、错误提示和schema等信息是否有变化来决定是否重新渲染<CoreRender />(在form-render@1.8.5中去掉了MCore这一层):
const areEqual = (prev, current) => {
if (prev.allTouched !== current.allTouched) {
return false;
}
if (prev.displayType !== current.displayType) {
return false;
}
if (prev.column !== current.column) {
return false;
}
if (prev.labelWidth !== current.labelWidth) {
return false;
}
if (
JSON.stringify(prev._value) === JSON.stringify(current._value) &&
JSON.stringify(prev.schema) === JSON.stringify(current.schema) &&
JSON.stringify(prev.errorFields) === JSON.stringify(current.errorFields)
) {
return true;
}
return false;
};
const MCore = React.memo(CoreRender, areEqual);
3.2 CoreRender
<div
style={columnStyle}
className={`${containerClass} ${debugCss ? 'debug' : ''}`}
>
<RenderField {...fieldProps}>{_children}</RenderField>
</div>
3.2.1 _children:根据子元素的类型是object、数组或checkbox来决定调用的组件
// 计算 children
let _children = null;
if (hasChildren) {
if (isObj) {
_children = objChildren; // 即RenderObject组件
} else if (isList) {
_children = listChildren; // 即RenderList组件
}
} else if (isCheckBox) {
_children = schema.title;
}
3.2.2 CoreRender
会调用RenderField组件渲染_children,RenderField的核心逻辑如下图:
ExtendedWidget中有一个逻辑是通过Suspense包裹了组件,这大概是因为有一部分组件通过React.lazy做了处理,需要配套使用Suspense。
3.2.3
RenderList
当组件类型为
list
时,会根据具体情况分别渲染成不同组件:
switch (renderWidget) {
case 'list0':
case 'cardList':
return <CardList {...displayProps} />;
case 'list1':
case 'simpleList':
return <SimpleList {...displayProps} />;
case 'list2':
case 'tableList':
return <TableList {...displayProps} />;
case 'list3':
case 'drawerList':
return <DrawerList {...displayProps} />;
case 'list4':
case 'virtualList':
return <VirtualList {...displayProps} />;
case 'tabList':
return <TabList {...displayProps} />;
default:
return <CardList {...displayProps} />;
}
3.2.4 RenderObject
const RenderObject = ({
children = [],
dataIndex = [],
displayType,
hideTitle,
}) => {
return (
<>
{children.map((child, i) => {
const FRProps = {
displayType,
id: child,
dataIndex,
hideTitle,
};
return <Core key={i.toString()} {...FRProps} />;
})}
</>
);
};
RenderObject
内部会递归调用Core
组件,一层层找到子组件配置进行渲染。
schema
有个配置类型是object
,type
是object
,title
为空,会被渲染成div
,例如根元素;如果type
为object
,但title
不为空会被渲染成Collpase
组件。
object
类型往往配置有properties,用来定义子组件们。