低代码开发平台(LCDP)是无需编码(0代码)或通过少量代码就可以快速生成应用程序的开发平台。让具有不同经验水平的开发人员可以通过图形化的用户界面,通过拖拽组件和模型驱动的逻辑来创建网页和移动应用程序。

  低代码的核心是呈现、交互和扩展,其中呈现和交互需要借助自行研发的渲染引擎实现。而此处的扩展特指物料库,也就是各类自定义的业务组件,有了物料库后才能满足更多的场景。

  在 4 个月前研发过一套可视化搭建系统,当时采用的是生成代码的方式渲染页面。而本次研发采用的则是运行时渲染,功能比较基础,基于React开发,代码量在 3000 多行左右,用户群是本组团队成员,目标是:

  1. 满足 80% 的后台需求,高效赋能解放生产力。
  2. 抽象共性,标准化流程,提升代码维护性。
  3. 减少项目代码量,加快构建速度。

  平台的操作界面如下,由于管理后台页面的元素比较单一,所以暂不支持拖拽和缩放等功能,也就是没有通用的布局器。

  

  组件区域可以选择内置的通用模板组件,点击添加可在预览区域显示对应的组件,位置可上下调整,并且可以像真实的页面那样进行动态交互。配置区域可填写菜单名称、权限、路由等信息,点击更新文件后,会将数据存储到 MongoDB 中。

一、渲染引擎

  在数据库中保存的组件是一套 JSON 格式的 Schema(页面的描述性数据),将 Schema 读取出来后,经过渲染引擎解析后,得到对应的组件,最后在页面中显示。

1)Schema

  下面的 Schema 描述的是一个提示组件,参数的值是字符串和布尔值。为了能让组件满足更多的场景,有时候,组件的参数值可以是字符串类型的 JSX 代码或回调函数,例如下面的 description 属性,那这些就需要做特殊处理了。

{
props: {
message: "123",
description: "<p>456</p>",
showIcon: true
},
name: "Prompt"
}

  点击 Schema 按钮,可实时查看当前的 Schema 结构,这些 Schema 最终也会存储到 MongoDB 中。

  

2)参数解析

  从组件区域得到的参数都是字符串类型,此时需要做一次适当的类型转换,变成数组、函数等。eval() 比较适合做这个活,它会将字符串当做 JavaScript 代码进行执行,执行后就能得到各种类型的值。

  在下面的遍历中,先对数组做特殊处理,然后再判断字符串是否是对象或数组,最后在运行 eval()函数时,要加 try-catch,捕获异常,因为字符串中有可能包含各种语法错误。

for (const key in values) {
// 未定义的值不做处理
if (values[key] === undefined) continue;
// 对数组做特殊处理
if (Array.isArray(values[key])) {
// 将数组的空元素过滤掉
values[key] = removeEmptyInArray(values[key]);
newValues[key] = values[key];
continue;
}
const originValue = values[key];
let value = originValue;
// 判断是对象或数组
const len = originValue.length;
if (
(originValue[0] === "{" && originValue[len - 1] === "}") ||
(originValue[0] === "[" && originValue[len - 1] === "]")
) {
try {
/**
* 字符串转换成对象
* 若 values[key] 是数组,会有BUG
* eval(`(${[1,2]})`)的值为 2,因为数组会先调用toString(),得到 eval("(1,2)")
*/
value = eval(`(${originValue})`);
} catch (e) {
// eval(`test`)字符串也会报test未定义的错误
value = originValue;
}
}
newValues[key] = value;
}

  在将参数转换类型后,接下来渲染引擎就会根据不同的组件对这些参数进行定制处理,例如将提示组件的 description 属性转换成 JSX 语法的代码。parse()是一个解析函数,来自于 html-react-parser 库,可将组件转换成 React.createElement() 的形式。回调函数的处理会在后面做详细的讲解。

{
handleProps: (values: ObjectType) => {
// 将字符串转换成JSX
if (values.description) {
values.description = parse(values.description.toString());
}
return values;
};
}

3)回调函数

  除了 JSX 之外,为了能适应更多的业务场景,提供了自定义的回调函数。

{
props: {
btns: `onClick: function(dispatch) {
dispatch({
type: "template/showCreate",
payload: {
modalName: 'add'
}
});`
},
name: "Btns"
}

  编辑器组件使用的是 react-monaco-editor,即 React 版本的 Monaco Editor

  

  编辑器默认是不支持放大的,这是自己加的一个功能。点击放大按钮后,修改编辑器父级的样式,如下所示,全屏状态能更直观的修改代码。

.fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10000;
}

  函数默认是字符串,需要进行一次转换,采用的是 new Function(),这种方式可以将参数传递进来。eval() 虽然也能执行字符串代码,但是它不能传递上下文或参数。

const stringToFunction = (func:string) => {
const editorWarpper = new Function(`return ${func}`);
return editorWarpper();
};

  本来是想在编辑器中沿用 TypeScript 语法,但是在代码中没有编译成功,会报错。

4)组件映射

  一开始是想在编辑器中直接输入 JSX 代码,然后通过 Babel 转译,但在代码中引入 Babel 后也是出现了一系列的错误,只得作罢。

  之前的 parse() 函数可将字符串转换成组件,但是在实际开发,需要添加各种类型的属性,还有各类事件,全部揉成字符串并不直观,并且 antd 组件不能直接通过 parse() 解析得到。所以仍然是书写一定规则的 Schema(如下所示),再转换成对应的组件。

{
name: "antd.TextArea",
props: {
width: 200
},
events: {
onChange: function (dispatch, e) {
const str = e.target.value;
const keys = str.match(/\{(\w+)\}/g);
const params = {};
keys && keys.forEach((item) => (params[item] = {}));
dispatch({
type: "groupTemplate/setSqlParams",
payload: params
});
}
}
};

  name 中会包含组件类别和名称,类别包括 4 种:antd、模板、HTML标准元素和自定义组件。

export const componentHash:ObjectType = {
admin: {
Prompt,
SelectTabs,
CreateModal,
},
antd: {
Affix,
Anchor,
AutoComplete,
},
html: {
a: (node:JSX.Element|string, props = {}) => <a {...props}>{parse(node.toString())}</a>,
p: (node:JSX.Element|string, props = {}) => <p {...props}>{parse(node.toString())}</p>,
},
custom: { ...Custom },
};

  jsonToComponent() 是将JSON转换成组件的函数,就是从上面的对象中得到组件,带上属性、子组件后,再将其返回。

const jsonToComponent = (item:JsonComponentItemType) => {
const {
name, props = {}, node,
} = item;
const names = name.split('.');
const types = componentHash[names[0]];
// 异常情况
if (!types || names.length === 1) {
return null;
}
const Component = types[names[1]];
// HTML元素处理
if (names[0] === 'html') {
return Component(node, props);
}
// 组件处理
if (node) { return <Component {...props}>{parse(node)}</Component>; }
return <Component {...props} />;
};

5)关联组件

  关联组件特指一个模板组件内包含另一个模板组件,例如标签栏组件,它会包含其他模板组件。

  

  如果要做到关联,最简单的方法是将组件的配置一起写到标签栏的参数中,但这么做会非常繁琐,并且内容太多,不够直观。还不如跳过低代码平台,直接在编辑器中编写,来的省事。

  后面就想到关联组件索引,关联的组件也可以在平台中编辑自己的参数。只是当组件删除后,关联的组件也要一并删除,代码的复杂度会变高。

6)交互预览

  在预览时,为了能实现交互,就需要修改状态驱动视图的更新。

  对于一些方法,在执行过后,就能实现状态或视图的更新。

  但对于一些属性,例如 values.allState,若要让其能动态读取内容,就需要借助 getter。

const values:ObjectType = {
get allState() {
return wrapperState;
},
};

二、配套设施

  要将该平台推广到内部使用,除了渲染引擎外,还需要些配套设施,包括自定义业务组件、页面呈现、持久化存储等。

1)业务组件

  内置的组件肯定是无法满足实际的业务,所以需要可以扩展业务组件,由此制订了一套简单的数据源规范。所有的业务组件我都放到了custom文件中,可自行创建新文件,例如 demo。

custom
├──── demo
├──── index.tsx
├──── test.tsx

  在 index.tsx 文件中,会引入自定义的组件,后面就能在平台中使用了。

import Demo from './demo';
const Components:ObjectType = {
Demo,
};
export default Components;

  为了便于调试,预留了测试组件的页面,在下拉框中选择相应的组件,并填写完属性后,就会在组件内容区域呈现效果。

  

2)生成文件

  在配置区域点击生成/更新文件后,就会将菜单、路由、权限等信息保存到 MongoDB 中。其中最重要的就是组件的原始信息,如下所示。

{
"components": [{
"props": {
"message": "44",
"description": "555",
"showIcon": true
},
"name": "Prompt"
}],
"auto_url": ['api', 'article/list'],
"authority": "backend.sql.ccc",
"parent": "backend.sql",
"path": "lowcode/test",
"name": "测试",
}

  为了与之前的路由和权限机制保持一致,在保存成功后,需要自动更新本地的路由文件(router.js)和权限文件(authority.ts)。

// 路由
{
path: "/view/lowcode/test",
exact: true,
component: "lowcode/editor/run"
}
// 权限
{
id: "backend.sql.test",
pid: "backend.sql",
status: 1,
type: 1,
name: "测试",
desc: "",
routers: "/view/lowcode/test"
}

3)页面呈现

  由于是运行时渲染,因此页面的呈现都使用了一套代码,只是路由会不同。所有的路由都是以 view/ 为前缀,在首次进入页面时,会根据路径读取页面信息,路径会去除前缀。

const { pathname } = location; // 查询参数
if (pathname.indexOf("/view/") >= 0) {
dispatch({
type: "getOnePage",
payload: { path: pathname.replace("/view/", "") }
});
}

  在页面呈现的内部,代码很少,在调用 initialPage() 函数后,得到组件列表,直接在页面中渲染即可。initialPage() 其实就是渲染引擎,内部代码比较多,在此不展开。

function Run({ dispatch, state, allState }:EditorProps) {
const { pageInfo } = state;
let components;
if (pageInfo.components) {
components = initialPage(pageInfo, dispatch, allState, false);
}
return (
<>
{components && components.map((item:ComponentType2) =>
(item.visible !== false && item.component))}
</>
);
}

4)体验优化

  体验优化很值得推敲,目前还有很多地方有待优化,自己只完成了一小部分。

  例如在创建页面时,第一次点击后,第二次点击是做更新,而不是再次创建。因为在创建后会更新路由和权限文件,那么就会重新构建,完成热更新,页面再刷新一次。为了下次点击按钮是更新,可以更改地址,带上id。

history.push(`/lowcode/editor2?id=${data._id}`)

  在组件区域提供一个按钮,还原最近一次的组件状态,这样即使页面报错,刷新后,还能继续上一步未完成的操作。

Node.js躬行记(24)——低代码的更多相关文章

  1. Node.js躬行记(6)——自制短链系统

    短链顾名思义是一种很短的地址,应用广泛,例如页面中有一张二维码图片,包含的是一个原始地址(如下所示),如果二维码中的链接需要修改,那么就得发代码替换掉. 原始地址:https://github.com ...

  2. Node.js躬行记(1)——Buffer、流和EventEmitter

    一.Buffer Buffer是一种Node的内置类型,不需要通过require()函数额外引入.它能读取和写入二进制数据,常用于解析网络数据流.文件等. 1)创建 通过new关键字初始化Buffer ...

  3. Node.js躬行记(2)——文件系统和网络

    一.文件系统 fs模块可与文件系统进行交互,封装了常规的POSIX函数.POSIX(Portable Operating System Interface,可移植操作系统接口)是UNIX系统的一个设计 ...

  4. Node.js躬行记(4)——自建前端监控系统

    这套前端监控系统用到的技术栈是:React+MongoDB+Node.js+Koa2.将性能和错误量化.因为自己平时喜欢吃菠萝,所以就取名叫菠萝系统.其实在很早以前就有这个想法,当时已经实现了前端的参 ...

  5. Node.js躬行记(15)——活动规则引擎

    在日常的业务开发中,会包含许多的业务规则,一般就是用if-else硬编码的方式实现,这样就会增加逻辑的维护成本,若无注释,可能都无法理解规则意图. 因为一旦规则有所改变,那么就需要修改代码再发布代码, ...

  6. Node.js躬行记(19)——KOA源码分析(上)

    本次分析的KOA版本是2.13.1,它非常轻量,诸如路由.模板等功能默认都不提供,需要自己引入相关的中间件. 源码的目录结构比较简单,主要分为3部分,__tests__,lib和docs,从名称中就可 ...

  7. Node.js躬行记(21)——花10分钟入门Node.js

    Node.js 不是一门语言,而是一个基于 V8 引擎的运行时环境,下图是一张架构图. 由图可知,Node.js 底层除了 JavaScript 代码之外,还有大量的 C/C++ 代码. 常说 Nod ...

  8. Node.js躬行记(23)——Worker threads

    Node.js 官方提供了 Cluster 和 Child process 创建子进程,通过 Worker threads 模块创建子线程.但前者无法共享内存,通信必须使用 JSON 格式,有一定的局 ...

  9. Node.js躬行记(3)——命令行工具

    一.自定义 创建一个空目录,然后通过npm init命令初始化package.json文件,并按提示输入相关信息或直接回车使用默认信息,生成的内容如下所示. { "name": & ...

随机推荐

  1. V8中的快慢数组(附源码、图文更易理解😃)

    接上一篇掘金 V8 中的快慢属性,本篇分析V8 中的快慢数组,了解数组全填充还是带孔.快慢数组.快慢转化.动态扩缩容等等.其实很多语言底层都采用类似的处理方式,比如:Golang中切片的append操 ...

  2. Redis 02 数据库

    参考源 https://www.bilibili.com/video/BV1S54y1R7SB?spm_id_from=333.999.0.0 版本 本文章基于 Redis 6.2.6 Redis 默 ...

  3. P4767 [IOI2000]邮局 - 平行四边形不等式优化DP

    There is a straight highway with villages alongside the highway. The highway is represented as an in ...

  4. 《吐血整理》进阶系列教程-拿捏Fiddler抓包教程(16)-Fiddler如何充当第三者,再识AutoResponder标签-上篇

    1.简介 Fiddler充当第三者,主要是通过AutoResponder标签在客户端和服务端之间,Fiddler抓包,然后改包,最后发送.AutoResponder这个功能可以算的上是Fiddler最 ...

  5. SpringMVC 06: 日期类型的变量的注入和显示

    日期处理和日期显示 日期处理 此时SpringMVC的项目配置和SpringMVC博客集中(指SpringMVC 02)配置相同 日期处理分为单个日期处理和类中全局日期处理 单个日期处理: 使用@Da ...

  6. Java学习笔记:基本输入、输出数据操作实例分析

    Java学习笔记:基本输入.输出数据操作.分享给大家供大家参考,具体如下: 相关内容: 输出数据: print println printf 输入数据: Scanner 输出数据: JAVA中在屏幕中 ...

  7. K8s nginx-ingress 如何配置二级目录转发远程静态服务器基于Vue路由history模式打包的应用程序

    背景 首先这标题有点绕,我先解释下: 首先我们有静态服务器,上面某个目录有Vue路由history模式打包的应用程序(也就是build后的产物): 但是静态服务器一般不做对外域名用的,我们需要在k8s ...

  8. day03-2无异常退出

    多用户即时通讯系统03 4.编码实现02 4.3功能实现-无异常退出系统 4.3.1思路分析 上述代码运行时,在客户端选择退出系统的时候,可以发现程序并没有停止运行,原因是: 退出时,程序将循环标志l ...

  9. 安装krew

    地址:https://krew.sigs.k8s.io/docs/user-guide/setup/install/ macOS/Linux Bash or ZSH shells 确保已安装git 2 ...

  10. Elasticsearch:Index alias

    现在让我们来谈谈Elasticsearch最简单和最有用的功能之一:别名 (alias).为了区分这里alias和文章"Elasticsearch : alias数据类型",这里的 ...