# 低代码组件显示
低代码引擎是低代码分层架构中最复杂的部分,引擎的核心功能包含入料、设计、画布渲染和出码等,它们的含义如下:
- 入料:向引擎注入设置器、插件和组件。
- 设计:对组件进行布局设置、属性设置以及增删改操作后,形成符合页面搭建协议的JSON Schema。
- 画布渲染:将 JSON Schema 渲染成 UI 界面。
- 出码:将 JSON Schema 转化成手写代码,这通常发生在页面发布的时候。
在之前的文章中介绍了低代码跨 iframe 拖拽,本文开始介绍画布渲染。画布渲染就是将设计器生成的 JSON Schema 渲染成 UI 界面,画布渲染有一个单独的 npm 包,简称 ReactRenderer,它与设计器不直接联系,而是通过 SimulatorRenderer 与设计器联系,SimulatorRenderer是另一个 npm 包。画布渲染至少包含四部分内容:
- 显示组件:这是画布渲染最基础的部分,不涉及任何交互,只是简单的显示。
- 获取数据源:让组件获取它要显示的数据。
- 表单联动:使表单控件的状态受其他数据的控制,包含禁用联动、显隐联动和取值联动。
- 表单校验:校验表单填写的值是否符合要求,不符合则给出错误提示。
本文只介绍画布渲染中的显示组件。
ReactRenderer 的作用是将 JSON Schema 描述的组件树显示在界面上,总体而言,它是一个 React 组件,接受如下属性:
interface Props {
// 页面的 json schema
schema: PageSchema;
// schema 中使用的组件
components: Map<string, React.ElementType>;
// schema 中的组件装载到界面后要执行的勾子
onCompGetRef?: (schema: NodeSchema, domElement: HTMLElement | null) => void;
// 画布渲染模式,设计态或运行态,默认值为运行态
rendererMode?: RendererMode;
// 容器组件没有子元素时的提示语
customEmptyElement?: (schema: NodeSchema) => React.ReactNode;
}
上述 schema 和 components 是必填属性,schema 中用到组件必须在 components 中声明,否则画布无法正常渲染。onCompGetRef 是选填属性,在设计态时它为设计器获取 Node 的位置提供了可能。
假如有如下一段 JSON Schema。
{
"componentName": "Page",
"packageName": "Page",
"containerType": "Page",
"isContainer": true,
"id": "def133",
"children": [
// 行
{
"componentName": "Row",
"packageName": "vitis-lowcode-row",
"containerType": "Layout",
"isContainer": true,
"id": "def134",
"props": [],
"children": [
// 列
{
"componentName": "Column",
"packageName": "vitis-lowcode-column",
"props": {},
"isContainer": true,
"id": "def135",
"containerType": "Layout",
"children": [
{
"componentName": "Select",
"packageName": "vitis-lowcode-select",
"props": {"label": "性别"},
"extraProps": {"name": "sex"},
"isFormControl": true,
"id": "def136",
}
]
},
// 列
{
"componentName": "Column",
"packageName": "vitis-lowcode-column",
"props": {},
"isContainer": true,
"containerType": "Layout",
"children": []
"id": "def137",
}
]
}
],
"props": {
"style": "padding: 10px"
},
"extraProps": {
// 这是数据源字段,该字段在下一个小节介绍
"dataSource": {...}
}
}
上述 schema 在界面上将显示为一行两列布局,第一列有一个下拉选择器,如下图:
ReactRenderer 将 schema 描述的组件分为4种类型,如下:
- 页面容器:这是 schema 的 root 必须是页面容器,它没有发布成单独的 npm 包,而是存在于 ReactRenderer 的内部。
- 布局容器:它通常是页面容器的 children,用来控制页面的布局,比如:行和列。
- 表单控件:它处于 schema 嵌套层级的最后一层,通常位于布局容器中,既能展示数据又能接受用户输入。
- 普通UI组件:它处于 schema 嵌套层级的最后一层,通常位于布局容器中,只能显示数据不能接受用户输入。
# 页面容器
页面容器是整个画布的根节点,对应的 React 组件是 PageRenderer,与视图相关的代码如下:
function PageRenderer(props: Props) {
const context = useContext(Context)
const rootRef = useGetDOM(props.schema) // line A
const { style } = props.schema.props
return (
<div
data-node-id={props.schema.id}
className="vitis-page-container"
ref={rootRef} // line B
style={typeof style === 'string' ? transformStringToCSSProperties(style): undefined}
>{
!props.schema.children.length ?
context.customEmptyElement ? context.customEmptyElement(props.schema): null:
<>{props.schema.children.map(child => <BaseComponentRenderer schema={child} key={child.id}/>)}</>
}</div>
)
}
如果页面容器有 children,那么 PageRenderer 将遍历每一个 child,将其显示在界面,没有 children,则显示提示语。BaseComponentRenderer 是一个 React 组件工厂,它根据 schema 描述的组件类型,分门别类的渲染组件。
PageRenderer 中最重要的代码是 line A 用到的 useGetDOM,它是一个 React Hook,其作用是等组件装载之后将组件的根 DOM 元素传递出去,让设计器能完成拖拽定位。useGetDOM 的代码如下:
function useGetDOM(schema: NodeSchema) {
const context = useContext(Context)
const rootRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (context.rendererMode === RendererMode.design && context.onCompGetRef) {
context.onCompGetRef(schema, rootRef.current)
}
return () => {
if (context.rendererMode === RendererMode.design && context.onCompGetRef) {
context.onCompGetRef(schema, null)
}
}
},[])
return rootRef
}
# 布局容器
行组件和列组件都属于布局容器,但列组件必须放置在行组件的 children 中,布局容器与视图相关的代码如下:
function LayoutComponent(props: Props) {
const rootRef = useGetDOM(props.schema)
const context = useContext(Context)
const { style, ...reset } = props.schema.props
const Component = context.components.get(props.schema.componentName) // lineA
if (!Component) { return <div>未知的布局组件</div>}
return (
<Component
style={typeof style === 'string' ? transformStringToCSSProperties(style): undefined}
ref={rootRef}
{...reset}
>
{!props.schema.children.length ?
context.customEmptyElement ? context.customEmptyElement(props.schema): null
:
props.schema.children.map(child => <BaseComponentRenderer schema={child} key={child.id}/>)
}
</Component>
)
}
布局容器究竟要渲染哪一个组件,这取决于 lineA 的取值,取值结果直接决定了界面上要显示的内容。
# 表单控件
表单控件能接受用户输入,存储用户输入的值,以及表单联动将在后续的小节重点介绍。表单控件与视图相关的代码如下:
function FormControl(props: Props) {
const rootRef = useGetDOM(props.schema)
const context = useContext(Context)
// 获取要渲染组件
const Com = context.components.get(props.schema.componentName)
if (!Com) { return <div>未知的表单控件</div> }
return (<Com {...props.schema.props} ref={rootRef} /> )
}
# 普通UI组件
普通 UI 组件只能用来展示数据,它的代码如下:
function UIComponent(props: Props) {
const rootRef = useGetDOM(props.schema)
const context = useContext(Context)
const Com = context.components.get(props.schema.componentName)
if (!Com) { return <div>未知的组件</div> }
return ( <Com {...props.schema.props} ref={rootRef} />)
}
# 小节
画布渲染的第一步就是要组件显示在界面上,不涉及任何交互,获取数据源、表单联动和表单校验是下一步要做的,在后续文章将单独介绍。