一种关于低代码平台(LCDP)建设实践与设计思路
简介: 作者在负责菜鸟商业中心CRM系统开发过程中发现有一个痛点:业务线很多,每个业务线对同一个页面都有个性化布局和不同的字段需求,而他所在的团队就3个人,那么在资源有限的情况下该如何支撑呢?本文就降本的情况下,和大家分享下作者是如何低成本构建产品能力去支撑多条业务线、多租户的。
作者 | 刘玄(玄哥)
来源 | 阿里开发者公众号
背景
负责菜鸟商业中心CRM系统开发已经有1年多时间,过程中发现有一个痛点:业务线特别多,每个业务线对同一个页面都有个性化布局和不同的字段需求,而我所在的团队就3个人,在资源有限的情况下如何支撑好呢?刚开始,我们是为各业务线单独定制页面和业务逻辑,1到2个业务线还能应付过来,目前已经发展有十几业务线,且每个业务线下还有子业务线,这种个性化的开发多了,工作量就大了,系统维护压力就巨大。所以就孕育而生了—— 销售魔方类低代码产品,与其说低代码产品,还不如说是一种解决千人千面的个性化业务搭建的前后端一体的解决方案。
本文就降本的情况下,我是如何低成本构建产品能力去支撑多条业务线、多租户,我先以小实践成果展示,再过度分享我后续升级的设计思路。
什么是LCDP
低代码开发平台(Low-Code Development Platform)是无需编码(0代码)或通过少量代码就可以快速生成应用程序的开发平台。通过可视化进行应用程序开发的方法(参考可视编程语言),使具有不同经验水平的开发人员可以通过图形化的用户界面,使用拖拽组件和模型驱动的逻辑来创建网页和移动应用程序。低代码开发平台(LCDP)的正式名称直到2014年6月才正式确定,整个低代码开发领域却可以追溯到更早前第四代编程语言和快速应用开发工具。
魔方核心能力
产品能力
上图是魔方1.0 MVP版本基本运行原理,以及上线后降本增效的数据,业务开发从60人日缩短到20人日,年省成本180人日。
以上版本基本满足了80%以上的业务个性化需求自闭环开发。
还有一些小问题,基于这个版本,我们又不断的升级,提升产品体验、能力提升和业务覆盖。
后续我们可做到新页面上线,只需5分钟,新增字段无需模型变更和无需java代码发布,复杂页面前端也能做到0代码。
基于我们业务的诉求,所以销售魔方需具有以下几个核心能力:
- 页面的千行千面(千人千面),包含同一个页面不同布局、不同字段、不同样式。
- 数据模块的千行千面(千人千面),根据不同身份执行不同的业务技术逻辑和服务编排。
- page一键创建,在没有新的业务组建和新的module情况无需开发接入,0代码上线,运营同学自行配置页面。
- 前端组件复用,在没有新前端组件,前端无需参与开发,后端只需编写module对应的业务接口。
- 实现module可复用,module数据渲染、数据写入,查询条件、浮层、半推页面、页面操作。
- 新增字段扩展0代码,模型字段可以自定义,动态扩展,可定义来自本地数据库、远程HSF接口数据。
- 环境可隔离,测试、预发、生产。
- 平台和业务代码分离,业务上线只需关注业务逻辑本身的代码。
- DO DTO可定义,动态映射。
- 数据枚举动态定义,动态绑定。
魔方的设计
产品界面
先展现一个实例配置界面,有个体感
通过以上配置可以个性化配置页面输出的布局和字段,动态输出个性化页面
用户
运营:运营可以根据自己的业务身份,定义独有的page实例,自由选择页面的版块和需要展现和编辑的字段
产品:配置初始化页面,module、以及全量字段池
技术:定义元数据,module,编写module group逻辑执行单元代码
产品模块
核心逻辑
一般低代码平台,主要分为两部分,前端页面的渲染和后端服务接口绑定(服务编排等)。
大的逻辑差不多,因为我这个主要还是具有行业特色的类低代码产品,所以是紧扣行业特殊性构建。
前端渲染
因为我负责的是后端,所以前端我就不过多叙述,大概的逻辑如下两张图,大致意思是前端的页面渲染就如同做菜一样,用户根据自己的需求,可选择菜谱、食材和烹饪方式,不用关心烹饪过程,也不用自己亲自烹饪,都叫由厨师烹饪,厨师会根据你提供的菜谱和食材做好,最后将美食给你端上桌。
同理,前端渲染引擎会根据数据协议、组件库、渲染方式,动态渲染成页面,如果有业务数据将会动态绑定。
后端绑定
我们这有个特殊性,页面是通过后端给定schema,由前端根据这个schema进行页面渲染。后端通过识别出用户的身份,通过接口输出给前端千人千面的个性化schema,前端就通过schema配置动态去渲染。
这样就能实现我们说的同一个功能页面,不同业务身份展示不同的布局和字段。
同时,还会会有一个业务数据接口,用于绑定前端页面填充业务数据或提交表单数据。
每一个组件绑定业务数据接口后,就不是单纯的前端组件了,就具有行业业务属性的组件和页面,这样在这个领域是可以被业务系统直接复用,无需重新编写业务代码。
这里有一个难点,每次新增业务线(租户)后就有新增字段需求,而且字段的差异还挺多,约占80%,在不修改模型的前提下,如何做到?
带着这个问题可以先思考下。
一般解决方案有以下几种:
1、通过设计纵表,以扩展字段。缺点就是查询复杂度高。
2、元数据驱动。缺点就是成本高,架构更加复杂
3、通过定义一个特殊feature字段,存储类型为json,约定好协议,扩展字段按照不同行业存储在json内。优点就是轻量。
根据我们自身的定位和资源情况,最终选择的是第3种方案。
这里有个担心就是json数据方便搜索吗?性能如何?
mysql5.7上,在相同数据量的情况下,虚拟索引和普通索引查询效率基本一致 在大数据量情况下不推荐用聚合函数计算json数据,但是如果json key建立了虚拟列,可以对该虚拟列进行聚合操作,效率跟普通列一样。
模型设计
整个模型可以看出,从左往右:
DO的定义、DTO的定义、module定义(等同VO定义)、原始页面定义(originalPage)、实例化页面配置(instancePage)
这个过程基本描述了一个页面是如何定义出来的,以及动态模型的扩展(模型驱动),从模型到页面组件的布局定义。
行业个性化配置则通过对OriginalPage实例化成InstancePage而得。这样就能动态生成千行千面。
OriginalPage到InstancePage这里借鉴了java的思想,类似Class和实例对象。
渲染页面逻辑
页面在渲染的时候,只需两个接口:
1、实例页面数据结构 ;2、业务数据。
前端根据两个接口分成两步渲染:
1、初始化页面布局;2、填充业务数据。
这个借鉴了Spring容器启动时的设计思路,类似实例化、初始化,属性数据填充。
template定义
即是对页面类型的定义
- 列表页面
- 详情页面
- 半开页面
- 表单提交页面
page定义
定义一张前端页面,分成两个阶段:origin;instance。
origin是定义的原始页面,可以理解成java 的Class类,可以构建多个实例页面。
instance page是最终渲染的运行态。
页面结构:一个page 由N个module组成、一个module由N个field组成。如下图
一个实例page由以下维度定义
1、用户传入参数
page_code
custom_dimension
2、系统获取参数
biz_code
sub_biz_code
enviroment
module定义
页面的组成单元,一个页面由多个module组成。代表页面显示区域单元,有多个前端组件组成,是页面容器的布局单元。
module_code 全局唯一,实例化后的module可被复用、重写、实现多态。
modules 数据结构为一个 B+tree,只有叶子节点是有具体的实体数据。
叶子节点是真正包含具体的字段和属性配置
module_type 定义
对module类型的定义
1、主列表查询模块 MAIN_LIST_MODULE
2、导出模块 EXPORT_MODULE
3、弹出页面模块 FLOAT_PAGE_MODULE
4、搜索条件区域 SEARCH_ARE_MODULE
5、子列表查询模块 SUB_LIST_MODULE
6、编辑表单模块 EDIT_MODULE
7、信息平铺呈现 DISPLAY_FLAT_MODULE
McubeContextAware
module容器代码定义
@Component
public class McubeContextAware implements ApplicationContextAware {
private static volatile ApplicationContext alc;
@Resource
private ModuleBeanFactory moduleBeanFactory;
@Resource
private ModuleGroupBeanFactory moduleGroupBeanFactory;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
alc = applicationContext;
}
@PostConstruct
public void init(){
setModuleBeanMap();
setModuleGroupBeanMap();
}
private void setModuleBeanMap() {
Map<String, McubeModuleExecutor> beanMap = alc.getBeansOfType(McubeModuleExecutor.class);
if (beanMap != null) {
beanMap.values().stream().forEach(m -> {
McubeModule module = AnnotationUtils.findAnnotation(m.getClass(), McubeModule.class);
if (module != null) {
String code = module.code();
String name = module.name();
if (code != null) {
moduleBeanFactory.getMcubeBeanMap().put(code, m);
}
}
});
}
}
private void setModuleGroupBeanMap() {
Map<String, McubeModuleExecutor> beanMap = alc.getBeansOfType(McubeModuleExecutor.class);
if (beanMap != null) {
beanMap.values().stream().forEach(m -> {
McubeModuleGroup module = AnnotationUtils.findAnnotation(m.getClass(), McubeModuleGroup.class);
if (module != null) {
String code = module.code();
String name = module.name();
moduleGroupBeanFactory.getMcubeBeanMap().put(code,m);
}
});
}
}
}
执行单元(moduleGroup executor)
为了保证页面的数据填充效率,所以并不是一个module绑定一个服务接口,
而是一个执行单元对应一个或多个module,负责多个module的数据渲染和数据写入,moduleGroup executor是一个页面计算单元。
通过moduleCode动态路由对应的module group,执行相应的计算单元。
每个module 执行单至少都包含读、新增、编辑和删除接口。
页面上的每一个module就自动绑定了后端的业务接口,实现了前后端一体化搭建。
/**
* Created by hzliuxuan on 2022/5/27.
* @author hzliuxuan
* 模块接口
*/
public interface McubeModuleExecutor<T,V> {
/**
* 填充数据,页面渲染,一般是read接口
* @param value
* @return
*/
T populate(V value);
/**
* 编辑模块
* @param value
* @return
*/
void edit(V value);
/**
* 写接口
* @param value
* @return
*/
void add(V value);
/**
* 删除接口
* @param value
* @return
*/
void delete(V value);
}
McubeModuleGroup
module执行组注解定义
field定义
一个module对应多个field。
如果要支持动态扩展,module需要对应一个实体模型。
module只代表一个VO层的部分显示片段,要想达到字段可以动态扩展需要定义一层实体模型的映射关系,这样才能找到统一的feature对象去解析,完成DO、DTO、VO的相互自动转换。
当module需要动态扩展的时候,从实体模型中去选择已经定义好的field。
因为我们的VO也是动态生成的,这样就不需要因为新增一个字段而进行模型变更或者代码发布。即实现0代码上线。
field数据结构定义
page 数据结构
public class McubePageBeanDTO {
/**
* 页面编码
*/
@CrmOperateLogBizCode
private String pageCode;
/**
* 业务线
*/
private String bizCode;
/**
* 配置类型
*/
private TemplateTypeEnum templateType;
/**
* 配置模块
*/
private List<McubeModuleBeanDTO> originalModules;
/**
* 配置字段
*/
private Map<String, List<McubeField>> originalFields;
/**
* 实例的模块
*/
private List<McubeModuleBeanDTO> instanceModules;
private List<String> instanceModulesList;
/**
* 实例的字段
*/
private Map<String, List<McubeField>> instanceFields;
private String subBizCode;
/**
* 元页面version
*/
private Byte originVersion;
/**
* 实例version
*/
private Byte instanceVersion;
/**
* module version
*/
private Byte moduleVersion;
/**
* 属性集合
*/
private List<Property> properties;
///**
// * 显示的模块
// */
//private List<String> instanceModulesList;
private Boolean isCache;
@Data
public static class Property {
/**
* property
*/
private Boolean checkable;
private Boolean isEdit;
private Boolean selectable;
private Boolean isLeaf;
private Boolean isAdd;
private Boolean isDelete;
private String showType;
private Integer level;
private String extendedField;
}
}
page渲染运行时序图
运行时类设计图
每一个模块背后都会绑定一个 moduleGroup executor ,业务开发只需通过对这个executor实现,即可快速完成开发上线,整个过程无需前端参与。简单的字段添加也无需发布上线,我们会通过动态扩展映射背后的DO扩展。
总结
好了,整个魔方的产品设计到这里基本描述完。
整个产品我理解更多的是贴近业务而产生的一种前后端一体化,低成本快速构建方案。如果要做大,做全,可以参考salesforce元数据驱动模型。
模型关系图:
可参考:https://www.infoq.cn/article/rwstpgujoxxuw9tlm88t
salesforce官方架构文档:https://www.developerforce.com/media/ForcedotcomBookLibrary/Force.com_Multitenancy_WP_101508.pdf
salesforce 已经超脱了模型驱动,下探到元数据模型驱动架构 Metadata-driven Architectures。
优点很明显,就是真的强大,业务都不需要建任何表,想怎么扩展模型就怎么扩展,此架构一般适用于与SAAS产品。
缺点也很明显,完全失去业务语义,开发成本和维护成本高,需要一套强大的sql管理和解析、实时的ETL数据架构、检索能力,后期成本也非常大。
故不适用我这个业务团队采纳的方案,但这套设计方案也给我打开了些思路。
魔方这套方案搭建工时约为50人日,两个后端加一个前端,解决了十几个业务线多租户的个性化接入,一定程度实现了模型驱动,千人千面的能力,我认为是典型小投入大产出吧,希望对正遇到同样问题的同学有一定的帮助和启发,限于个人能力,最终要搞大的需要有专业的团队支撑,也欢迎指正和探讨。
阿里云产品评测—阿里云容器镜像服务 ACR
免费试用体验面向容器镜像、Helm Chart 等符合 OCI 标准的云原生制品安全托管及高效分发平台,发布你的评测更有机会获得千元机械键盘,限量定制礼品。
点击这里,查看详情。McubeContextAwareMcubeContextAware
一种关于低代码平台(LCDP)建设实践与设计思路的更多相关文章
- vivo 低代码平台【后羿】的探索与实践
作者:vivo 互联网前端团队- Wang Ning 本文根据王宁老师在"2022 vivo开发者大会"现场演讲内容整理而成.公众号回复[2022 VDC]获取互联网技术分会场议题 ...
- vivo 游戏中心低代码平台的提效秘诀
作者:vivo 互联网服务器团队- Chen Wenyang 本文根据陈文洋老师在"2022 vivo开发者大会"现场演讲内容整理而成.公众号回复[2022 VDC]获取互联网技术 ...
- 基于低代码平台(Low Code Platform)开发中小企业信息化项目
前言:中小企业信息化需求强烈,对于开发中小企业信息化项目的软件工作和程序员来说,如何根据中小企业的特点,快速理解其信息化项目的需求并及时交付项目,是一个值得关注和研讨的话题. 最近几年来,随着全球经济 ...
- 低代码开发LCDP,Power Apps系列 - 新建泰勒创新中心门户案例
低代码简介 上世纪八十年代,美国就有一些公司和实验室开始了可视化编程的研究,做出了4GL"第四代编程语言",到后来衍生成VPL"Visual Programming La ...
- 低代码开发LCDP,Power Apps系列 - 搭建入职选购电脑设备案例
低代码简介 上世纪八十年代,美国就有一些公司和实验室开始了可视化编程的研究,做出了4GL"第四代编程语言",到后来衍生成VPL"Visual Programming La ...
- 低代码平台--基于surging开发微服务编排流程引擎构思
前言 微服务对于各位并不陌生,在互联网浪潮下不是在学习微服务的路上,就是在使用改造的路上,每个人对于微服务都有自己理解,有用k8s 就说自己是微服务,有用一些第三方框架spring cloud, du ...
- 分析师机构发布中国低代码平台现状分析报告,华为云AppCube为数字化转型加码
摘要:Forrester指出,中国企业数字化转型过程中,有58%的决策者正在采用低代码工具进行软件构建,另有16%的决策者计划采用低代码. 华为消息,知名研究与分析机构Forrester Resear ...
- 使用WtmPlus低代码平台提高生产力
低代码平台的概念很火爆,产品也是鱼龙混杂. 对于开发人员来说,在使用绝大部分低代码平台的时候都会遇到一个致命的问题:我在上面做的项目无法得到源码,完全黑盒.一旦我的需求平台满足不了,那就是无解. ...
- OpenDataV低代码平台增加自定义属性编辑
上一篇我们讲到了怎么在OpenDataV中添加自己的组件,为了让大家更快的上手我们的平台,这一次针对自定义属性编辑,我们再来加一篇说明.我们先来看一下OpenDataV中的属性编辑功能. 当我们拖动一 ...
- 干货!可以使用低代码平台代替Excel吗?
低代码开发平台可以代替Excel?不用惊讶,答案是肯定的,而且,低代码开发平台可以完全代替Excel.例如Zoho Creator低代码平台,可以围绕数据存储.管理和创建工作流程.期间不需要IT人员介 ...
随机推荐
- esp8266 I2C 实例解析及源码分析
一 前言 作为一个方案商兼芯片开发者,研究芯片和功能实现除了基本的工作需要,还有一层就是也变成了一种职业习惯.从芯片到方案,发现很多方案公司的人水平都比较堪忧,只会调用api,根本不会看底层的代码实 ...
- gulp-imagemin版本9图片压缩
由于网上大多数的博文已经比较久,参考性不大 版本 gulp PS D:\XXX\github\hexo> gulp -v CLI version: 2.3.0 Local version: 4. ...
- RTMP录屏直播屏幕数据获取与MediaCodec编码
目录 前言 RTMP直播实现流程 视频采集--MediaProjection 编码--MediaCodec 音频采集--AudioRecord RTMP音频包数据 RTMP视频数据 前言 本文介绍的是 ...
- 你的DDPG/RDPG为何不收敛?
园子好多年没有更过了,草长了不少.上次更还是读博之前,这次再更已是博士毕业2年有余,真是令人唏嘘.盗链我博客的人又见长,身边的师弟也问我挖的几个系列坑什么时候添上.这些着实令我欣喜,看来我写的东西也是 ...
- Jmeter的Throughput有误差与分布式测试时的坑
我是两台压力机,分布式启动jmeter压测180秒,结果throughput显示3075,我用总请求数/总耗时,64万左右/180秒,得到的TPS是3500左右.误差17% 网上说jmeter的thr ...
- MapReduce的基础知识
1.什么是MapReduce Hadoop MapReduce 是一个 分布式计算框架,用于轻松编写分布式应用程序,这些应用程序以可靠,容错的方式并行处理大型硬件集群(数千个节点)上的大量数据(多TB ...
- APP探索之iAPP
APP探索之iAPP 1.基本作用 iAPP是一个手机上的应用,可以用于快速设计手机应用,基本免费.使用的语言好像是自创的脚本语言.无聊时可以用iAPP做一些简单的训练,可以练习文件和数据的操作.对于 ...
- 跳转到制定Sheet页及提交指定sheet页内容
一.跳转到指定Sheet的实现 话不多说,先上效果图 两个按钮的事件分别如下: _g().loadSheetByName("sheet1") # 跳转至sheet1按钮事件 _g( ...
- layui框架使用单页面弹出层组件layer
layui实现单页面弹出层 首先需要导入layui的js和css: <link rel="stylesheet" href="layui/css/layui.css ...
- jQuery获得或设置内容和属性、添加属性 append和after的区别
来自w3school 在线教程 jQuery获得或设置内容和属性 text() - 设置或返回所选元素的文本内容 html() - 设置或返回所选元素的内容(包括 HTML 标记) val() - 设 ...