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来动态加载对应版本的静态资源,保证页面在不同版本之间的切换时,能够正常渲染。
后续会出一个自动检查所有已有页面渲染引擎版本的脚本,自动检测所有页面的渲染引擎版本,再根据设置的过期时间,动态筛除已经过期且不再使用的渲染引擎版本,释放相关的静态资源空间。
备注:上面这个方案AI评估技术成本较高,属于理论可行但实践存疑的方案~
# 前端部署流程
目前主要有 debug、test、pre、opn、online 这五个环境,debug是研发自测联调环境,test是测试环境,pre是预发环境,opn是仿真环境,online是线上生产环境
本地项目开发联调完毕,
git commit提交,git push推送到线上代码仓库;开发分支自动编译,编码规范检查;编译通过后 Code Review 评审;评审通过可将提交代码合并到线上开发分支;
之后将开发分支合并到部署分支,比如debug环境的
DEBUG分支,自动会触发已经配置好的流水线编译任务;执行的其实是项目根目录下的
ci.yml配置好的命令,这个文件是环境部署编译的入口,这里会配置好不同name具体需要执行的command,比如:
Default:
profile: [editor] # 默认执行的name
Profiles:
- profile:
name: editor # 低码编辑端
mode: AGENT
environment:
image: DECK_STD_CENTOS7 # 镜像
tools: # 工具
- nodejs: 20.10.0
- yarn: 1.22.4
- pnpm: 10.0.0
build:
command: pnpm install && pnpm deploy:editor # 具体执行命令
artifacts:
release: true
cache:
enable: true
paths:
- node_modules
check:
- enable: true
reuse: TASK
流水线中如果配置的name是
editor,那么就会执行command:pnpm install && pnpm deploy:editor;
- 上面配置的命令是先通过 pnpm 安装依赖,之后执行项目根目录下
package.json中的deploy:editor命令:
"build:editor": "rimraf -rf dist/editor && pnpm --filter editor build",
"deploy:editor": "pnpm build:editor && bash ./deploy.sh editor",
可以看到,deploy:editor 执行的是上面的 build:editor 命令,它是先删除 dist 下面的 editor 目录,然后通过
pnpm --filter editor执行packages/editor/项目下package.json中的build命令:
"build": "tsc && vite build" // 这条命令就是通过 vite 进行打包
需要提前在editor项目中的
vite.config.ts中配置outDir: '../../dist/editor', 也就是打包产物输出到根目录下的dist目录下
- 打包完成后会在根目录下的
dist/editor目录下生成打包后的静态资源,然后执行bash ./deploy.sh editor命令:
# deploy.sh
# 部署单个项目的函数
deploy_project() {
local project=$1 # 获取传入的项目名称
echo "开始部署 $project 项目到生产环境..."
# 检查构建结果
if [ ! -d "dist/$project" ]; then
echo "构建失败,dist/$project 目录不存在!"
exit 1
fi
# 连接服务器并部署
echo "正在部署到服务器..."
# 上传dist目录下的所有文件到服务器
echo "正在上传文件..."
rm -rf output # 删除服务器上的output目录
mkdir output # 新建output目录
cp -rf dist/$project/* output # 将dist目录下项目目录的文件复制到output目录下
echo "$project 部署完成!"
}
# 根据参数部署对应项目
case $PROJECT in
"web")
deploy_project "web"
;;
"editor")
deploy_project "editor"
;;
esac
这个脚本就是将打包后的静态资源复制到对应环境服务器的指定目录上
- 至此前端就完成了项目的打包和环境部署;
FCnap到这里就可以了,Opera部署的项目,还需要将生成的产物打包生成版本号;之后流水线中【上线】操作则是通过这个版本号拿到产物,部署到服务器上;在整个流流水线执行过程中QA可以添加数据采集、部署前检查等任务,用于限制非窗口期,或非测试状态的需求部署~
# 后端服务部署流程
后端Node服务的部署跟上面前端类似,都是有好几个环境,都需要推送代码到线上仓库,合并,指定对应流水线
- 首先流水线编译时执行的
ci.yml中的命令:
- profile:
name: serverTest # opera 部署 test
build:
command: sh buildserver.sh test
- 上面的command是直接执行
sh buildserver.sh test:
# buildserver.sh
# 获取部署项目参数
build_command=$1
echo "build_command: $build_command"
# 根据 build_command 设置 NODE_ENV 环境变量
if [ -n "$build_command" ]; then
export NODE_ENV=$build_command
echo "设置环境变量 NODE_ENV=$NODE_ENV"
else
export NODE_ENV="test"
echo "未指定环境,默认使用 test 环境"
fi
# 目录
DIR=$(cd $(dirname $0);pwd) # 当前脚本所在目录
SERVER_DIR=$DIR/server # 服务端代码目录
OUTPUT_WORKSPACE_DIR=$DIR/output/workspace # 输出目录
OPERA_MODULE_DIR=$OUTPUT_WORKSPACE_DIR/output # 输出目录
# 清理工作目录
rm -rf $DIR/output $DIR/dist
mkdir -p $OUTPUT_WORKSPACE_DIR # 创建输出目录
# 切换到 server 目录
cd $SERVER_DIR
# 下载依赖
yarn install --ignore-scripts --prefer-offline --registry=http://registry.npm.baidu-int.com --production
# 进入到服务器输出目录,下载打包工具
cd $OUTPUT_WORKSPACE_DIR
# 下载打包工具(baidu/apaas/package-template)的最新发布版本
curl "http://agile.baidu.com/api/agile/ge...."
# 创建输出目录
mkdir -p $OPERA_MODULE_DIR/package
# 创建环境配置文件(服务器受限,不能将配置文件复制过去,只能手动创建)
cat > $SERVER_DIR/env-config.js << EOF
// 此文件由构建脚本自动生成,请勿手动修改
process.env.NODE_ENV = '$NODE_ENV';
console.log('环境变量已从构建时注入: NODE_ENV=$NODE_ENV');
EOF
# 将环境变量硬编码到打包文件中
echo "require('./env-config.js');" > temp_header # 创建临时文件
cat app.js >> temp_header # 合并文件
mv temp_header app.js # 替换文件
# 将项目源码复制到输出目录
cp -rL app.js config.js controller error main.js package.json pm2.config.js router service sql data utils env-config.js $OPERA_MODULE_DIR/package
# 将 node_modules 目录递归复制到 output 目录下,-r 表示递归复制,-f 表示强制覆盖已存在的文件
cp -rf node_modules $OPERA_MODULE_DIR/package
# 打成指定包opera-module.tgz
tar zcf $DIR/output/opera-module.tgz -C $OPERA_MODULE_DIR .
# 清理工作目录
rm -rf $OUTPUT_WORKSPACE_DIR
上面的脚本执行命令比较复杂,大致就是打包,拷贝代码,然后打包成tar.gz文件;之后流水线执行【发布】命令生成版本号;之后通过流水线执行【上线】操作,将对应版本号的压缩包解压部署到对应服务器上;Opera平台好像针对Node服务会自动读取
pm2.config.js启动服务,不需要脚本中显式启动;如果是Fcnap中部署Node服务的话,前面流程都差不多,推送代码 => 打包 => 拷贝文件,除此外项目根目录下还需要新建
Dockerfile文件:
# Dockerfile
ARG NODE_VERSION=iregistry.baidu-int.com/ee-fe/node:20.1.0-alpine3.17 # 基础镜像
FROM $NODE_VERSION AS intermediate # 临时镜像,用于拷贝代码
COPY output/ /app/ # 上一步build.sh输出的output/ 拷贝到 当前服务器 /app 上
RUN mkdir -p /app/logs # 打印当前目录和目录下的文件
RUN chown -R node:node /app && chmod -R 777 /app # 更改用户和权限
FROM $NODE_VERSION AS runner # 实际镜像,用于运行
WORKDIR /app # 设置工作目录
COPY --from=intermediate /app /app # 从临时镜像中拷贝代码
ENV NODE_ENV=production # 设置环境变量
ENV TZ=Asia/Shanghai # 设置时区
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # 设置时区
# 添加健康检测(解决fcnap部署失败)
# 容器应用的pod不断重启,无法部署成功; 考虑容器是否未向外暴露端口,如果没有,健康检测会认为容器没启动成功然后重启。
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD wget -q --spider http://localhost:9527/health || exit 1
USER node
EXPOSE 9527 # 暴露端口
CMD ["yarn", "prd"] # 启动命令
- 后端服务中
package.json配置prd命令:
// package.json
"prd": "pm2-runtime start pm2.config.js" // 通过pm2启动服务
// pm2.config.js
module.exports = {
apps: [
{
name: 'LowcodeNodeServer', // 应用名称
script: './app.js' // 入口文件
}
]
};
用 pm2 来进行服务管理
- 之后在
service层通过const connection = require('../sql/index')引入mysql2配置,进行数据库初始化,主要是通过传入的host,port,user,password,database进行数据库连接:
const pool = mysql2.createPool({
...sqlParams,
dateStrings: true, // 日期作为字符串返回
connectionLimit: 10, // 连接池最大连接数
queueLimit: 0, // 排队限制,0 表示不限制
waitForConnections: true, // 当没有可用连接时是否等待
enableKeepAlive: true, // 启用 TCP keepalive
keepAliveInitialDelay: 0 // keepalive 初始延迟
});
- 之后需要调数据库时则通过
const [result] = await connection.execute('SELECT * FROM lowcode_user_table WHERE 1=1', params)进行查询操作;
整体流程:ci.yml => buildserver.sh => Dockerfile => pm2启动Node服务 => 数据库创建连接,配置初始化
# 平台监控
公司有公共的监控平台,先申请平台id,然后再平台通过代码接入,统计页面加载性能指标,在平台查看数据。