作者基于前面所有的博文,实现了一个低代码平台 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
部分属性,它的内容都是函数,没有必要加type
、source
; - 新增
functions
用于保存用户拖拽生成的 js 函数,只记录名称、函数体,设计器、测试态通过 pageId + 函数名称查询节点信息; - 新增
vars
,用户记录用户在界面新增的变量,包括应用级、页面级; - 删除
utils
,添加 assets,用于引入第三方 js、css;
schema 渲染
参考之前的博文:低代码平台精髓:常见功能及实现思路
页面设计
项目采用左中右布局,结构如下:
项目基于插件机制,用户可以定制一切功能,用户开发的组件库可以包括 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.json
、prop-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 注册当前组件库。