低代码平台精髓:速搭(soda)低代码平台设计思路

1,611 阅读4分钟

作者基于前面所有的博文,实现了一个低代码平台 POC 版本,本文用于介绍前面没有提到的低代码平台设计部分内容,方便读者进一步了解“速搭”低代码平台。

在前面的 《Typescript 编译原理的应用——一种基于 Typescript 特性的低代码解决方案》 我提出了一种设想,希望通过 typescript 类型系统完成自动生成组件属性编辑器,我基于这一思想,实现了一个全新的低代码平台的 POC 版本,本文用于介绍该低代码平台的设计理念。

github 地址:github.com/linc2023/so…

目前版本预览地址:linc2023.github.io

Schema 设计

这部分参考了 lowcode-engine 的 《低代码引擎搭建协议规范》,如下:

{
    pageId:"sadddda2",
    components: [
      { package: "@soda/base", version: "1.0.0", componentName: "A" },
      { package: "@soda/base", version: "1.0.0", componentName: "Button" },
      { package: "@soda/base", version: "1.0.0", componentName: "Span" },
      { package: "@soda/base", version: "1.0.0", componentName: "EventTest" },
      { package: "@soda/base", version: "1.0.0", componentName: "MixinTest" },
    ],
    dataSources: [
      {
        datsSourceId:"xx1",
        options:{
          url:"/api/aa",
          method:"xxxx",
          params: {
            body: { paging: { currentPage: 1, pageSize: 10 } }
          }
        },
        beforeRequest:"function (options) {options.headers['content-type']='application/json'}",
        afterReqtes:"function (res) {return {data:res.list,total:res.count}}",
        onError:"function (error) {console.log(error)}"
      }
    ],
    functions: {
      {name:"xxx",body:"function xxx(a){console.log(a)}"}
    },
    vars:[
      {name:"userInfo",scope:"app",defaultValue:"{}"},
      {name:"currentPage",scope:"page",defaultValue:"{}"}
    ],
    assets:[
        {type:"script",url:"xxxx.js"},
        {type:"style",url:"xxxx.css"},
    ]
    css:"",
    methods:{
      "refreshMainTable":"function refreshMainTable(result) {  this.$(\"table_oclvkdok6o1\").refresh()}"
    },
    componentsTree: [
      {
        componentName: "Page",
        id: "aqasiz7lkk7a3dy222z1",
        children: [
          { componentName: "A", id: "aqasiz7lkk7a3dyz1", props: {} },
          { componentName: "EventTest", id: "aqasiz7lkk7a3dyz12", props: {} },
          { componentName: "Button", id: "kje68elzgza63nve2", props: {} },
          { componentName: "A", id: "aqasiz7lkk7a3dyz", props: {} },
          { componentName: "Span", id: "kje68elzgza63nve", props: {} },
          { componentName: "MixinTest", id: "kje68elzgza63nv11e", props: {} },
        ],
      },
    ],
    i18n:{}
  }

针对 lowcode-engine 的页面 schema ,soda 做了一些优化:

  • 删除 schema 中所有组件 props 的默认值,组件打包产物中有,没必要保留;
  • 删除originCode, 这个字段太长,运行态不需要,设计态通过 pageId 获取;
  • 删除 methods 部分属性,它的内容都是函数,没有必要加 typesource;
  • 新增 functions 用于保存用户拖拽生成的 js 函数,只记录名称、函数体,设计器、测试态通过 pageId + 函数名称查询节点信息;
  • 新增 vars,用户记录用户在界面新增的变量,包括应用级、页面级;
  • 删除 utils,添加 assets,用于引入第三方 js、css;

schema 渲染

参考之前的博文:低代码平台精髓:常见功能及实现思路

页面设计

项目采用左中右布局,结构如下:

截屏2024-09-05 11.31.05.png

项目基于插件机制,用户可以定制一切功能,用户开发的组件库可以包括 UI扩展事件扩展DesignTool组件Designer属性编辑器

UIPlugin

整个页面都是 UIPlugin 组成(图中左侧有页面管理、组件组件树、方法等,右侧有属性编辑器,中间部分是设计器)

使用如下:

import { UIPlugin } from "@soda/designer";
export class PageManagerPlugin extends UIPlugin {
  static placement: UIPluginPlacement = "left";
  static priority: number = 1;
  render() {
    return <div >页面1</div>;
  }
}
// 使用
 globalState.plugin.register(PageManagerPlugin, "PageManagerPlugin");

事件扩展

事件部分,它是一个发布订阅模式,用于组件之间通信,还能覆盖内置事件(比如:修改内置的保存、组件改变,用于实现保存到数据库、记录历史记录等),用户可以基于 LogicPlugin 影响低代码内核,它可以是一个函数,也可以是一个 LogicPlugin

Event.$on("designNode:propsChange", () => {
  console.log(111)
});

也可以覆盖内置事件,比如:

Event.$on("designNode:propsChange", {
    uniqueName: "rerenderEditor",
    exec ()=>{}
},{ allowOverride: true });

DesignTool

设计器扩展,针对中间设计器部分,用户可以基于 DesignTool 实现画布没有的功能,比如缩放、右击菜单、框选、画笔等。

import { DesignTool } from "@soda/designer";
export class ContextMenuDesignTool extends DesignTool {
  onContextMenu(event){
      console.log(this.designNode)
  }
}
// 使用和 UIPlugin 类似

组件

基于 《Typescript 编译原理的应用——一种基于 Typescript 特性的低代码解决方案》 解析出所有信息。

import { BaseComponent, reactive } from "@soda/core";

class B extends BaseComponent {
  /**
   * @label 属性/字符串1
   */
  @reactive str = "初始值";
  render() {
    return <span>{this.str}</span>;
  }
}
/**
 * 表示一个A
 * @label 继承测试
 * @icon ./button.svg
 * @hidden false
 */
export class A extends B {
  /**
   * @label 属性/字符串
   */
  override str = "初始值";
  /**
   *  @label 样式/str2
   */
  @reactive str2 = "xx";
  render() {
    return <span>继承测试:{this.str + "___" + this.str2}</span>;
  }
}

Designer

设计器扩展,用户可以在实现组件时,基于这一扩展截获设计器事件,比如新增组件、拖动组件、删除组件等。

import {ComponentDesigner} from "@soda/designer"
// Text 组件不允许被删除
export class TextComponentDesigner extends ComponentDesigner {
    override beforeNodeRemove() { return false }
}

在组件上添加 @editor 使用:

import { BaseComponent, reactive } from "@soda/core";
/**
 * @label 文本
 * @editor ContainerComponentDesigner
 */
export class Text extends BaseComponent {
  /**
   * @label 属性/字符串1
   */
  @reactive str = "初始值";
  render() {
    return <span>{this.str}</span>;
  }
}

属性编辑器

属性编辑器就是右侧的属性编辑框,平台内置了一些属性编辑器(比如:Input、InputNumber、Switch、CodeEditor 等),如果无法满足用户需求,有两种方式扩展:

PropertyEditor

import { PropertyEditor } from "@soda/core";
import { InputNumber } from "@soda/common";

export class NumberPropertyEditor extends PropertyEditor {
  static name = "数字输入";
  render() {
    return <InputNumber {...this.props}></InputNumber>;
  }
}

使用:

import { BaseComponent, reactive } from "@soda/core";
/**
 * @label 测试
 */
export class Text extends BaseComponent {
  /**
   * @label 属性/数字
   * @editor NumberTypeEditor
   */
  @reactive number = 1111;
  render() {
    return <span>{this.number}</span>;
  }
}

TypeEditor

import { TypeEditor } from "@soda/core";
import { Input } from "@soda/common";
export class RefTypeEditor extends TypeEditor {
  static name = "数字输入";
  render() {
    return <Input {...this.props}></InputNumber>;
  }
}

新增一个 ts 类型:

/**
 * @editor RefTypeEditor
 */
export type Ref = string

使用: 使用:

import { BaseComponent, reactive } from "@soda/core";
/**
 * @label 按钮
 */
export class Button extends BaseComponent {
  /**
   * @label 属性/ref
   */
  @reactive ref: Ref;
  render() {
    return <button>按钮</button>;
  }
}

脚手架设计

脚手架最起码需要提供打包、预览两项功能。

build

打包需要将用户将用户编写的产物编译成 js,同时解析组件库元数据信息、组件属性元数据。

组件库元数据

组件库元数据用于加载左侧组件面板定位组件,它需要记录组件库名称(name)、window下挂载名称(library)、中文名称(displayName)、版本号(version)、组件元数据信息(components),组件元数据需要描述、图标、序号、组件名称、中文名称等。

{
   "name": "@soda/base",
  "library": "sodaBase1",
  "displayName": "基础组件",
  "version": "1.0.0",
  "components": [
    {
      "description": "表示一个按钮",
      "icon": "base64/图片 URL",
      "order": "12",
      "componentName": "Button",
      "displayName": "所有类型测试"
    }
    ]
}

组件属性元数据

组件属性元数据包括属性编辑器类型、名称、中文名称、所在 tab、所在分组等,如下:

{
  // 组件名称
  "MixinTest": [
    {
      // 属性编辑器信息
      "editorsProps": [
        // 类型为 3,props 为 {clearable:true}
        {"type": 3, props:{clearable:true}}
      ],
      // 属性名称 a
      "name": "a",
      // 展示名称是 A
      "label": "A",
      // 在 "属性" tab 下
      "tab": "属性",
      // 在 “基础属性” 分组下
      "group":"基础属性"
    }
  ]
}

如何打包

调用 vite.build(),合并本地 vite.config.ts 或者 vite.config.js,解析 package.json 中的 name、library、noMeta 等字段,然后选择打包策略,在 generateBundle 阶段,使用 typescript compiler api 解析 ts 文件,生成 component.jsonprop-mtea.json

具体参见:@soda/cli scripts 中的 build.ts。

思路参考:《Typescript 编译原理的应用——一种基于 Typescript 特性的低代码解决方案》

dev

除了打包,还需要提供 devServer,发布用户开发、测试。devServer 这样编写就行了:

await vite.createServer();
await server.listen();
server.printUrls();

为了不改变 现有逻辑,需要在运行之前调用“前面提到的build”编译组件库,打包到一个特定的位置,系统选用 ./node_modules/.soda-cli 目录,首先它会被 git 忽略掉,另外路径也比较简单。

启动之后还需要在页面追加一段 js ,使得项目能够引用打包产物,同时调用 designer 注册当前组件库。