声明:文章为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
大家好啊!本文来到了实战前端发布平台的最后一个阶段,实战前端获取Jenkins构建日志。在这里!你有机会知道全栈 webSocket 怎么玩,掌握 webSocket 的实战应用场景和技能!话不多说,赶紧往下看。
系列文章:
- 总览前端自动化部署流程,如何实现前端发布平台?文章链接
- 前端发布平台
node server
实战!文章链接 - 前端发布平台
jenkins
实战!如何实现前端自动化部署?文章链接 - 前端发布平台全栈实战(前后端开发完整篇)!开发一个前端发布平台 文章链接
websocket
全栈实战,实现唯一构建实例 + 日志同步
回顾之前的实现,我们已经把整个发布平台的自动化部署链路给跑通了,还差一丢丢就算完成整个发布平台的开发了。所以这一篇,我们接着之前的实战进行。本文主旨:
- 前端发起 jenkins 构建
- 实战
websocket
获取 jenkins 构建日志
快速看源码
一、前端构建页面
回顾当前的前端界面实现:
从上一篇文章中,笔者已经实现了整个构建配置信息的 CRUD
了,那接下来就是要在前端发起 jenkins 构建了。这里笔者是这样想的,用户在当前 table界面
点击对应的配置信息,进入配置详情页面,然后在配置详情页面中完成构建等操作。话不多说,马上进入配置详情页面的实战!
1. 前端动态路由配置
思考一下:其实配置详情的入口就在 table
中,所以完全可以根据每一个配置的唯一标识(如id),再配合使用前端动态路由来实现 配置详情页 的需求。
首先我们对 table
中的项目名称进行改动,实现其可点击:
<el-table-column label="项目名称" prop="projectName">
<!-- 添加 @click ,传入 rowData,点击后通过 id 进入配置详情 -->
<template #default="scope">
<el-button
type="primary"
link
@click="handleToDetail(scope.row)"
>
{{ scope.row.projectName }}
</el-button>
</template>
</el-table-column>
复制代码
此时的界面效果如下:
紧接着,需要在路由文件中配置一下动态路由的配置,并在 pages
目录中新增一个 ConfigDetail
的文件夹:
- 配置动态路由:
ConfigDetail
{ path: '/configDetail/:id', component: () => import('@/pages/ConfigDetail/index.vue'), name: 'ConfigDetail' }, 复制代码
- 新增
ConfigDetail组件
: - 实现跳转函数
handleToDetail
:const handleToDetail = (rowData) => { // rowData 就是传递进来的参数 router.push({ name:"ConfigDetail", params:{ id: rowData._id }} ) } 复制代码
上述步骤完成后,已经实现了从 table 界面点击后,进入到配置详情界面了:
这里,我们思考一下,配置详情页面需要什么,然后按照需求点再去设计配置详情界面。跟着笔者接着往下走~
2. 配置详情页
首先先整理一下配置详情页面的需求点:
- 展示配置信息
- 发起构建
- 显示构建进度(打印构建日志)
- 编辑构建信息
- ...
整个配置详情页如果要做得完善(用户体验好),功能点还是很多的。不过本文只围绕核心点去实现,所以上述的1-3点将是本文实现的重点。笔者顺序,一步步实现整个配置详情页面。
首先是展示配置信息。上文提到的通过 配置id 的切换实现前端动态路由来展示配置详情界面,所以我们可以通过 url
中获取到当然的 配置id,当然,我们也需要在后端中实现 配置信息详情 的查询(之前只实现了数据的分页查询)。
笔者在这里先快马加鞭的把后端接口给实现了:
- 新增查询 配置详情 的
接口
:// 在路由文件中新增 /jobDetail 的接口 router.get('/jobDetail', controller.getConfigDetail) 复制代码
- 实现
getConfigDetail
函数:
其中export async function getConfigDetail (ctx, next) { try { // 获取 id(从前端带上来) const { id } = ctx.request.query // 通过 id 查询数据(调用 service 层) const data = await services.findJobDetail(id) // 返回数据 ctx.state.apiResponse = { code: RESPONSE_CODE.SUC, data } } catch (e) { ctx.state.apiResponse = { code: RESPONSE_CODE.ERR, msg: '配置详情查询失败' } } next() } 复制代码
services.findJobDetail
的实现也是很简单,直接通过 mongoose 的api
:export function findJobDetail (id) { return JobModel.findById(id) } 复制代码
后端接口实现后,还是老样子,通过 postman 测试一下接口:
后端接口实现后,就可以进入到前端界面开发了。只需要在组件 onMounted
阶段发起请求,并将返回的数据丢到一个响应式对象中即可。具体的前端代码实现笔者就跳过了,因为相对比较简单,就是 vue + element-plus
一把梭。直接看一下当前的界面效果:
如上图所示,整个配置详情页分为三块。第一块是配置信息区域;第二块是操作区域;第三块是日志区域。到这一阶段整个前端发起构建的准备工作就完成了,接下来我们进入一个阶段: 实战 webSocket。
二、实战 webSocket
实战 webSocket
阶段,最为主要的就是为了在前端实时显示出 jenkins 的当前构建日志。回顾之前实战 node + jenkins 的文章中,笔者那时候是通过一个 build
的 post
接口去实现触发 jenkins 的构建的。我们这一步接着在之前的基础上继续完善。
1. 初始化 Socket
首先!先装包!!给前后端项目都安装一个 socket.io(v4) 的包~
- 前端项目:
pnpm install socket.io-client 复制代码
- 后端项目:
pnpm install socket.io 复制代码
安装完成后,我们先要初始化 Socket 并测试一下能否正常通信。万事开头难!只要联通了前后端通信,后续只需针对业务逻辑码业务代码而已(也就是各种 on
、 emit
!!!如下图所示),所以初始化这块要去踩踩坑啦,看文档去~
-
前端初始化:
由于整个构建步骤在配置详情页中进行,所以笔者这里直接在配置详情页
onMounted
阶段开始连接socket
。const initSocket = () => { const { id } = route.params const ioInstance = io( { // 后端也会实现一个 /jenkins/build 的route path: '/jenkins/build', query: { id // 把当前的配置 id 带给后端 } }) // 初始化成功后,可以通过 on('xxx') 接收后端 emit 的事件、数据 ioInstance.on('', function () {}) } 复制代码
-
后端初始化:
上面前端初始化中有提到后端也要实现一个
/jenkins/build
的route
,因此后端的可以在路由目录中编写初始化代码,再在入口文件中import
后执行。跟之前我们实现的普通接口的路由初始化逻辑类似~export default function initBuildSocket (httpServer) { // 实现 /jenkins/build 的route const io = new Socket(httpServer, { path: '/jenkins/build', }); // 当触发连接事件时执行 controller.socketConnect io.on('connection', controller.socketConnect) } 复制代码
controller.socketConnect
中,我们可以在参数中拿到socket
的实例,因此,我们就可以通过socket实例
的on
、emit
监听、触发事件跟前端实现通信了!// controler 层的 socketConnect 伪代码实现 export function socketConnect (socket) { // 这里打印一下以验证成功连接 console.log('connection suc'); socket.on('', function () {}) } 复制代码
这样,大概就是整个前后端的 sokcet.io
初始化了,但是你以为就此完了吗?当然没有。需要注意的是,目前前后端的 server 端口号是不同的,所以会存在跨域问题,所以还需要解决一下开发环境的跨域问题。当然,笔者这里就以开发环境的场景进行配置前端的 devServer
,至于如果是要发布到生产上的话,还需要自行配置一些 cors
的配置的~
之前在实现前端调用后端接口的时候,已经在前端项目的 vite.config
中配置了以 '/api'
为标识的 proxy
规则,所以我们目前需要对 '/jenkins'
标识进行 proxy 配置:
如上图所示,这里需要给 /jenkins
为首的请求路径也加上 proxy
配置就能解决 ws
的跨域问题啦。
'/jenkins': {
target: 'http://localhost:3200',
changeOrigin: true
}
复制代码
一切准备工作完成,启动前端项目看看效果:
- netWork 的 fetch/xrh 界面出现了多条
http
的轮询请求 - ws 界面出现了我们的请求连接
'/jenkins/build'
- Node调试工具成功打印
'connection suc'
2. Sokcet 同步构建日志
这一步可以说就是整个前端实战中的核心部分了,所以!不多说,直接进入实战。之前在 node 端触发 jenkins 构建是提供了一个 post
接口给 postman
调用模拟前端触发的,而这里我们直接通过 socket
去触发,并且完成构建日志同步。
于是,笔者这里对构建触发进行改写,从原来的 build
接口迁移到 socket 这里触发。具体的构建流程笔者就不在这里展开了,如果想详细了解、回顾的话,可以回到笔者的 第三篇文章node+jenkins实战构建中详细查看 build
的实现。
简要回顾一下之前的代码:
上述代码是 触发jenkins构建 的核心代码,我们在
build
中实现了 触发构建 、 拿到构建number 、 获取构建日志 的功能。在之前的基础上,开始实战 socket 触发构建的流程!
// 前端代码实现
const handleBuild = () => {
// 点击构建按钮后 emit 'build:start'
ioInstance.value.emit('build:start')
}
// 后端代码实现
socket.on('build:start', async function () {
// 监听到前端 'build:start' 事件执行构建
console.log('build start');
const jobName = 'test-config-job'
// 根据 id 查询构建配置
const config = await jobConfig.findJobById(id)
// 配置 jenkins job
await jenkins.configJob(jobName, config)
// 触发 jenkins 构建,拿到 buildNumber 和 logStream 实例
const { buildNumber, logStream } = jenkins.build(jobName) // 上图的 build 方法
console.log('buildNumber', buildNumber)
})
复制代码
到这一步,我们先验证一下前端点击构建按钮后能否正常触发 jenkins 构建并且拿到 buildNumber
。
点击后结果如图所示,可以成功获取
buildNumber
和控制台中成功打印出构建日志:
既然成功触发构建,也就意味着我们现在只需要把 logStream
的输出,通过 socket
交互,传输到前端就可以实现我们的需求了。稍微改造一下刚才的构建代码:
// 前端代码 ----------------------------------
// 1. 新增一个 ref 数据来接收日志信息
const stream = ref('')
// 2. 在按钮点击后 执行 initLogStream 初始化接受 socket 事件
const initLogStream = () => {
ioInstance.value.on('build:data', function (data) {
// 将收到的日志信息赋值给 stream
stream.value = data
})
ioInstance.value.on('build:error', function (err) {})
ioInstance.value.on('build:end', function () {})
}
// 3. 最后将 stream 数据放到 <pre> 中进行展示
<pre>{{ stream }}</pre>
// 后端代码 ----------------------------------
// 这里接着前文 socket.on('build:start') 的代码
const { buildNumber, logStream } = await jenkins.build(jobName)
// 拿到 logStream 实例
logStream.on('data', function(text) {
console.log(text);
// 这里通过 socket 将日志信息 emit 出去
socket.emit('build:data', text)
});
logStream.on('error', function(err) {
console.log('error', err);
// 这里通过 socket 将错误信息 emit 出去
socket.emit('build:error', err)
});
logStream.on('end', function() {
console.log('end');
// 这里通过 socket 将结束节点 emit 出去
socket.emit('build:end')
});
复制代码
完成 socket 接收日志后,再次点击构建看看效果!
如图所示,成功在前端界面中展示构建日志信息,这样一来,使用发布平台的用户就能实时获取到当前项目的构建进度、构建状态、构建错误的提示信息等等...这一步总算大功告成啦,紧接着我们进入最后一个阶段,实现全局唯一的构建实例。
三、全局唯一构建实例
什么叫全局唯一构建实例呢!可能很多小伙伴第一次读到这句的时候都是懵的!(当然,这都不是你们的问题,是笔者的语言功底太菜了)笔者现在展开说说:jenkins 同个 free style job
一次就执行一个,即使连续发起同个 job 的构建,当前只有一个构建任务执行,其余都在队列中等待。基于此,如果同个项目配置在构建中,所有进入到这个构建配置的前端页面的用户应该都是看到相同的状态。好吧,笔者也说不下去了,赶紧画个图!
如上图所示,如果当前项目正在构建中,那在不同终端打开这个配置的界面时,应该展示的是同样的信息、同样的构建进度!很显然,当前的实现是没办法做到这一点的,因为我们没有对构建状态进行一个统一的管理,所以多开浏览器tab看到的前端界面是不一致的:
那接下来,我们接着改造我们实战代码去实现这个功能!先来捋一捋思路,首先我们要自己掌控构建状态,因此每个配置构建时都应该生成一个实例,实例有一个构建中的状态;其次我们需要把构建的状态进行一个保存;然后让每个接入的终端能找到当前的构建状态。我们当前的 node端 并没有 fork
子进程,所以我们可以简单地在全局中维护一个 map
数据,通过 配置id 作为 key
,构建实例 作为 value
,把每一个构建中的状态保存到全局唯一的 map
中即可实现这个需求~
那接下来,马上进入实战阶段相比之前的实战比较有技术难度的一环!
首先我们先实现一个 构建类(构建实例的构造器):
// 每个构建配置只生成一个 build 实例
export default class Build {
constructor (id, delBuilderFn) {
this.id = id // 配置 id
this.isBuilding = false // 构建状态
this.logStream = null // 存放 logStream 实例
this.logStreamText = '' // 存放构建日志(防止构建中途进来的用户丢失之前的构建日志)
this.buildNumber = '' // 存放构建 number
this.delBuilderFn = delBuilderFn // 删除存在 map 中的实例
}
async build (socket) {
this.isBuilding = true // 改变构建状态
/* 这一堆都是构建代码,大家都很熟悉了就不再展开了 */
const jobName = 'test-config-job'
const config = await jobConfig.findJobById(this.id)
await jenkins.configJob(jobName, config)
const { buildNumber, logStream } = await jenkins.build(jobName)
this.buildNumber = buildNumber
this.logStream = logStream
/* /这一堆都是构建代码,大家都很熟悉了就不再展开了 */
this.logStream.on('data', (text) => {
// 这里只有在触发构建的时候执行一次
// 保证不会因为多个相同监听造成 logStreamText 叠加问题
this.logStreamText += text // 整个构建实例唯一日志str保存
});
// 初始化日志同步前端
this.initLogStream(socket)
}
stop () {}
initLogStream (socket) {
if (!this.logStream) return
// 注意:这里 socket 是保存到闭包里面的
this.logStream.on('data', () => {
socket.emit('build:data', this.logStreamText)
});
this.logStream.on('error', (err) => {
socket.emit('build:error', err)
this.isBuilding = false // 改变构建状态
this.delBuilderFn(this.id) // 删除 map 缓存
});
this.logStream.on('end', () => {
socket.emit('build:end')
this.isBuilding = false // 改变构建状态
this.delBuilderFn(this.id) // 删除 map 缓存
});
}
destroy () {
// 等着被GC吧
this.id = null
this.isBuilding = null
this.logStream = null
this.logStreamText = null
this.buildNumber = null
}
}
复制代码
相关核心点:
Build类
笔者定义为管理整个构建生命周期的类,它的实例在整个构建的生命周期中只会创建唯一一个。initLogStream
。其接收一个 socket实例 作为参数,这里笔者只是想通过闭包保存当前的 socket实例。这样可以保证 n个socket接入 时跟多个客户端维持多对多的 socket 关系,保证每个客户端的socket都能收到相应的数据。
紧接着,我们需要实现一个 构建管理类 :
import Build from './index' // 引入构建类
class Admin {
constructor() {
this.map = {} // 构建实例存放的 map
}
getBuilder (id, socket) {
// 判断是否已经存在构建实例
if (Reflect.has(this.map, id)) {
// 注意⚠️,这里会调用 initLogStrea 并传入 socket(socket会被闭包保存)
this.map[id].initLogStream(socket)
return this.map[id]
}
// 不存在则新建构建实例
return this.createBuilder(id)
}
createBuilder (id) {
// 实例化构建类,传入 id 和 删除函数
const builder = new Build(id, this.delBuilder.bind(this))
this.map[id] = builder
return builder
}
delBuilder (id) {
// 调用构建实例的 destroy 方法
this.map[id] && this.map[id].destroy()
// 清除实例在 map 的缓存
Reflect.deleteProperty(this.map, id)
}
}
export default new Admin()
复制代码
相关核心点:
- 通过
Admin
类全局管理Build实例
。创建、删除Build
实例都在这里进行 - 每个
socket
连接时调用getBuilder()
,获取全局唯一构建实例,并且同步构建日志也是这里处理的。
完成这两个类的编写后,我们简单的改写一下之前的触发构建的代码,改动后如下:
export function socketConnect (socket) {
console.log('connection suc');
const { id } = socket.handshake.query
// 通过 adminInstance 获取构建实例
const builder = adminInstance.getBuilder(id, socket)
socket.on('build:start', async function () {
console.log('build start');
// 构建代码通过上述获得的 builder 调用 build 方法
await builder.build(socket)
})
}
复制代码
代码打完之后,激动的我赶紧打开了一个页面,点击构建后再打开一个新的页面,这时候!!两个页面的构建日志同时同步输出了(完美)!效果如图所示:
到这里为止,整个 webSocket 实战获取 jenkins 构建日志的内容就讲完了,不知道是否能给大家带来一丝丝的收获,反正自己是写麻了~这里还是要重点提醒一下,整篇文章笔者个人认为最最最难的部分就是全局构建实例这里,如果大家自己也想实战开发的话一定要注意如何处理多个socket连接的问题;构建类、管理构建类的设计等等,因为笔者这里只是 demo 级别的。再提一次,多个 socket实例 笔者本文是通过闭包存起来的,可能有些隐晦,大家自己干的时候得注意一下~
写在最后
到这一步,整个全栈开发前端发布平台的核心功能就算是完工了。现在我们已经可以实现在前端创建、编辑配置、发起构建、获取构建日志... ...当然,笔者的实战代码都是 demo 级别的,如果是需要开发企业级别的还是要多点的设计,然后注意内存的回收问题(我这 demo 基本没怎么处理内存回收,很可能有内存泄露的问题哈哈哈)各种各样的问题吧。然后好像还有很多功能点都没讲到,不过其实掌握了核心功能后,很多细节点都可以自己去完善了,比如停止构建、回滚这种,你觉得呢!!!好啦,本文到这里吧就,后会有期~