10+核心功能点!低代码平台实现不完全指南 🧭(上)

一、前言

低代码平台通过可视化编排、简单配置以及少量的代码,实现快速开发和交付应用程序,受到了广大企业和开发者的青睐。当前,低代码平台已经演进出了图元编排、流程编排、页面编排和数据编排等多种形式,广泛应用于不同领域和行业,例如低码页面搭建、低码数据研发、多维表格、低码 AI 应用开发和业务流程自动化等。页面搭建是低代码领域最为常见的应用场景,本文也将以该场景为切入点,深入探讨如何通过低代码的方式实现页面的编排构建,包括组件拖拽、画布布局、组件渲染、组件联动、脚本执行、物料加载等核心功能。如需了解更多低码建站内容,可参考业界优秀产品 Retool(低码产品1️⃣哥)、Appsmith(开源)、ToolJet(开源)、Lowcode Engine(阿里,React 技术栈)、TinyEngine(华为,Vue技术栈)、tmagic-editor(腾讯)等。

  • 上篇:组件物料、组件拖拽、组件布局、组件渲染、组件加载、属性设置
  • 下篇:脚本执行、组件联动、沙箱隔离、辅助操作、撤销重做、多页路由

二、整体概述

以 Lowcode Engine 为例,低代码平台中用户进行页面搭建的核心交互界面主要分为 4 个区域:

  • 画布区域:采用所见即所得的设计理念,对组件可进行编排组合,并且实时渲染;
  • 物料区域:组件资源集中管理区,用户可以选择合适的组件并拖拽到画布中使用;
  • 属性区域:在属性区域用户能够便捷地调整组件属性,还能设置组件的各种交互行为;
  • 工具栏:提供全局操作和辅助功能,例如撤销/重做、保存/发布、尺寸切换、预览等;

操作流程如下:用户在低代码平台完成组件拖拽、布局编排与属性配置,整个过程如同搭建积木------当设计完成后,系统自动生成满足协议规范的 JSON Schema(领域特定语言DSL) ,此即低代码平台的最终输出产物之一。页面渲染器则通过解析该 JSON Schema 进行页面的渲染,精准还原用户在编辑器中配置的页面结构与交互逻辑,实现设计态与运行态的一致。注意,页面渲染器也是可视化编辑器中画布区域的核心内容,用于内容的渲染。

⚠️ 低代码平台的另一种产物形态是出码,即 JSON Schema 转化为可二次开发的源代码,但二次开发之后是不可逆的,本文暂不讨论该场景。

三、功能实现

代码平台要想通过拖拽+配置的方式实现页面的搭建和渲染在实现过程中其实涉及非常多的功能点,上篇我们将重点探索其中的核心功能点:组件物料、组件拖拽、组件布局、组件渲染、组件加载和属性设置等,剖析其设计与实现。在功能实现之前,首先需要确定的就是协议规范,也就是 JSON Schema 的格式。协议规范是低代码平台的基石,贯穿整个低代码的生命周期,离开协议规范所有的功能实现都是白扯,如果协议设计不好,想要扩展低代码平台是很困难的,甚至需要推翻重来。参考 Lowcode Engine 的协议:

  1. 《低代码引擎搭建协议规范》:定义通过用户拖拉拽搭建产出的协议,用于页面渲染
  2. 《低代码引擎物料协议规范》:规范低代码引擎渲染物料组件内容及其属性配置的标准
  3. 《低代码引擎资产包协议规范》:定义资产包的标准,用于多个物料的统一管理和消费

3.1 组件物料

组件物料是低代码平台的灵魂,一般低代码平台的左侧会有一个物料区域,列举了所有可供使用的组件物料,我们可以将其拖动到画布区域。我们会有一个componentList包含了所有物料的描述信息,然后将其循环渲染在物料区域即可,当然也可以对物料进一步进行类别划分。当然,我们还会有一个componentMap,我们能基于此快速通过组件名获取到组件的描述信息了。

typescript 复制代码
interface IComponent {
  // 组件名称
  componentName: string;
  // 组件中文名
  title: string;
  // 组件的小图标
  icon: string;
  // 组件 npm 源
  npm: {
    // 组件库名
    package: string;
    // 版本号
    version: string;
    // 组件导出的名称
    exportName: string;
  };
  // 组件属性信息 props,通常包含名称、类型、描述、默认值
  props: {}[];
  // 增强信息
  configure: {
    // 属性面板配置
    props: {}[];
  };
}

const componentList: IComponent[] = [];
const componentMap: { [key: string]: IComponent } = {};

3.2 组件拖拽

低代码平台编排最为重要的交互方式就是拖拽组件,将我们需要的物料拖拽到画布中期望的位置进行布局,那么组件拖拽是如何实现的呢?主要可以参考以下几种方法:

  1. 原生拖拽 API:
    • 拖拽源(被拖拽元素)事件
      • dragstart(开始拖拽)
      • drag(拖拽过程中)
      • dragend(拖拽结束)
    • 放置目标(接收拖拽元素)事件
      • dragenter(进入目标区域)
      • dragover(在目标区域内移动)
      • dragleave(离开目标区域)
      • drop(放置操作)
  2. 拖拽库:
  3. 拖拽引擎:Lowcode Engine 在浏览器原生事件之上自建了一套拖拽引擎 Dragon,参考源代码,原因是画布渲染区被包在一个 iframe 内,Dragon 需要进行坐标系转换从而支持跨 iframe 拖拽。Dragon 通过捕获并标准化原生鼠标/触摸事件,然后通过「坐标投影 + 协议数据传递」把拖拽意图翻译成「在某某父节点、第几个子节点之前插入」的指令。需要注意的是:拖拽过程中传递的是描述组件的 JSON 数据,而非实际的 DOM 节点。

3.3 组件布局

组件布局本质上是将组件以特定的空间位置、层级关系和交互顺序进行布局与组合,布局方式将直接影响页面的构建模式和最终呈现效果,当下主流的低代码生态,画布组件的编排形态已呈多元演进之势。

当前最主流的布局方式是容器画布,布局基于一个个容器组件,容器可以使用Block 布局、Flex 布局和 Grid 布局等形式。开发者只需把其他任意组件拖入这些容器组件内,即可借助容器自带的布局规则,自动完成排版、对齐与响应式适配。 一个页面通常包含了多种不同类型的内容和功能模块,需要采用不同的布局方式来满足各自的需求,这种方式能够充分发挥各种布局方式的优势,灵活地应对复杂的页面布局需求,实现更精细、更合理的布局效果。

另一种方式是栅格画布, 例如在 Retool 的页面搭建中,基于 Grid 布局将整个画布巧妙地划分为一个灵活的网格体系,其中每一行和每一列都具有明确的尺寸和间距定义,每一个组件都被赋予了特定的行列空间,以及精准的行列位置坐标。而在渲染过程中,Retool 会根据容器的宽度和高度,结合预先定义好的行列划分规则,动态计算出每个组件的宽度和高度,并将其设置为固定的值,同时基于绝对布局将组件绘制到页面的精确位置。通过这种方式,每个组件都被精确地放置在其对应的行列位置上,从而实现了页面布局的高效构建和精准呈现。

还有一种绝对定位自由画布,一般基于 Canvas 实现,这是一种完全自由的布局,允许使用者将组件在坐标系中任意拖拽、缩放、旋转,配合辅助线、吸附、自动分布等智能提示,一般应用于设计工具或者高度自定义的页面,例如 Figma 的画布就是一个 Canvas 元素。

3.4 组件渲染

那么拖拽到画布上的组件是怎么渲染出来的呢?组件在 JSON Schema 中可以保存在componentTree中,这是一个树状的数据结构,代表了组件之间的层级和位置顺序关系。

javascript 复制代码
const componentTree = [
  {
    componentName: "Page",
    id: "page1",
    props: {},
    children: [
      { componentName: "text", id: "text1", props: {} },
      { componentName: "button", id: "button1", props: {} },
      {
        componentName: "block",
        id: "block1",
        props: {},
        children: [{ componentName: "text", id: "text2", props: {} }],
      },
    ],
  },
];

组件渲染可以以 Vue / React 等现代框架为渲染引擎,将这份结构化描述映射为最终可见的 DOM。我们可以将这个componentTree结合组件一起传入到渲染器中,递归渲染即可,以 Vue/React 为例:

xml 复制代码
<template>
  <component :is="componentTree.componentName" v-bind="componentTree.props">
    <template v-for="(item, index) in config.children" :key="index">
      <!-- 递归自调用 -->
      <RenderComponent :componentTree="item" />
    </template>
  </component>
</template>

<script>
// 注意,使用到的组件可以提前通过全局注册好,这样is直接传组件名称即可,也可以直接传入具体内容
export default {
  name: "RenderComponent",
  props: {
    componentTree: {
      type: Object,
      default: () => ({}),
    },
  },
};
</script>
javascript 复制代码
// 可以将使用到的组件渲染函数获取后记录在全局,直接根据组件名获取
const componentRenderList = {
  button: Button,
  text: Text,
}

function RenderComponent(props: { componentTree: any }) {
  const { componentName, children, props } = componentTree;
  const Component = componentRenderList[componentName];
  return (
    <Component {...props}>
      {children && children.map((child, index) => <RenderComponent key={index} componentTree={child} />)}
    </Component>
  );
}

export default RenderComponent;

注意,这个componentTree很重要哦,接下去我们所有在低代码平台上的配置操作其实本质上都是在修改这一份componentTree数据。

一般情况下,画布渲染是独立的 iframe 环境,但是同源环境可以与父页面通信

  1. 父页面访问 iframe:iframeDom.contentWindow
  2. iframe访问父页面:window.parent 或 window.top

3.5 组件加载

方式一:import 加载

javascript 复制代码
const name = 'Button' // 组件名称
const component = await import('https://xxx.xxx/bundle.js')
Vue.component(name, component)

方式二:script 脚本加载

javascript 复制代码
function loadjs(url) {
    return new Promise((resolve, reject) => {
        const script = document.createElement('script')
        script.src = url
        script.onload = resolve
        script.onerror = reject
    })
}
const name = 'Button' // 组件名称
await loadjs('https://xxx.xxx/bundle.js')
// 这种方式加载组件,一般直接将组件挂载在全局变量 window 下,所以 window[name] 取值后就是组件
Vue.component(name, window[name])

当然对于 Vue/React 框架来说,都有动态引入组件的机制,示例如下:

javascript 复制代码
const AsyncComponent = defineAsyncComponent(() =>  {
  return new Promise((resolve) => {
    const script = document.createElement("script");
    script.src = "https://your-cdn.com/component.js";
    script.onload = () => {
      resolve(window.RemoteComponent);
    };
    document.head.appendChild(script);
  })
})

const AsyncComponent = React.lazy(() => 
  import('https://your-cdn.com/component.js')
    .then(module => ({ default: window.RemoteComponent }))
);

上述代码示例均仅供参考,实际使用需要考虑组件打包产物是如何使用,即导入后如何获取对应的组件内容。

3.6 属性设置

在低代码平台中,"属性/事件设置"是最频繁的操作之一:

  • 用户选中画布中的某个组件;
  • 平台在右侧(或底部)的属性区动态渲染出该组件可配置的属性、事件、样式等;
  • 用户的每一次修改都会实时同步到 JSON Schema 中;
  • 画布中的组件立即拿到新的 props 重新渲染。

那么,在属性区域我们可以配置什么属性,属性配置有通过什么样的设置器进行设置,以及属性的默认值填写什么,这一些内容我们都可以在组件的物料协议中进行约定,可参考《低代码引擎物料协议规范》,例如:

javascript 复制代码
export default {
  componentName: 'Button',
  title: '按钮',
  // 1. props 协议
  props: [
    {
      name: 'text',
      title: '按钮文案',
      defaultValue: '按钮',
      setter: 'InputSetter',
    },
    {
      name: 'type',
      title: '按钮类型',
      defaultValue: 'primary',
      setter: {
        componentName: 'SelectSetter',
        props: {
          options: [
            { label: '主要', value: 'primary' },
            { label: '次要', value: 'secondary' },
          ],
        },
      },
    },
    {
      name: 'disabled',
      title: '是否禁用',
      defaultValue: false,
      setter: 'SwitchSetter',
    },
  ],
  // 2. 事件协议
  events: [
    {
      name: 'onClick',
      title: '点击事件',
      setter: 'EventSetter',
    },
  ],
};

属性面板本质上是一个巨大的 Form,将 setters 遍历渲染,然后当我们修改属性值的时候会同步修改 JSON Schema 中 componentTree 的内容,使用单向数据流的方式,组件会根据传入的 props 重新渲染。

四、总结

低代码平台通过将可视化设计与底层技术实现分离,极大地简化了应用开发流程。本文详细剖析了低代码平台的12个核心功能模块,揭示了平台背后的技术原理,本文分为上下两篇,当前为上篇,其余内容请阅读下篇。从实现来说,低代码就是有一个大型组件可以传入 props 来渲染,这个大型组件就是渲染器,低代码平台就是生产这个满足协议的 props 的,整体实现思路都可以从这个角度去思考。

参考资料

相关推荐
张元清几秒前
Neant:0心智负担的React状态管理库
前端·javascript·面试
阳树阳树1 分钟前
小程序蓝牙API能力探索 1——蓝牙协议发展历史
前端
阿华的代码王国3 分钟前
【Android】PopupWindow实现长按菜单
android·xml·java·前端·后端
ygming12 分钟前
Q51- code295- 数据流的中位数 + Q52- code767- 重构字符串
前端
袋鱼不重14 分钟前
手把手搭建Vue轮子从0到1:4. Reactivity 模块的实现
前端·vue.js·源码
!win !14 分钟前
免费的个人网站托管-GitHub Pages篇
前端·开发工具
xw516 分钟前
免费的个人网站托管-GitHub Pages篇
前端·github
阿星AI工作室17 分钟前
扣子可以发布到小米搞钱了!手把手教程来了丨coze开发者瓜分亿级流量池指南
前端·人工智能·后端
小华同学ai17 分钟前
GitHub 开源爆款工具|MediaCrawler:程序员零门槛采集抖音/小红书/B站等社交评论,30K star 背后的场景实战揭秘!
前端·后端·github
盏灯17 分钟前
🔥🔥🔥websocket 前后端通信,接受命令,并执行
前端·后端·websocket