B端低码平台的实践与思考

# B端低码平台的实践与思考

# 业务背景

B端页面的交互逻辑主要以增删改查为主,且大多数页面对于UI没有太多个性化的要求;随着传统B端项目的迭代,里面可能已经开发了大量的页面,很多都是CRUD式的粘贴复制,代码也比较冗余。基于这些痛点,我们团队打算搭建一个可以快速产出b端页面的工具,方便平时开发提效,缩短B端页面开发时长,同时也是为了减少B端项目代码体积。

关于前端B端低码平台的实现,目前业内已经有一些比较成熟的方案,比如阿里的LowCodeEngine、百度的amis等等,如果直接接入这些第三方,可能需要收费,很多功能依赖别人提供技术支持,对于我们开发会比较被动,如果有问题也不能及时的解决;而且第三方的工具一般只能支持通用型组件配置,对于某些特殊业务场景不太适配;基于这些原因,所以打算自研开发一套完整的适配于我们业务的B端低码平台。

# 平台架构

在V1.0版本里,想的是快速搭建一个能实现逻辑闭环的MVP应用,能满足基本的CRUD操作,后续再根据业务场景进行功能扩展。如果从0到1开始搭建一个完整的低码平台,需要考虑以下几个方面:

  1. 用户权限管理:考虑到在V1.0版本里主要是验证B端低码页面的功能实现和快速落地生产,所以用户权限管理复用已有的平台权限系统,后续再根据业务场景进行功能扩展。
  2. Editor编辑模块和Web端渲染模块:其中Editor端和Web端需要设计一套通用的渲染引擎,能满足页面的基本交互逻辑,同时也需要考虑到性能优化问题,是平台的核心功能模块
  3. 页面的展示形式:这次主要是以iFrame的形式嵌入到已有平台里面,后续再单独搭建专门的低码页面平台,需要设计页面的嵌入方式,满足动态添加低码页面的需求。
  4. 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 只会指向匿名函数,定位到具体配置的哪一行、哪一个字段比较困难。

优化方案:

  1. 如果以后平台面向外部客户 / 多租户,可以改为预置渲染器,让配置只存“函数名/类型”,真正的实现写在代码里; 渲染器白名单 + 表达式/模板 的组合,尽量不要让外部写任意 JS。
  2. 受控执行环境 / 沙箱:如果业务必须支持“用户写 JS”,尽量把执行环境隔离;
  3. 限制使用场景:仅在“内部项目”或“可信租户”下开启;或需要开启一个“高级模式 / 超级权限”才允许写 JS。
  4. 严格校验输入:对 item.render 做静态检查/正则过滤,禁止 window , document , eval , Function , XMLHttpRequest 等敏感标识符(虽然不绝对安全,但能挡住一部分问题)。

# 事件流配置

在低码平台中,页面DOM的静态渲染可以通过配置不同的组件物料来实现渲染,但如果想给某个按钮添加点击事件,就需要自定义事件流来完成交互逻辑。

属性 - 事件 配置中可以进行事件流配置,该功能用到的第三方库react-infinite-viewer来实现配置画布节点的渲染:

每一个事件节点可添加方法,平台会提供一些默认方法,比如【页面跳转】、【消息通知】等等;也可以选择已添加组件提供的方法,这些方法需要在组件创建时就定义好。

事件流执行逻辑:默认按照数组进行存储,在执行时会转化成链表结构:{action:{...}, next:{...}},然后依次往后执行;每一个事件节点可往下一节点传参

事件流完整数组: (7) [{…}, {…}, {…}, {…}, {…}, {…}, {…}] 
事件流完整链表: {action: {…}, next: {…}}
当前事件节点: {action: {…}, next: {…}}
当前事件节点上下文参数: {id: 8, …}  // 这里打印的是传递到当前事件节点的参数

事件流执行流程

  1. 事件流创建完成后会生成一个事件流ID,同时将配置信息数组的形式保存到页面整体的schema中,并将该事件流ID添加到组件配置中;
  2. 后续在执行事件流时,会根据事件流ID从schema中获取对应的配置信息数组;
  3. 将配置信息数组转化成链表结构,然后依次执行每个事件节点的方法;如果某个方法调用了其他组件的方法,就会根据组件ID从schema中获取对应的方法配置,然后执行该方法。
  4. 每个事件节点可添加参数,参数可以是静态值,也可以是动态值,动态值会从当前事件节点的上下文参数中获取。

# 接口如何传参

一般接口参数可在 接口 => 接口配置 => 发送参数 中配置:

  • 静态值:如果有些接口需要固定传入某个参数,可以直接添加一个静态值参数,直接在参数值中输入即可;
  • 变量:如果参数值想获取某个已定义的变量,直接输入即可,如:context.variable.recordDetail.id
  • 模板语法:如果需要从传入参数中获取变量,可以使用模板语法,支持数组的join、map等方法
    • 如参数名 c 需要将传入参数值逗号分隔,在参数值中输入:${c.join(',')} 即可;
    • 如果有一个不属于事件流传入参数的参数名 price,需要将 a*100,那就需要这样写:${data.a * 100}
  • 函数变量:如果需要进行比较复杂的处理,可以点击函数图标,在函数模板中输入函数即可,如这里需要根据传入的status来进行判断:
function run() {
    const {status} = context.eventParams;
    return status === 1 ? 0 : 1;
}

# 表单联动显隐

例:表单字段a只有当表单字段b值等于1时才展示

  1. 选中字段a组件, 属性 => 组件显隐,点击显示条件的fx图标,打开逻辑编辑器面板;
  2. 在面板右侧参数和变量中找到字段b,点击添加到编辑器:context.Form_8ycpmhrdyg.b === 1 ? true : false
  3. 添加这种的三元表达式,保存后立即生效;

原理就是绑定动态变量,监听这个变量的变化,当变量变化时,根据表达式的结果来判断是否展示该组件。

# 样式隔离

目前平台产出的低码平台是以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方向的功能实践:

  1. 低码页面的AI配置助手:通过AI助手引导用户进行低码页面的配置,包括组件配置、接口配置、变量配置、事件流配置等。
  2. 低码页面的AI代码生成器:通过RAG生成固定格式的schema,用户只需要输入一些简单的描述,即可生成对应的低码页面代码。

具体实践我整理在另一篇博客里了:RAG实战:低码平台接入RAG知识库,这里不再赘述~

# 思考

  • 目前主要支持CRUD类页面,以后可考虑支持PC首页、图表展示、调查问卷、H5等不同类型页面;
  • 目前提供的组件主要是一些基础表单组件,以后可支持更丰富的表单嵌套组件,也可根据需求进行业务组件定制,以期能覆盖到业务中更多的需求场景;
  • 目前是在已有平台中通过iframe嵌入低码页面的形式使用,以后可考虑通过低码平台直接配置独立的项目,然后再为该项目配置页面;
  • 对于传统项目中的已有页面,也可考虑迁移到低码配置,减少传统项目中的代码冗余;

# 备注

# 页面版本控制

目前平台创建的页面会在数据库里存储最多3个已发布版本,存储规则为先进先出,支持回滚,原理就是通过接口调用/page/rollback来实现回滚到上一个已发布版本,同时更新publish_id等信息。页面的版本控制一般是基于同一个版本的渲染引擎,每次修改后再发布都会生成新的publish_id。

# 平台版本控制

  • 低码平台按照按照是否会用到渲染引擎来区分的话:
  1. 没有用到渲染引擎的部分其实就是一个传统的B端平台,有首页list展示,页面快速初始化这些基础通用模块
  2. 用的到渲染引擎的就是比较核心的部分,这部分涉及 组件物料 => 业务组件 => 页面模板的渲染和事件流执行,以及各种属性的配置

所以在平台版本控制上:

  1. 每次平台有新的修改发版上线都会生成一个新的版本号:主版本号.次版本号.修订号,存储到服务器上的打包后的js等静态资源也会按照版本号进行存储;这里需要把含有渲染引擎相关逻辑的js包和入口main.js进行拆包,分别存储到不同的目录下,比如/dist/main.js/dist/v1.1.0/index.js,其中v1.1.0就是版本号
  2. 同时后端会提供有一个可以获取所有版本号的接口,也需要根据版本号来获取动态加载对应版本的静态资源;
  3. 创建页面时默认选中最新的版本号,加载最新的静态资源,同时页面创建成功会在Schema中存储version信息;页面编辑时再通过version来动态加载对应版本的静态资源,保证页面在不同版本之间的切换时,能够正常渲染。

后续会出一个自动检查所有已有页面渲染引擎版本的脚本,自动检测所有页面的渲染引擎版本,再根据设置的过期时间,动态筛除已经过期且不再使用的渲染引擎版本,释放相关的静态资源空间。

# 平台监控

公司有公共的监控平台,先申请平台id,然后再平台通过代码接入,统计页面加载性能指标,在平台查看数据。

# 监控

上次更新: 3/18/2026, 12:19:45 AM
最近更新
01
RAG实战:低码平台接入RAG知识库
03-04
02
AI原创短片创作实操笔记
02-23
03
Fine-tuning学习笔记
02-07
更多文章>