全栈 webSocket 实战获取 jenkins 构建日志
声明:文章为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
大家好啊!本文来到了实战前端发布平台的最后一个阶段,实战前端获取Jenkins构建日志。在这里!你有机会知道全栈 webSocket 怎么玩,掌握 webSocket 的实战应用场景和技能!话不多说,赶紧往下看。
系列文章:
总览前端自动化部署流程,如何实现前端发布平台?文章链接前端发布平台node server实战!文章链接前端发布平台jenkins实战!如何实现前端自动化部署?文章链接前端发布平台全栈实战(前后端开发完整篇)!开发一个前端发布平台文章链接websocket全栈实战,实现唯一构建实例+日志同步回顾之前的实现,我们已经把整个发布平台的自动化部署链路给跑通了,还差一丢丢就算完成整个发布平台的开发了。所以这一篇,我们接着之前的实战进行。本文主旨:
前端发起jenkins构建实战websocket获取 jenkins 构建日志快速看源码前端项目仓库后端项目仓库一、前端构建页面回顾当前的前端界面实现:
从上一篇文章中,笔者已经实现了整个构建配置信息的CRUD了,那接下来就是要在前端发起jenkins构建了。这里笔者是这样想的,用户在当前table界面点击对应的配置信息,进入配置详情页面,然后在配置详情页面中完成构建等操作。话不多说,马上进入配置详情页面的实战!
1. 前端动态路由配置思考一下:其实配置详情的入口就在table中,所以完全可以根据每一个配置的唯一标识(如id),再配合使用前端动态路由来实现配置详情页的需求。
首先我们对table中的项目名称进行改动,实现其可点击:
el-table-columnlabel="项目名称"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:consthandleToDetail=(rowData)={
//rowData就是传递进来的参数
router.push({
name:"ConfigDetail",
params:{
id:rowData._id
}}
)
}
上述步骤完成后,已经实现了从 table 界面点击后,进入到配置详情界面了:
这里,我们思考一下,配置详情页面需要什么,然后按照需求点再去设计配置详情界面。跟着笔者接着往下走~
2. 配置详情页首先先整理一下配置详情页面的需求点:
展示配置信息发起构建显示构建进度(打印构建日志)编辑构建信息...整个配置详情页如果要做得完善(用户体验好),功能点还是很多的。不过本文只围绕核心点去实现,所以上述的1-3点将是本文实现的重点。笔者顺序,一步步实现整个配置详情页面。
首先是展示配置信息。上文提到的通过配置id的切换实现前端动态路由来展示配置详情界面,所以我们可以通过url中获取到当然的 配置id,当然,我们也需要在后端中实现配置信息详情的查询(之前只实现了数据的分页查询)。
笔者在这里先快马加鞭的把后端接口给实现了:
新增查询 配置详情 的接口://在路由文件中新增/jobDetail的接口
router.get('/jobDetail',controller.getConfigDetail)
实现getConfigDetail函数:exportasyncfunctiongetConfigDetail(ctx,next){
try{
//获取id(从前端带上来)
const{id}=ctx.request.query
//通过id查询数据(调用service层)
constdata=awaitservices.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:exportfunctionfindJobDetail(id){
returnJobModel.findById(id)
}
后端接口实现后,还是老样子,通过postman测试一下接口:
后端接口实现后,就可以进入到前端界面开发了。只需要在组件onMounted阶段发起请求,并将返回的数据丢到一个响应式对象中即可。具体的前端代码实现笔者就跳过了,因为相对比较简单,就是vue + element-plus一把梭。直接看一下当前的界面效果:
如上图所示,整个配置详情页分为三块。第一块是配置信息区域;第二块是操作区域;第三块是日志区域。到这一阶段整个前端发起构建的准备工作就完成了,接下来我们进入一个阶段:实战 webSocket。
二、实战 webSocket实战webSocket阶段,最为主要的就是为了在前端实时显示出jenkins的当前构建日志。回顾之前实战node + jenkins的文章中,笔者那时候是通过一个build的post接口去实现触发 jenkins 的构建的。我们这一步接着在之前的基础上继续完善。
1. 初始化 Socket首先!先装包!!给前后端项目都安装一个socket.io(v4)的包~
前端项目:pnpminstallsocket.io-client
后端项目:pnpminstallsocket.io
安装完成后,我们先要初始化 Socket 并测试一下能否正常通信。完事开头难!只要联通了前后端通信,后续只需要针对业务逻辑码业务代码而已(也就是各种on、emit!!!如下图所示),所以初始化这块要去踩踩坑啦,看文档去~
image.png前端初始化:
由于整个构建步骤在配置详情页中进行,所以笔者这里直接在配置详情页onMounted阶段开始连接socket。
constinitSocket=()={
const{id}=route.params
constioInstance=io({
//后端也会实现一个/jenkins/build的route
path:'/jenkins/build',
query:{
id//把当前的配置id带给后端
}
})
//初始化成功后,可以通过on('xxx')接收后端emit的事件、数据
ioInstance.on('',function(){})
}
后端初始化:
上面前端初始化中有提到后端也要实现一个/jenkins/build的route,因此后端的可以在路由目录中编写初始化代码,再在入口文件中import后执行。跟之前我们实现的普通接口的路由初始化逻辑类似~
exportdefaultfunctioninitBuildSocket(httpServer){
//实现/jenkins/build的route
constio=newSocket(httpServer,{
path:'/jenkins/build',
//当触发连接事件时执行controller.socketConnect
io.on('connection',controller.socketConnect)
}
controller.socketConnect中,我们可以在参数中拿到socket的实例,因此,我们就可以通过socket实例的on、emit监听、触发事件跟前端实现通信了!
//controler层的socketConnect伪代码实现
exportfunctionsocketConnect(socket){
//这里打印一下以验证成功连接
console.log('connectionsuc');
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的轮询请求image.pngws 界面出现了我们的请求连接'/jenkins/build'image.pngNode调试工具成功打印'connection suc'image.png2. Sokcet 同步构建日志这一步可以说就是整个前端实战中的核心部分了,所以!不多说,直接进入实战。之前在node端触发jenkins构建是提供了一个post接口给postman调用模拟前端触发的,而这里我们直接通过socket去触发,并且完成构建日志同步。
于是,笔者这里对构建触发进行改写,从原来的build接口迁移到socket这里触发。具体的构建流程笔者就不在这里展开了,如果想详细了解、回顾的话,可以回到笔者的第三篇文章node+jenkins实战构建中详细查看build的实现。
简要回顾一下之前的代码:上述代码是触发jenkins构建的核心代码,我们在build中实现了 触发构建 、 拿到构建number 、 获取构建日志 的功能。在之前的基础上,开始实战 socket 触发构建的流程!
//前端代码实现
consthandleBuild=()={
//点击构建按钮后emit'build:start'
ioInstance.value.emit('build:start')
}
//后端代码实现
socket.on('build:start',asyncfunction(){
//监听到前端'build:start'事件执行构建
console.log('buildstart');
constjobName='test-config-job'
//根据id查询构建配置
constconfig=awaitjobConfig.findJobById(id)
//配置jenkinsjob
awaitjenkins.configJob(jobName,config)
//触发jenkins构建,拿到buildNumber和logStream实例
const{buildNumber,logStream}=jenkins.build(jobName)//上图的build方法
console.log('buildNumber',buildNumber)
})
到这一步,我们先验证一下前端点击构建按钮后能否正常触发jenkins构建并且拿到buildNumber。点击后结果如图所示,可以成功获取buildNumber和控制台中成功打印出构建日志:
既然成功触发构建,也就意味着我们现在只需要把logStream的输出,通过socket交互,传输到前端就可以实现我们的需求了。稍微改造一下刚才的构建代码:
//前端代码----------------------------------
//1.新增一个ref数据来接收日志信息
conststream=ref('')
//2.在按钮点击后执行initLogStream初始化接受socket事件
constinitLogStream=()={
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}=awaitjenkins.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一次就执行一个,连续发起构建其他的构建任务就只会先排队。基于此,如果同个项目配置在构建中,所有进入到这个构建配置的前端页面的用户应该都是看到相同的状态。ha好吧,笔者也说不下去了,赶紧画个图!
全局唯一实例.png如上图所示,如果当前项目正在构建中,那在不同终端打开这个配置的界面时,应该展示的是同样的信息!很显然,当前的实现是没办法做到这一点的,因为我们没有对构建的状态啥的进行一个统一的管理,所以多开tab的时候,看到的前端界面是不一致的:
那接下来,我们接着改造我们实战代码去实现这个功能!先来捋一捋思路,首先我们需要把构建的状态进行一个保存;然后让每个接入的终端能找到当前的构建状态。我们当前的 node端 并没有fork子进程,所以我们可以简单地在全局中维护一个map数据,通过配置id作为key,构建实例作为value,把每一个构建中的状态保存到全局唯一的map中即可实现这个需求~
那接下来,马上进入实战阶段相比之前的实战比较有技术难度的一环!
首先我们先实现一个构建类(构建实例的构造器):
//每个构建配置只生成一个build实例
exportdefaultclassBuild{
constructor(id,delBuilderFn){
this.id=id//配置id
this.isBuilding=false//构建状态
this.logStream=null//存放logStream实例
this.logStreamText=''//存放构建日志(防止构建中途进来的用户丢失之前的构建日志)
this.buildNumber=''//存放构建number
this.delBuilderFn=delBuilderFn//删除存在map中的实例
}
asyncbuild(socket){
this.isBuilding=true//改变构建状态
/*这一堆都是构建代码,大家都很熟悉了就不再展开了*/
constjobName='test-config-job'
constconfig=awaitjobConfig.findJobById(this.id)
awaitjenkins.configJob(jobName,config)
const{buildNumber,logStream}=awaitjenkins.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都能收到相应的数据。紧接着,我们需要实现一个构建管理类:
importBuildfrom'./index'//引入构建类
classAdmin{
constructor(){
this.map={}//构建实例存放的map
}
getBuilder(id,socket){
//判断是否已经存在构建实例
if(Reflect.has(this.map,id)){
//注意??,这里会调用initLogStrea并传入socket(socket会被闭包保存)
this.map[id].initLogStream(socket)
returnthis.map[id]
}
//不存在则新建构建实例
returnthis.createBuilder(id)
}
createBuilder(id){
//实例化构建类,传入id和删除函数
constbuilder=newBuild(id,this.delBuilder.bind(this))
this.map[id]=builder
returnbuilder
}
delBuilder(id){
//调用构建实例的destroy方法
this.map[id]this.map[id].destroy()
//清除实例在map的缓存
Reflect.deleteProperty(this.map,id)
}
}
exportdefaultnewAdmin()
相关核心点:
通过Admin类全局管理Build实例。创建、删除Build实例都在这里进行每个socket连接时调用getBuilder(),获取全局唯一构建实例,并且同步构建日志也是这里处理的。完成这两个类的编写后,我们简单的改写一下之前的触发构建的代码,改动后如下:
exportfunctionsocketConnect(socket){
console.log('connectionsuc');
const{id}=socket.handshake.query
//通过adminInstance获取构建实例
constbuilder=adminInstance.getBuilder(id,socket)
socket.on('build:start',asyncfunction(){
console.log('buildstart');
//构建代码通过上述获得的builder调用build方法
awaitbuilder.build(socket)
})
}
代码打完之后,激动的我赶紧打开了一个页面,点击构建后再打开一个新的页面,这时候!!两个页面的构建日志同时同步输出了(完美)!效果如图所示:
到这里为止,整个 webSocket 实战获取 jenkins 构建日志的内容就讲完了,不知道是否能给大家带来一丝丝的收获,反正自己是写麻了~这里还是要重点提醒一下,整篇文章笔者个人认为最最最难的部分就是全局构建实例这里,如果大家自己也想实战开发的话一定要注意如何处理多个socket连接的问题;构建类、管理构建类的设计等等,因为笔者这里只是 demo 级别的。再提一次,多个socket实例笔者本文是通过闭包存起来的,可能有些隐晦,大家自己干的时候得注意一下~
写在最后到这一步,整个全栈开发前端发布平台的核心功能就算是完工了。现在我们已经可以实现在前端创建、编辑配置、发起构建、获取构建日志... ...当然,笔者的实战代码都是 demo 级别的,如果是需要开发企业级别的还是要多点的设计,然后注意内存的回收问题(我这 demo 基本没怎么处理内存回收,很可能有内存泄露的问题哈哈哈)各种各样的问题吧。然后好像还有很多功能点都没讲到,不过其实掌握了核心功能后,很多细节点都可以自己去完善了,比如停止构建、回滚这种,你觉得呢!!!好啦,本文到这里吧就,后会有期~
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线