通信是一个应用中不可或缺的一个功能,现如今前端视图类框架大多数都是由数据驱动,通过数据来进行视图层的展示渲染。举个简单的例子如下,这是一个常见的 React 列表渲染:
// each
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li>{number}</li>
);
// render
<ul>{ listItems }</ul>
在实际开发场景下,numbers 的值往往是动态变化的,通过 setState 可以进行数据的更新从而带动页面视图的渲染,以此来达到数据驱动的结果。
一款通用的低代码成品也一定会具备不同的物料组件交互和联动以及数据共享、函数执行等功能。
因此组件通信是实现低代码平台模块化、提高扩展性和任务连接的关键手段。
状态管理设计
前端应用程序的数据状态通常包括用户输入、服务端返回的数据、组件之间的数据传递等,这些数据状态的变化会影响到前端应用程序的展示和交互。 在实际应用中,大多数的状态(State)都是以下几种形式存在的:
- 局部状态管理: 将状态存储在组件的本地状态中,没有通过实例或者是属性向外传递,这种状态适用于简单的区块组件,其特点就是完全内部独立,无需进行共享;
- 全局状态管理:将状态存储在一个上层容器中,通过向下注入的方式来统一收集状态管理流,统一管理状态的变化的事件的触发,只需要在应用程序中用到的地方进行订阅即可获取最新的状态内容。通常在一些大型应用和复杂的区块模板中会经常使用到。
在 React 中,实现状态管理的方案有很多,其中比较知名的就是 Mobx 与 Redux,这是早期流行的解决方案,分别是单向数据流与双向数据流的典型代表作品,除此之外,也有 recoil、zustand、xState 等方案都可以实现 React 的状态管理。
在低代码平台中,后续实现也会基于 Redux 来实现组件通信的状态管理,选择 Redux 的原因有几下几个方面的考量:
- 使用足够简单,应用程序的全局状态都在存储区内的对象树里面,通过派发事件可以来进行对应的状态修改;
- 扁平化状态管理,可以自顶向下的管理自身的状态引用,能够将应用复杂的状态变得可预测和可维护;
- 与框架无关,Redux 本身就是一种状态容器,可以在不同的 JS 环境下运行;
- 使用 Redux 的中间件可以让我们在核心层中对状态做一些魔法改造,比如日志、数据处理、错误兜底等一些通用类的处理;
- 调试,借用 redux-tool 可以在开发时更好地理解应用程序中发生的事件和状态的变化,增强错误排查的能力和开发调试的体验。
以上都是一些使用 Redux 的原因,在低代码平台中也不需要开发者来维护一个非常规范的 Store 目录,减少了很多使用 Redux 时代码组织的烦恼。
数据流
数据流是指在应用程序中数据传递的方向和路径。在前端开发中,数据流通常是单向的。
如果在页面的 Text 组件中,我需要来获取一个动态的数据变量(root)。此时,会进行一次向上路径树的寻址最终来获取到相关的变量。也可以通过具体的 NamePath 获取对应的状态数据,最终来完成页面的显示。
如下图所示,是前期对平台一个基本数据链空间的状态流转:
但在我们的产品设计中,最终还是将状态链的方案给摒弃掉了,而是使用了最基础的 Redux 数据流模式来进行工作,通过一个中间件来完成整个状态的调度。这么做的原因是大部分框架都是由数据来驱动的,对于低代码平台的数据流而言,应尽可能的简单,便于预测和集中化统一调度。
如下图,是一个 Redux 的基本数据流的过程,在平台中,页面组件属性都可以绑定一个 store 层中声明的状态,一旦绑定后,这个属性将会成为一个受控内容,组件的操作往往会引起应用程序状态的改变,而状态的改变又会反过来影响组件的状态和行为。
这就是组件通信数据流的概念与作用。
组件联动与操作
组件操作是指页面中组件的一些行为和交互,通过这些操作来实现页面的功能。
在设计数据流的时候也提到了受控属性会因为状态发生改变而改变。也就是说一个组件的状态会随着另外一个组件或者本身的操作而发生变化,在组件操作的设计中,大体上分为以下几种:
- 事件绑定:通过绑定事件处理函数来响应用户操作,例如点击、鼠标悬停、拖拽等。事件绑定通常通过 React 的事件系统或其他库来实现,它们可以使得页面变得更加动态和交互性;
- 表达式: 通过表达式来计算和展示页面的内容和样式。表达式可以是简单的变量、函数调用或复杂的逻辑运算,它们可以使得页面的内容更加灵活和可定制。除此之外,也有类似于 amis 的后端公式法则,也属于是表达式相关的一种;
- 循环遍历&条件判断:循环遍历和条件判断可以使得页面的结构和内容根据数据来进行相应的显示,往往会随着状态数据改变而重新进行视图的渲染,展示动态变化的页面;
- 属性绑定:将组件属性变更为可控属性,从而来实现状态与内容的一致和灵活,也是日常开发中最常见的功能之一。
基于设计好的事件流,后续组件的联动就无需将组件与组件挂钩了,而是与状态关系之间相关联,也就是常见的 UI(State)解法。
如下图所示:在逻辑块中创建了onBtnClick
保存后,此时就创建了一个可执行的函数,在函数内部操作了 Redux 的 Dispatch
来派发 UPDATE Reducer
更新内容。
保存函数后,在对应的属性面板点击事件绑定将其进行关联。
需要注意的是,所有的表达式在存储的时候都会会进行一个类型的标记。比如上述事例中的 onBtnClick
,在协议中会进行如下转换一个简单的数据结构进行存储,参考如下代码:
onBtnClick: {
type: 'Function',
source: "onBtnClick = () => {\n __store__.dispatch({\n type: 'UPDATE',\n payload: {\n data: __props__.value + 1\n }\n })\n}"
}
那么这串协议怎么用呢?
那就需要使用 BrowserCodeExecVM 来进行处理了。如下所示:
BrowserCodeExecVM 是一个代码执行的虚拟容器,它能够动态的解析代码字符串并解析,当绑定事件执行的时候,会被 BrowserCodeExecVM 进行执行,然后将结果返回给触发器的源头,以此来实现一个中继闭环。
在本章节中,主要是对其有一个基本的认识了解,在后续动态逻辑执行实现中,会详细的来刨析 BrowserCodeExecVM 的设计与实现过程。
AST与未来
协议是整个低代码平台的基石,随着对低代码平台的要求越来越高的情况下,那么承载与底层的协议必然会随之迭代的非常复杂。如何使用协议来描述一个组件许,其最终形态能够满足用户将状态、PRops、表达式等大部分功能可以像 React 一样快速的进行开发。
基于此,有部分的同学脑海中立马就浮现了 AST 的身影,基于 AST 方案的抽象协议可以很好的满足用户对大部分状态和组件的交互操作。就目前而言,AST 是代码最真实抽象的存在,但是同样的本身结构也非常复杂,相比于搭建平台抽象的协议而言,两者的等级相差还是非常大的。
在社区中看到一片文章感觉非常有意思,如下代码所示:
{
type: 'Component',
name: 'Test',
props: [
{
type: 'ComponentProp',
name: 'initalValue',
init: { type: 'Literal', value: 0 },
},
],
state: [
{
type: 'Val',
name: 'counter',
init: { type: 'Identifier', name: 'initialValue' },
},
],
}
const Test = ({ initialValue = 0 }) => {
const [counter, setCounter] = useState(initialValue);
// ....其他
}
这样的设计是 reka.js 的解法,基于 AST 来为搭建页面构建复杂的状态管理。用户可以无负担的将自己搭建的应用呈现在用户面前,这一切的背后工作都是解释器在完成。解释器会根据协议处理好属性,状态,逻辑表达式等一系列的问题。
但是就目前而言,AST 的方案会更加的完善和智能,同样的解析成本和对编辑器的工作要求也是相当之高的。所以说是未来解决方案的一种考量。
总结
在本章节,主要的内容是来设计一个低代码平台的通用化状态管理的过程方案以及组件联动的过程,在接下来的实战当中,会主要使用 Redux 来完成低代码平台状态的管理和事件派发相关。
在未来不久,通过动态逻辑执行与在线代码执行 Vm 的帮助,一举来打通整个事件 + 状态 = 视图变化的一个完整体系,真正意义上做到低代码级别的搭建效果。
资源推荐
medium.com/dailyjs/whe… github.com/prevwong/re… juejin.cn/post/720463… zhuanlan.zhihu.com/p/373545726
写在最后
如果你有什么疑问或者更好的建议,欢迎在评论区提出。 👏
10 架构:组件通信设计