渲染
维基百科中对渲染的描述如下:“渲染(英语:render,或称为绘制、彩现)在电脑绘图中,是指以软件由模型生成图像的过程。模型是用语言或者数据结构进行严格定义的三维物体或虚拟场景的描述,它包括几何、视点、纹理、照明和阴影等信息。图像是数字图像或者位图图像。渲染用于描述:计算视频编辑软件中的效果,以生成最终视频的输出过程。”
即,渲染是从模型生成影像的过程。即三个要素:模型、影像、渲染过程。
模型
模型是计算机描述语言,例如浏览器使用的是 HTML、CSS、JavaScript 作为描述语言,并生成所需模型。
影像
影像只是描述的结果,对于大多数人说,只有表达形式的不同。能比较有感知的区别是:图片、视频、操作系统等。
渲染过程
模型到影像的过程就是我们这里描述的渲染了。根据图像的载体(电视、浏览器、手机、电脑、客户端...)、模型/描述语言等的不同,渲染的过程是不同的。其对于大多数人都是黑盒,可以理解为计算机技术来实现这一黑盒。我们不止有一个黑盒,而是有千千万万个黑盒,来帮助我们实现这五彩斑斓的计算机世界。
Web 渲染
首先我们拆解一下 Web 渲染的三个要素。模型是 HTML、CSS、JavaScript 等描述语言。影像是我们在浏览器看到的网页。而渲染过程就比较复杂了,该过程是由浏览器来实现的,也就是渲染的黑盒是浏览器提供的。这里我们简单描述一下其渲染的过程:
解析
在渲染到屏幕上面之前,HTML、CSS、JavaScript 必须被解析完成。
构建 DOM 树
处理 HTML 标记并构造 DOM 树。
构建 CSSOM 树
处理 CSS 并构建 CSSOM 树。CSS 对象模型和 DOM 是相似的。DOM 和 CSSOM 是两棵树. 它们是独立的数据结构。浏览器将 CSS 规则转换为可以理解和使用的样式映射。浏览器遍历 CSS 中的每个规则集,根据 CSS 选择器创建具有父、子和兄弟关系的节点树。
渲染网页
渲染步骤包括样式、布局、绘制,在某些情况下还包括合成。在解析步骤中创建的 CSSOM 树和 DOM 树组合成一个 Render 树,然后用于计算每个可见元素的布局,然后将其绘制到屏幕上。在某些情况下,可以将内容提升到它们自己的层并进行合成,通过在 GPU 而不是 CPU 上绘制屏幕的一部分来提高性能,从而释放主线程。
交互
当上述的所有过程都完成之后,可能会再继续执行 JavaScript,由于 JavaScript 是单线程的,只有线程不处于占用状态时,才会继续执行。
以上只是很简单的描述了浏览器渲染的过程,大量的细节都可以在网上进行查询和搜索,这里不做详细描述。我们可以看到该渲染过程是非常复杂的,而使用网页的人,是完全可以不用了解这一过程的,也是不影响使用的。
低代码渲染
刚刚我们介绍了 Web 端渲染,由于我们目前的低代码渲染的载体大多数都是在 Web 浏览器上。所以低代码渲染是在 Web 渲染的基础上进行的另一个渲染黑盒。
低代码渲染的模型是基于标准的《低代码引擎搭建协议规范》产出的 Schema。而影像是同 Web 端一样的网页,由于其特性,它可以只作为网页的一部分存在,也就是区块;它也可以作为一整个应用存在,即应用级别的渲染。后续为了表述方便,我们均使用页面来指代需要渲染的影像。
Schema 和物料组件的产出在其他章节会有详细的介绍,这里不展开描述。我们主要讲解的是,基于 Schema 和物料组件,如何渲染出我们的页面。
低代码渲染就是把 Schema 和组件渲染成页面的过程。接下来我们会说明这一过程,当然就像使用网页一样,不需要了解其渲染原理也完全不影响其使用。不了解低代码渲染原理也不会影响低代码渲染能力的使用。
分类
编译模式
编译是将相同的程序从一种计算机程序语言转换到另一种语言计算机语言的过程。
由于低代码渲染实际上底层是利用的 web 端渲染能力,即最终我们的语言都会转化为 web 端的语言。所以这一个过程需要进行转化。而像是大多数编译器一样,低代码渲染的编译模式也分为预先编译(AOT)或运行时编译(JIT)形式工作。
运行时编译渲染(JIT)
运行时编译渲染又称为实时渲染。指的是,保留原始的描述模型,在网页加载并执行 JS 的时候,通过递归、计算 props、设置上下文等实时操作来完成渲染的过程。
JIT 优势
实时性强:运行时渲染是有其优势的,它省略掉了编译的时间,做到实时生效。对于低代码搭建的产品来说,进行了修改之后是不希望在 1-2 min 甚至更长的时间才能在页面中看到效果,而是希望在修改之后的下一秒就可以看到变更的效果。因为,大多数低代码的渲染都是选择这一方式,或者就是 JIT 和 AOT 的渲染能力都能提供。
轻量化:现代化的前端项目基本上都没办法离开工程化体系,一个 React 项目至少需要 Webpack、Babel、发布、部署等步骤。而对于运行时框架,我们需要的就只有 Schema 和一份前端渲染所需的 SDK。对于运营、产品、后端等低代码使用人员是十分友好的。
说完了 JIT 的优势,我们也聊聊其局限性。
JIT 局限性
性能瓶颈,和大多数的 JIT 的框架一样,JIT 渲染由于需要运行时进行编译,这个编译是会带来性能损耗的。一方面是首屏时间时间的损耗。另外一方面在交互的过程中,由于上下文的改变,需要重新进行递归计算,这个带来的损耗影响是更大的,因为这个会在首屏渲染出来之后,还会有操作卡顿、延迟等问题。
部分平台不支持 JIT,原生小程序不支持动态化渲染。小程序作为移动端上实现快速迭代、提升用户体验、集成端能力的集大成的解决方案,已经成为商业的重要技术基础设施。而原生小程序有着诸多的限制,它不能够直接操作 DOM,所以导致动态化渲染无法在该平台上完成。
预先编译渲染(AOT)
预先编译渲染又称为出码渲染,指的是,将 Schema 等模型通过编译器编译为特定语言的代码。
例如,将下面的 Schema 编译为对应的 React 代码。
{
"version": "1.0.0", // 当前协议版本号
"componentsMap": [
{ // 组件描述
"componentName": "Button",
"package": "@alifd/next",
"version": "1.0.0",
"destructuring": true,
},
{
"package": "@alifd/next",
"version": "1.3.2",
"componentName": "Page",
"destructuring": true,
}
],
"state": {
"name": "lucy"
},
"utils": [],
"componentsTree": [
{ // 描述内容,值类型 Array
"componentName": "Page", // 单个页面,枚举类型 Page|Block|Component
"fileName": "Page1",
"props": {},
"children": [
{
"componentName": "Button",
"props": {
"prop1": 1234, // 简单 json 数据
"prop2": [
{ // 简单 json 数据
"label": "选项 1",
"value": 1
},
{
"label": "选项 2",
"value": 2
}
],
"prop3": [
{
"name": "myName",
"rule": {
"type": "JSExpression",
"value": "/\w+/i"
}
}
],
"valueBind": { // 变量绑定
"type": "JSExpression",
"value": "this.state.name"
},
"onClick": { // 动作绑定
"type": "JSFunction",
"value": "function(e) { console.log(e.target.innerText) }"
},
}
}
]
}
],
}
编译之后的代码如下:
import { Button } from '@alifd/next';
import { Page } from '@alifd/next';
export default class PageExample {
state = {
name: 'lucy'
}
render() {
return (
<Page>
<Button
prop1={1234}
prop2={[{ // 简单 json 数据
"label": "选项 1",
"value": 1
}, {
"label": "选项 2",
"value": 2
}]}
prop3={[{
"name": "myName",
"rule": /\w+/i,
}]}
valueBind={this.state.name}
onClick={function(e) { console.log(e.target.innerText) }}
>
</Button>
</Page>
)
}
}
以上的预先编译渲染,我们在低代码领域叫做出码。上述的示例是出码成 React,这样就可以省略掉运行时编译的递归和计算流程,能进一步提升其性能。
如果想完全抛弃运行时相关的代码,也可以直接出码为 JavaScript + HTML。
AOT 优势
性能优化上限更高,不能说 AOT 性能就会比运行时性能好,毕竟这很大程度依赖于出码之后的代码,如果出码之后是没有性能优化的,那么它的性能很可能低于运行时。但是由于其出码的形式是没有受到限制的,所以它的性能是可以比运行时的性能优化的更好的。
AOT 缺点
成本高,由于 AOT 出码不是统一的,所以需要出码到不同的模型都需要写一份,所以出码的成本相对比较高。实时性低,出码需要较长的编译时间,所以在搭建页面修改之后,无法即时生效。所以无法使用在低代码预览场景。
渲染规模
渲染由于其使用方式不同,渲染规模是不同的。接下来我们介绍一下不同的渲染规模及其表现形式。
应用级渲染
低代码渲染托管了整个应用的渲染,包括页面、路由跳转、导航。
页面级渲染
页面(Page),由组件 + 区块组合而成。由页面容器组件包裹,可描述页面级的状态管理和公共函数。
低代码渲染只渲染应用中的页面,而页面的切换、导航等是由 ProCode 负责的,这相对来说,是需要有一定前端基础的才能更好地将其渲染出来。
区块级渲染
区块(Block),是通过低代码搭建的方式,将一系列业务组件、布局组件进行嵌套组合而成,不对外提供可配置的属性。可通过区块容器组的包裹,实现区块内部具备有完整的样式、事件、生命周期管理、状态管理、数据流转机制。能独立存在和运行,可通过复制 Schema 实现跨页面、跨应用的快速复用,保障功能和数据的正常。
低代码渲染只渲染了整个页面中的一个区块,类似于微前端的方式,互相之间的作用域和生命周期等是独立的。ProCode 需要控制区块渲染的内容的展示隐藏。
组件级别渲染
组件级别的渲染,是指在 ProCode 中使用一个低代码渲染模块,像使用一个正常的 ProCode 组件一样,可以传入 API,可以进行交互。例如:我们可以将低代码组件出码,在 ProCode 中进行引用,并传入 API 或者其他的,像使用一个正常的 ProCode 组件一样。
渲染能力
由于低代码渲染的粒度都是从应用、页面、区块、组件一层层往下的。我们通过递归处理每一个组件,而多个组件组成区块、再有区块和组件组成页面,页面和导航等再组成低代码渲染的应用。
我们从组件开始一层层解析其能力的组成。
组件渲染的能力有:
- props 解析,包括表达式、事件、函数等;
- 保留并传入上下文,包括循环上下文,插槽上下文等;
- 样式注入;
- 组件渲染,包括条件渲染、循环渲染。
区块/页面渲染的能力有:
- 区块/页面上下文生成;
- 区块/页面生命周期的生成和执行;
- 区块/页面状态、数据管理;
- 区块/页面内组件树描述生成,并递归处理;
- 区块/页面 API 支持。
应用渲染的能力包括:
- 路由跳转;
- 导航生成;
- 页面渲染;
- 工具类扩展;
- 国际化多语言支持、国际化相关 API;
- 应用级别的公共函数或第三方扩展。
以上是整个低代码渲染理想的能力,目前我们并没有全部实现,不过整体能力上是比较接近的。下面我们就实现的渲染框架说明一下技术原理。
渲染框架原理
整体架构
- 协议层:基于标准的《低代码引擎搭建协议规范》产出的 Schema 作为我们的规范协议。
- 能力层:提供组件、区块、页面等渲染所需的核心能力,包括 Props 解析、样式注入、条件渲染等。
- 适配层:由于我们使用的运行时框架不是统一的,所以统一使用适配层将不同运行框架的差异部分,通过接口对外,让渲染层注册/适配对应所需的方法。能保障渲染层和能力层直接通过适配层连接起来,能起到独立可扩展的作用。
- 渲染层:提供核心的渲染方法,由于不同运行时框架提供的渲染方法是不同的,所以其通过适配层进行注入,只需要提供适配层所需的接口,即可实现渲染。
- 应用层:根据渲染层所提供的方法,可以应用到项目中,根据使用的方法和规模即可实现应用、页面、区块的渲染。
核心解析
这里主要解析一下刚刚提到的架构中的能力层、适配层和渲染层。
能力层
- 上下文、状态和数据管理
上下文、状态和数据管理和层级以及包裹的组件是有关系的,其中页面下的组件,使用的是页面的上下文、数据和状态。在页面包裹的区块下的组件,优先使用区块下的上下文、状态和数据,如果区块中不存在,这时会去页面上下文、状态和数据中寻找。
上下文、状态和数据管理使用的是 __proto__
来实现的。当进入区块时,会新建区块数据和区块上下文,并使用 __proto__
来继承页面上下文和页面数据,这样就可以在区块中优先使用区块的数据和上下文,当区块中没有的时候,会向页面数据和上下文中查找。整体逻辑类似下面的伪代码:
// 页面上下文
var content = {
a: '1',
b: '2',
};
// 页面数据
var state = {
a: 'a',
b: 'b',
};
function Block1() {
// 区块上下文
var blockContent = {
a: '3'
};
blockContent.__proto__ = content;
// 区块数据
var blockState = {
a: 'c'
};
blockState.__proto__ = state;
// 区块内组件使用
console.log('区块内组件 a', content.a, state.a);
console.log('区块内组件 b', content.b, state.b)
// 页面内组件使用
console.log('页面内组件 a', content.a, state.a);
console.log('页面内组件 b', content.b, state.b)
Block1();
}
输出结果如下:
组件 props 解析
解析 props 时,如果遇到了表达式,会通过 new Function 来执行,并获取到计算后的 props 值,传给真实的组件。这里会获取到上下文、工具库等进行辅助计算。
const code = `with($scope || {}) { ${schema.value} }`;
new Function('$scope', code)(scope);
其中上下文通过 __proto__
保存父子关系,来确保子组件能获取到正确的上下文状态和对应的数据。
例如循环时,会在继承当前上下文时,也会新增自己的上下文对象。
const itemArg = (schema.loopArgs && schema.loopArgs[0]) || 'item';
const indexArg = (schema.loopArgs && schema.loopArgs[1]) || 'index';
schema.loop.map((item, i) => {
const loopScope = {
[itemArg]: item,
[indexArg]: i,
};
loopScope.__proto__ = superCtx;
return // ...
})
样式注入
样式注入会在渲染的时候,通过 style element 注入。
const css = getValue(this.props.__schema, 'css', '');
let style = this.styleElement;
if (!this.styleElement) {
style = document.createElement('style');
style.type = 'text/css';
style.setAttribute('from', 'style-sheet');
if (style.firstChild) {
style.removeChild(style.firstChild);
}
const head = document.head || document.getElementsByTagName('head')[0];
head.appendChild(style);
this.styleElement = style;
}
if (style.innerHTML === css) {
return;
}
style.innerHTML = css;
低代码组件渲染
低代码组件实际上也是低代码渲染的一种,只不过对于低代码页面渲染来说,它是作为一个组件注册到低代码页面渲染的上下文中的。对于低代码组件的处理逻辑为:
- 使用 Renderer 渲染生成低代码组件
class LowCodeComp extends React.Component<any, any> {
render() {
const extraProps = getLowCodeComponentProps(this.props);
return createElement(LowCodeRenderer, {
...extraProps, // 防止覆盖下面内置属性
// 使用 _schema 为了使低代码组件在页面设计中使用变量,同 react 组件使用效果一致
schema: _schema,
components: renderer.components,
designMode: renderer.designMode,
device: renderer.device,
appHelper: renderer.context,
rendererName: 'LowCodeRenderer',
customCreateElement: (Comp: any, props: any, children: any) => {
const componentMeta = host.currentDocument?.getComponentMeta(Comp.dis
playName);
if (componentMeta?.isModal) {
return null;
}
const { __id, __designMode, ...viewProps } = props;
// mock _leaf,减少性能开销
const _leaf = {
isEmpty: () => false,
};
viewProps._leaf = _leaf;
return createElement(Comp, viewProps, children);
},
});
}
}
export default LowCodeComp;
- 在页面的 Renderer 中,将生成的低代码组件传入到页面中。
import ReactRenderer from '@alilc/lowcode-react-renderer';
import ReactDOM from 'react-dom';
import { Button } from '@alifd/next';
import { LowCodeComp } from './LowCodeComp';
const schema = {
componentName: 'Page',
props: {},
children: [
{
componentName: 'Button',
props: {
type: 'primary',
style: {
color: '#2077ff'
},
},
children: '确定',
},
{
componentName: 'LowCodeComp',
props: {
type: 'primary',
style: {
color: '#2077ff'
},
},
children: '确定',
},
],
};
const components = {
Button,
LowCodeComp,
};
ReactDOM.render((
<ReactRenderer
schema={schema}
components={components}
/>
), document.getElementById('root'));
区块/页面内组件树描述生成
通过深度遍历 Schema 模型结构,针对不同类型的容器组件采用不同的渲染器进行解析并构造对应的虚拟 DOM 树。
路由跳转
路由跳转的能力需要基于 React/Rax Renderer 做一层封装,例如后续会讲述的 React/Rax Simulator。
路由跳转的能力支持可以参考下述的伪代码。
import { Router, Route, Switch } from 'react-router';
export default class SimulatorRendererView extends Component<{ rendererContainer:
SimulatorRendererContainer }> {
render() {
const { pages } = this.props;
return (
<Router>
<Routes pages={pages} />
</Router>
);
}
export class Routes extends Component<{ rendererContainer: SimulatorRendererContai
ner }> {
render() {
const { pages } = this.props;
return (
<Switch>
{pages.map(({ schema, path, id, components }) => {
return (
<Route
path={path}
key={id}
render={(routeProps) => <Renderer schema={schema} components={co
mponents} />}
/>
);
})}
</Switch>
);
}
}
class Renderer extends Component {
render() {
return (
// @ts-ignore
<LowCodeRenderer
schema={this.props.schema}
components={this.props.components}
/>
);
}
}
适配层
适配层提供的是各个框架之间的差异项。比如 React 和 Rax createElement 方法是不同的。所以需要在适配层对 API 进行抹平。
React
import { createElement } from 'react';
import {
adapter,
} from '@alilc/lowcode-renderer-core';
adapter.setRuntime({
createElement,
});
Rax
import { createElement } from 'rax';
import {
adapter,
} from '@alilc/lowcode-renderer-core';
adapter.setRuntime({
createElement,
});
这时,在核心层使用的 createElement 会基于使用不同的 renderer 而使用不同的方法,自动适配框架所需的运行时方法。这里我们对所需的方法做一个简单的介绍:
- setRuntime:设置运行时相关方法
- Component:组件类,参考 React 的 Component。
- PureComponent:组件类,参考 React 的 PureComponent。
- createContext:创建一个 Context 对象的方法。例如,当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。
- createElement:创建 Component 元素,例如在 React 中即为创建 React 元素。
- forwardRef:ref 转发的方法。Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。
- findDOMNode:是一个访问底层 DOM 节点的方法。如果组件已经被挂载到 DOM 上,此方法会返回浏览器中相应的原生 DOM 元素。
- setRenderers
- PageRenderer:页面渲染的方法。可以定制页面渲染的生命周期,定制导航,定制路由等。
- ComponentRenderer:组件渲染的方法。
- BlockRenderer:区块渲染的方法。
渲染层
React Renderer
内部的技术栈统一都是 React,大多数适配层的 API 都是按照 React 来设计的,所以对于 ReactRenderer 来说,需要做的不多。
React Renderer 的代码量很少,主要是将 React API 注册到适配层中。
import React, { Component, PureComponent, createElement, createContext, forwardRe
f, ReactInstance, ContextType } from 'react';
import ReactDOM from 'react-dom';
import {
adapter,
pageRendererFactory,
componentRendererFactory,
blockRendererFactory,
addonRendererFactory,
tempRendererFactory,
rendererFactory,
types,
} from '@alilc/lowcode-renderer-core';
import ConfigProvider from '@alifd/next/lib/config-provider';
window.React = React;
(window as any).ReactDom = ReactDOM;
adapter.setRuntime({
Component,
PureComponent,
createContext,
createElement,
forwardRef,
findDOMNode: ReactDOM.findDOMNode,
});
adapter.setRenderers({
PageRenderer: pageRendererFactory(),
ComponentRenderer: componentRendererFactory(),
BlockRenderer: blockRendererFactory(),
AddonRenderer: addonRendererFactory(),
TempRenderer: tempRendererFactory(),
DivRenderer: blockRendererFactory(),
});
adapter.setConfigProvider(ConfigProvider);
function factory() {
const Renderer = rendererFactory();
return class ReactRenderer extends Renderer implements Component {
readonly props: types.IProps;
context: ContextType<any>;
setState: (
state: types.IState,
callback?: () => void,
) => void;
forceUpdate: (callback?: () => void) => void;
refs: {
[key: string]: ReactInstance,
};
constructor(props: types.IProps, context: ContextType<any>) {
super(props, context);
}
isValidComponent(obj: any) {
return obj?.prototype?.isReactComponent || obj?.prototype instanceof Componen
t;
}
};
}
export default factory();
Rax Renderer
Rax 的大多数 API 和 React 基本也是一致的,差异点在于重写了一些方法。
import { Component, PureComponent, createElement, createContext, forwardRef } fro
m 'rax';
import findDOMNode from 'rax-find-dom-node';
import {
adapter,
addonRendererFactory,
tempRendererFactory,
rendererFactory,
} from '@alilc/lowcode-renderer-core';
import pageRendererFactory from './renderer/page';
import componentRendererFactory from './renderer/component';
import blockRendererFactory from './renderer/block';
import CompFactory from './hoc/compFactory';
adapter.setRuntime({
Component,
PureComponent,
createContext,
createElement,
forwardRef,
findDOMNode,
});
adapter.setRenderers({
PageRenderer: pageRendererFactory(),
ComponentRenderer: componentRendererFactory(),
BlockRenderer: blockRendererFactory(),
AddonRenderer: addonRendererFactory(),
TempRenderer: tempRendererFactory(),
});
function factory() {
const Renderer = rendererFactory();
return class extends Renderer {
constructor(props: any, context: any) {
super(props, context);
}
isValidComponent(obj: any) {
return obj?.prototype?.setState || obj?.prototype instanceof Component;
}
};
}
const RaxRenderer = factory();
const Engine = RaxRenderer;
export {
Engine,
CompFactory,
};
export default RaxRenderer;
多模式渲染
预览模式渲染
预览模式的渲染,主要是通过 Schema、components 即可完成上述的页面渲染能力。
import ReactRenderer from '@alilc/lowcode-react-renderer';
import ReactDOM from 'react-dom';
import { Button } from '@alifd/next';
const schema = {
componentName: 'Page',
props: {},
children: [
{
componentName: 'Button',
props: {
type: 'primary',
style: {
color: '#2077ff'
},
},
children: '确定',
},
],
};
const components = {
Button,
};
ReactDOM.render((
<ReactRenderer
schema={schema}
components={components}
/>
), document.getElementById('root'));
设计模式渲染(Simulator)
设计模式渲染就是将编排生成的《搭建协议》渲染成视图的过程,视图是可以交互的,所以必须要处理好内部数据流、生命周期、事件绑定、国际化等等。也称为画布的渲染,画布是 UI 编排的核心,
它一般融合了页面的渲染以及组件/区块的拖拽、选择、快捷配置。画布的渲染和预览模式的渲染的区别在于,画布的渲染和设计器之间是有交互的。所以在这里我们新增了一层 Simulator 作为设计器和渲染的连接器。Simulator 是将设计器传入的 DocumentModel 和组件/库描述转成相应的 Schema 和 组件类。再调用 Render 层完成渲染。我们这里介绍一下它提供的能力。
整体架构
- Project:位于顶层的 Project,保留了对所有文档模型的引用,用于管理应用级 Schema 的导入与导出。
- Document:文档模型包括 Simulator 与数据模型两部分。Simulator 通过一份 Simulator Host 协议与数据模型层通信,达到画布上的 UI 操作驱动数据模型变化。通过多文档的设计及多Tab 交互方式,能够实现同时设计多个页面,以及在一个浏览器标签里进行搭建与配置应用属性。
- Simulator:模拟器主要承载特定运行时环境的页面渲染及与模型层的通信。
- Node:节点模型是对可视化组件/区块的抽象,保留了组件属性集合 Props 的引用,封装了一系列针对组件的 API,比如修改、编辑、保存、拖拽、复制等。
- Props:描述了当前组件所维系的所有可以「设计」的属性,提供一系列操作、遍历和修改属性的方法。同时保持对单个属性 Prop 的引用。
- Prop:属性模型 Prop 与当前可视化组件/区块的某一具体属性想映射,提供了一系列操作属性变更的 API。
- Settings:SettingField 的集合。
- SettingField:它连接属性设置器 Setter 与属性模型 Prop,它是实现多节点属性批处理的关键。
- 通用交互模型:内置了拖拽、活跃追踪、悬停探测、剪贴板、滚动、快捷键绑定。
模拟器介绍
在画布上也是无法统一的。
运行时环境
从运行时环境来看,目前我们有 React 生态、Rax 生态。而在对外的历程中,我们也会拥有 Vue 生态、Angular 生态等。
布局模式
不同于 C 端营销页的搭建,中后台场景大多是表单、表格,流式布局是主流的选择。对于设计师、产品来说,是需要绝对布局的方式来进行页面研发的。
研发场景
从研发场景来看,低代码搭建不仅有页面编排,还有诸如逻辑编排、业务编排的场景。
基于以上思考,我们通过基于沙箱隔离的模拟器技术来实现了多运行时环境(如 React、Rax、小程序、Vue)、多模式(如流式布局、自由布局)、多场景(如页面编排、关系图编排)的 UI 编排。通过注册不同的运行时环境的渲染模块,能够实现编辑器从 React 页面搭建到 Rax 页面搭建的迁移。通过注册不同的模拟器画布,你可以基于 G6 或者 mxgraph 来做关系图编排。你可以定制一个流式布局的画布,也可以定制一个自由布局的画布。
能力介绍
通信能力
为了设计器和渲染器之间的影响降低到最小,Simulator 和设计器是处于两个 Frame 中来进行样式和代码的隔离。而它们之间的事件通信、方法调用是通过各自的代理对象进行的。
为了实现他们之间的通信,我们在 Simulator 和 设计器中间新增了 host。host 理论上可以访问到设计器中所有模块,但在 Renderer 中只能调用 host 生成的 Schema、组件类获取以及若干配置属性。
存储 Ref
每个组件都会获取其实例的 ref,并通过这个方法存储在 DocumentModel 中。之后设计器可以通过相关方法获取到 Dom 的 x、y、width、height。才能完成 hover 高亮、拖拽等辅助 UI 逻辑。
onCompGetRef={(schema: any, ref: ReactInstance | null) => {
documentInstance.mountInstance(schema.id, ref);
}}
Schema 生成
每个 Node 节点都有 export 方法,该方法可以获取其 Schema。对于顶层节点来说,DocumentModel 上的 export 获取的 Root 的 Schema。
schema={this.document.export('render')}
组件生成
提供 buildComponents、createComponent 等方法,来完成组件的生成,包括第三方组件、低代码组件等。
- 第三方组件
export function buildComponents(libraryMap: LibraryMap,
componentsMap: { [componentName: string]: NpmInfo | ComponentType<any> | Co
mponentSchema },
createComponent: (schema: ComponentSchema) => Component | null) {
const components: any = {};
Object.keys(componentsMap).forEach((componentName) => {
let component = componentsMap[componentName];
if (component && (component as ComponentSchema).componentName === 'Co
mponent') {
components[componentName] = createComponent(component as ComponentSc
hema);
} else if (isReactComponent(component)) {
if (!acceptsRef(component)) {
component = wrapReactClass(component as FunctionComponent);
}
components[componentName] = component;
} else {
component = findComponent(libraryMap, componentName, component);
if (component) {
if (!acceptsRef(component)) {
component = wrapReactClass(component as FunctionComponent);
}
components[componentName] = component;
}
}
});
return components;
}
当 componentName 为 'Component' 时,会调用 createComponent 来生成低代码组件。
- 低代码组件
createComponent(schema: NodeSchema): Component | null {
const _schema: any = {
...compatibleLegaoSchema(schema),
};
_schema.methods = {};
_schema.lifeCycles = {};
if (schema.componentName === 'Component' && (schema as ComponentSchema).
css) {
const doc = window.document;
const s = doc.createElement('style');
s.setAttribute('type', 'text/css');
s.setAttribute('id', `Component-${schema.id || ''}`);
s.appendChild(doc.createTextNode((schema as ComponentSchema).css || ''));
doc.getElementsByTagName('head')[0].appendChild(s);
}
const renderer = this;
class LowCodeComp extends React.Component<any, any> {
render() {
const extraProps = getLowCodeComponentProps(this.props);
return createElement(LowCodeRenderer, {
...extraProps, // 防止覆盖下面内置属性
// 使用 _schema 为了使低代码组件在页面设计中使用变量,同 react 组件使用效果一
致
schema: _schema,
components: renderer.components,
designMode: renderer.designMode,
device: renderer.device,
appHelper: renderer.context,
rendererName: 'LowCodeRenderer',
customCreateElement: (Comp: any, props: any, children: any) => {
const componentMeta = host.currentDocument?.getComponentMeta(Comp.d
isplayName);
if (componentMeta?.isModal) {
return null;
}
const { __id, __designMode, ...viewProps } = props;
// mock _leaf,减少性能开销
const _leaf = {
isEmpty: () => false,
};
viewProps._leaf = _leaf;
return createElement(Comp, viewProps, children);
},
});
}
}
return LowCodeComp;
}
增量更新
由于设计器可以修改组件渲染所需的 props、事件、样式等。每一次渲染相当于整个 React renderer 进行渲染,这样带来了画布卡顿的问题。所以我们需要增量更新来实现按需渲染按需加载。