B端低码平台的实践与思考
# B端低码平台的实践与思考
# 业务背景
B端页面的交互逻辑主要以增删改查为主,且大多数页面对于UI没有太多个性化的要求;随着传统B端项目的迭代,里面可能已经开发了大量的页面,很多都是CRUD式的粘贴复制,代码也比较冗余。基于这些痛点,我们团队打算搭建一个可以快速产出b端页面的工具,方便平时开发提效,缩短B端页面开发时长,同时也是为了减少B端项目代码体积。
关于前端B端低码平台的实现,目前业内已经有一些比较成熟的方案,比如阿里的LowCodeEngine、百度的amis等等,如果直接接入这些第三方,可能需要收费,很多功能依赖别人提供技术支持,对于我们开发会比较被动,如果有问题也不能及时的解决;而且第三方的工具一般只能支持通用型组件配置,对于某些特殊业务场景不太适配;基于这些原因,所以打算自研开发一套完整的适配于我们业务的B端低码平台。
# 平台架构
在V1.0版本里,想的是快速搭建一个能实现逻辑闭环的MVP应用,能满足基本的CRUD操作,后续再根据业务场景进行功能扩展。如果从0到1开始搭建一个完整的低码平台,需要考虑以下几个方面:
- 用户权限管理:考虑到在V1.0版本里主要是验证B端低码页面的功能实现和快速落地生产,所以用户权限管理复用已有的平台权限系统,后续再根据业务场景进行功能扩展。
- Editor编辑模块和Web端渲染模块:其中Editor端和Web端需要设计一套通用的渲染引擎,能满足页面的基本交互逻辑,同时也需要考虑到性能优化问题,是平台的核心功能模块。
- 页面的展示形式:这次主要是以iFrame的形式嵌入到已有平台里面,后续再单独搭建专门的低码页面平台,需要设计页面的嵌入方式,满足动态添加低码页面的需求。
- Server端接口开发:需要快速设计一套满足低码页面创建编辑的接口,包括页面的创建、编辑、保存、发布等操作。
V1.0 目标
- 搭建了一套完整的B端低码最小可行性应用,支持线上生产使用
- 支持常见的B端CRUD页面的搭建,减少传统项目代码冗余
- 减少B端CRUD页面开发成本,可快速产出页面支持业务需求迭代
# 技术栈选择
这个项目是一个全栈项目,包含前端的Editor编辑模块、Web端渲染模块,还有Server端的接口模块;技术栈上前端采用React + TypeScript + Vite +Ant Design,后端采用Node.js + Koa + Mysql2;整体代码仓库是用pnpm搭建的monorepo项目,前端代码和后端代码都在同一个仓库里
- 前端用React是考虑到团队里对React比较熟悉,还有就是其他B端平台用的UI框架基本都是Ant,所以选择Ant Design也是方便保证UI统一,可以降低上手成本;
- 后端用Node.js是因为笔者之前有过Node.js的项目开发经验,也比较熟悉Koa框架,所以选择Node.js也是为了快速搭建一个MVP应用,后续再根据业务场景进行功能扩展。还有就是这个项目主要是给内网的B端系统使用,所以对性能要求不是太高,Node.js的单线程模型也能满足需求。
项目目录结构如下:
├── packages
│ ├── editor # 编辑端项目
│ └── web # 展示端项目
├── server # 后端项目
├── package.json # 根目录package.json
├── deploy.sh # 部署脚本
├── Dockerfile # 服务端 server 容器化配置文件
├── ci.yml # ci配置文件
└── pnpm.workspace.yaml
# 平台架构设计
平台架构设计图如下:
# 前端功能模块梳理
Editor编辑端
编辑端主要是提供可视化的页面编辑功能,包括页面的创建、编辑、保存等操作,可增删组件,配置组件事件流、变量、接口等,是该平台的核心功能模块;是一个用
React + Vite搭建的前端项目。
左侧菜单栏:
- 组件:平台提供的组件(容器组件、高级组件、表单组件、业务组件、...),点击可往画布添加
- 大纲:实时展示当前页面的DOM结构,可拖拽排序,点击可在画布选中
- 代码:实时展示当前页面配置的JSON信息
- 接口:配置需要在当前页面使用的接口信息,可以添加当前页面所需接口,包括
请求方式、数据格式、传参等等 - 变量:配置需要在当前页面使用的变量信息
中间画布:
- 顶部可选择页面宽度,有【保存】、【预览】等操作
- 中间画布区域实时展示当前配置页面,点击可选中,同时在右侧会展示当前选中组件的配置信息
- 底部左下角会展示当前添加的Modal弹窗、Drawer抽屉组件,双击可展示
右侧属性配置区域:
- 属性:展示当前选中组件的属性配置信息,这些信息需要在创建组件时就提前定义好
- 样式:可配置一些常用的基础样式,也可自定义样式
- 事件:给选中组件添加事件,如搜索表单组件的【提交】、【重置】等事件就需要在这里配置
- 数据:给当前组件添加数据,主要有【静态数据】、【接口请求】、【动态变量】三种,如上方页面在初始化时需要通过接口获取数据,就需要在这里配置
Web展示端
负责将编辑端生成的页面配置渲染到Web端,包括组件的渲染、事件绑定等;也是一个用
React + Vite搭建的前端项目。
组件物料: 该目录单独存在平台需要渲染展示的组件,跟编辑端的组件是分开的,但两端的组件物料大致上应该都是一样的,只是在编辑端的组件物料上会有一些额外的配置项,如事件流、变量、接口等,在展示端的组件物料上就不需要这些配置项了,展示端只需要根据编辑端的配置项渲染组件即可。所以在开发的时候需要注意组件物料的同步更新。
页面展示:目前Web端主要是负责页面展示,可以通过路由
page/:id来访问具体的低码页面。
# Server端梳理
Server端主要负责提供低码页面的创建、编辑、保存、发布等接口;用
Node.js + Koa搭建的后端项目,数据库用Mysql2。
router:路由,负责处理前端请求,将请求分发给对应的Controller处理;路由需要区分Editor编辑端和Web展示端的请求,编辑端的请求需要走/editor路由,展示端的请求需要走/web路由;目前接口主要有:/page/list:获取低码页面列表/page/delete:删除低码页面/page/detail:获取低码页面详情/page/update:更新低码页面配置/page/create:创建低码页面/page/publish:发布低码页面/page/rollback:回滚低码页面到上一个版本/page/detail/:id:获取低码页面详情,:id为页面的id/page/path:通过path访问低码页面/xx/proxy:代理接口,用于调用外部接口,如一些第三方的通用接口等;需要做转发,将前端请求转发到外部接口,返回外部接口的响应结果。
controller:控制器,负责处理具体的业务逻辑,用户权限校验service:服务层,负责处理业务逻辑的实现,如调用数据库、调用其他服务等。
# 页面创建流程
这里梳理了一下目前生产环境下的页面创建流程:
上面这个页面创建流程目前已在生产环境落地使用,后续可根据需要进行优化。
# 技术点分析
# 表格字段如何展示
表格字段展示目前支持单行文本、多行文本、图片、时间、状态、金额、开关等不同格式,配置入口在 属性 => 列配置,点击编辑图标即可配置
- 单行文本:默认配置,如果列字段就是接口返回的字段,直接就可以展示,不需要多余配置
- 多行文本:展示配置 => 显示格式 选择多行文本,按照提示需要在 自定义 => 自定义渲染 中,自定义渲染格式:
// 这里可以自定义渲染字段处理,默认返回列字段
function render(text, record, index, variables) {
return text;
}
// 比如需要根据返回的code展示label值,list是提前定义好的变量:
function render(text, record, index, variables) {
return variables.list.find(v => v.value === text)?.label || '-';
}
// 都是纯前端的写法
- 时间、金额的显示直接在 显示格式 中选择即可,平台已经内置的常用的格式转换;
- Switch开关:选择该格式后,在 事件 中添加 列Switch开关 事件,就可以添加相应的事件流配置;
# 自定义代码片段怎么执行
这里是手动生成一个
new Function来执行的,代码如下:
// 文本处理完后,如果存在render,则执行render
if (item.render) {
try {
// 构造一个 Function 实例
const renderFn = new Function('text', 'record', 'index', 'variables', `return (${item.render})(text,record,index, variables);`);
txt = renderFn(txt, record, index, variableMap); // 传入变量并执行
} catch (error) {
console.error(`列[${item.title}]渲染失败`, error);
txt = '解析异常';
}
}
优点:
- 能把js代码片段在运行期直接执行,做出非常灵活的列渲染逻辑;
- 作用域可控,不会像
eval一样直接访问当前局部变量,只能用显式传进来的参数; - 性能比频繁
eval稍好一点;
缺点:
- 安全风险:存在XSS攻击风险,因为自定义代码片段是在运行期直接执行的,所以如果用户输入了恶意代码,就会直接在页面执行,导致安全问题;
- 完全绕过类型系统和构建期检查;
- 调试体验差,出错时
stack trace只会指向匿名函数,定位到具体配置的哪一行、哪一个字段比较困难。
优化方案:
- 如果以后平台面向外部客户 / 多租户,可以改为预置渲染器,让配置只存“函数名/类型”,真正的实现写在代码里; 渲染器白名单 + 表达式/模板 的组合,尽量不要让外部写任意 JS。
- 受控执行环境 / 沙箱:如果业务必须支持“用户写 JS”,尽量把执行环境隔离;
- 限制使用场景:仅在“内部项目”或“可信租户”下开启;或需要开启一个“高级模式 / 超级权限”才允许写 JS。
- 严格校验输入:对 item.render 做静态检查/正则过滤,禁止
window , document , eval , Function , XMLHttpRequest等敏感标识符(虽然不绝对安全,但能挡住一部分问题)。
# 事件流配置
在低码平台中,页面DOM的静态渲染可以通过配置不同的组件物料来实现渲染,但如果想给某个按钮添加点击事件,就需要自定义事件流来完成交互逻辑。
在 属性 - 事件 配置中可以进行事件流配置,该功能用到的第三方库react-infinite-viewer来实现配置画布节点的渲染:
每一个事件节点可添加方法,平台会提供一些默认方法,比如【页面跳转】、【消息通知】等等;也可以选择已添加组件提供的方法,这些方法需要在组件创建时就定义好。
事件流执行逻辑:默认按照数组进行存储,在执行时会转化成链表结构:{action:{...}, next:{...}},然后依次往后执行;每一个事件节点可往下一节点传参
事件流完整数组: (7) [{…}, {…}, {…}, {…}, {…}, {…}, {…}]
事件流完整链表: {action: {…}, next: {…}}
当前事件节点: {action: {…}, next: {…}}
当前事件节点上下文参数: {id: 8, …} // 这里打印的是传递到当前事件节点的参数
事件流执行流程
- 事件流创建完成后会生成一个事件流ID,同时将配置信息数组的形式保存到页面整体的schema中,并将该事件流ID添加到组件配置中;
- 后续在执行事件流时,会根据事件流ID从schema中获取对应的配置信息数组;
- 将配置信息数组转化成链表结构,然后依次执行每个事件节点的方法;如果某个方法调用了其他组件的方法,就会根据组件ID从schema中获取对应的方法配置,然后执行该方法。
- 每个事件节点可添加参数,参数可以是静态值,也可以是动态值,动态值会从当前事件节点的上下文参数中获取。
# 接口如何传参
一般接口参数可在 接口 => 接口配置 => 发送参数 中配置:
- 静态值:如果有些接口需要固定传入某个参数,可以直接添加一个静态值参数,直接在参数值中输入即可;
- 变量:如果参数值想获取某个已定义的变量,直接输入即可,如:
context.variable.recordDetail.id - 模板语法:如果需要从传入参数中获取变量,可以使用模板语法,支持数组的
join、map等方法- 如参数名 c 需要将传入参数值逗号分隔,在参数值中输入:
${c.join(',')}即可; - 如果有一个不属于事件流传入参数的参数名 price,需要将 a*100,那就需要这样写:
${data.a * 100};
- 如参数名 c 需要将传入参数值逗号分隔,在参数值中输入:
- 函数变量:如果需要进行比较复杂的处理,可以点击函数图标,在函数模板中输入函数即可,如这里需要根据传入的status来进行判断:
function run() {
const {status} = context.eventParams;
return status === 1 ? 0 : 1;
}
# 表单联动显隐
例:表单字段a只有当表单字段b值等于1时才展示
- 选中字段a组件, 属性 => 组件显隐,点击显示条件的
fx图标,打开逻辑编辑器面板; - 在面板右侧参数和变量中找到字段b,点击添加到编辑器:
context.Form_8ycpmhrdyg.b === 1 ? true : false - 添加这种的三元表达式,保存后立即生效;
原理就是绑定动态变量,监听这个变量的变化,当变量变化时,根据表达式的结果来判断是否展示该组件。
# 样式隔离
目前平台产出的低码平台是以iFrame嵌入到其他平台中,页面中可能会存在全局样式覆盖低码页面样式的情况,比如
Modal,Toast这种全局组件,这里为了保持低码页面的样式不受全局样式的影响,需要在低码页面中添加一些特殊的样式来避免样式冲突。
这里采用的方案是Shadow DOM,通过在低码页面中创建一个Shadow Root,将所有组件的DOM节点都挂载到这个Shadow Root下,从而实现样式隔离。
// 创建容器并附加Shadow DOM
const modalContainer = window.parent.document.createElement('div');
modalContainer.setAttribute('id', `acc-lowcode-shadow-modal__wrapper__${+new Date()}`);
const shadowRoot = modalContainer.attachShadow({ mode: 'open' });
# 高并发场景下,如何保证生成ID的唯一性
目前在高并发场景下,生成唯一ID的方式主要有以下几种:
- 数据库自增ID:在数据库表中添加一个自增ID字段,每次插入数据时,数据库会自动给该字段赋值,确保每个ID都是唯一的。
- 分布式ID生成器:使用分布式ID生成器,如Twitter的Snowflake算法、Google的UUID等,这些算法可以在分布式系统中生成唯一的ID。
- 数据库唯一索引:在数据库表中添加一个唯一索引,确保每个ID都是唯一的。
虽然目前这个平台主要在B端使用,不太可能会遇到C端流量突然涌入的高并发情况,但为了底层功能的健壮性,我这边采用的是Snowflake雪花算法。
- 该方法生成的ID 整体上按时间自增,同一毫秒内生成的 ID 也是唯一的;不同数据中心/机器生成的 ID 不会冲突;
- 支持每毫秒生成 4096 个不同的 ID,如果同一毫秒内序列号达到最大值,即生成的 ID 数量超过 4096 个时,序列号会自动归零,触发等待下一毫秒的逻辑。
# AI结合
在前端+AI的项目上,低代码平台其实是一个不错的方向,目前配置一个低码页面需要了解组件配置、接口配置、变量配置、事件流配置等前置知识点,对于想快速搭建一个页面的新手来讲可能门槛较高,以后可以考虑通过AI助手引导用户进行配置,或者通过RAG生成固定格式的schema方便用户快速创建页面;
目前在该平台已经有两个关于AI方向的功能实践:
- 低码页面的AI配置助手:通过AI助手引导用户进行低码页面的配置,包括组件配置、接口配置、变量配置、事件流配置等。
- 低码页面的AI代码生成器:通过RAG生成固定格式的schema,用户只需要输入一些简单的描述,即可生成对应的低码页面代码。
具体实践我整理在另一篇博客里了:RAG实战:低码平台接入RAG知识库,这里不再赘述~
# 思考
- 目前主要支持CRUD类页面,以后可考虑支持PC首页、图表展示、调查问卷、H5等不同类型页面;
- 目前提供的组件主要是一些基础表单组件,以后可支持更丰富的表单嵌套组件,也可根据需求进行业务组件定制,以期能覆盖到业务中更多的需求场景;
- 目前是在已有平台中通过iframe嵌入低码页面的形式使用,以后可考虑通过低码平台直接配置独立的项目,然后再为该项目配置页面;
- 对于传统项目中的已有页面,也可考虑迁移到低码配置,减少传统项目中的代码冗余;
# 备注
# 页面版本控制
目前平台创建的页面会在数据库里存储最多3个已发布版本,存储规则为先进先出,支持回滚,原理就是通过接口调用
/page/rollback来实现回滚到上一个已发布版本,同时更新publish_id等信息。页面的版本控制一般是基于同一个版本的渲染引擎,每次修改后再发布都会生成新的publish_id。
# 平台版本控制
- 低码平台按照按照是否会用到渲染引擎来区分的话:
- 没有用到渲染引擎的部分其实就是一个传统的B端平台,有首页list展示,页面快速初始化这些基础通用模块
- 用的到渲染引擎的就是比较核心的部分,这部分涉及
组件物料 => 业务组件 => 页面模板的渲染和事件流执行,以及各种属性的配置
所以在平台版本控制上:
- 每次平台有新的修改发版上线都会生成一个新的版本号:
主版本号.次版本号.修订号,存储到服务器上的打包后的js等静态资源也会按照版本号进行存储;这里需要把含有渲染引擎相关逻辑的js包和入口main.js进行拆包,分别存储到不同的目录下,比如/dist/main.js和/dist/v1.1.0/index.js,其中v1.1.0就是版本号 - 同时后端会提供有一个可以获取所有版本号的接口,也需要根据版本号来获取动态加载对应版本的静态资源;
- 创建页面时默认选中最新的版本号,加载最新的静态资源,同时页面创建成功会在Schema中存储version信息;页面编辑时再通过version来动态加载对应版本的静态资源,保证页面在不同版本之间的切换时,能够正常渲染。
后续会出一个自动检查所有已有页面渲染引擎版本的脚本,自动检测所有页面的渲染引擎版本,再根据设置的过期时间,动态筛除已经过期且不再使用的渲染引擎版本,释放相关的静态资源空间。
# 平台监控
公司有公共的监控平台,先申请平台id,然后再平台通过代码接入,统计页面加载性能指标,在平台查看数据。