likes
comments
collection

全栈 webSocket 实战获取 jenkins 构建日志

作者站长头像
站长
· 阅读数 9

大家好啊!本文来到了实战前端发布平台的最后一个阶段,实战前端获取Jenkins构建日志。在这里!你有机会知道全栈 webSocket 怎么玩,掌握 webSocket 的实战应用场景和技能!话不多说,赶紧往下看。

系列文章:

  1. 总览前端自动化部署流程,如何实现前端发布平台?文章链接
  2. 前端发布平台 node server 实战!文章链接
  3. 前端发布平台 jenkins 实战!如何实现前端自动化部署?文章链接
  4. 前端发布平台全栈实战(前后端开发完整篇)!开发一个前端发布平台 文章链接
  5. websocket 全栈实战,实现唯一构建实例 + 日志同步

回顾之前的实现,我们已经把整个发布平台的自动化部署链路给跑通了,还差一丢丢就算完成整个发布平台的开发了。所以这一篇,我们接着之前的实战进行。本文主旨:

  1. 前端发起 jenkins 构建
  2. 实战 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>

此时的界面效果如下: 全栈 webSocket 实战获取 jenkins 构建日志

紧接着,需要在路由文件中配置一下动态路由的配置,并在 pages 目录中新增一个 ConfigDetail 的文件夹:

  1. 配置动态路由:ConfigDetail
    {
      path: '/configDetail/:id',
      component: () => import('@/pages/ConfigDetail/index.vue'),
      name: 'ConfigDetail'
    },
    
  2. 新增 ConfigDetail组件全栈 webSocket 实战获取 jenkins 构建日志
  3. 实现跳转函数 handleToDetail
    const handleToDetail = (rowData) => {
      // rowData 就是传递进来的参数
      router.push({
        name:"ConfigDetail",
        params:{
          id: rowData._id
        }}
      )
    }
    

上述步骤完成后,已经实现了从 table 界面点击后,进入到配置详情界面了: 全栈 webSocket 实战获取 jenkins 构建日志

这里,我们思考一下,配置详情页面需要什么,然后按照需求点再去设计配置详情界面。跟着笔者接着往下走~

2. 配置详情页

首先先整理一下配置详情页面的需求点:

  1. 展示配置信息
  2. 发起构建
  3. 显示构建进度(打印构建日志)
  4. 编辑构建信息
  5. ...

整个配置详情页如果要做得完善(用户体验好),功能点还是很多的。不过本文只围绕核心点去实现,所以上述的1-3点将是本文实现的重点。笔者顺序,一步步实现整个配置详情页面。

首先是展示配置信息。上文提到的通过 配置id 的切换实现前端动态路由来展示配置详情界面,所以我们可以通过 url 中获取到当然的 配置id,当然,我们也需要在后端中实现 配置信息详情 的查询(之前只实现了数据的分页查询)。

笔者在这里先快马加鞭的把后端接口给实现了:

  1. 新增查询 配置详情 的接口
    // 在路由文件中新增 /jobDetail 的接口
    router.get('/jobDetail', controller.getConfigDetail)
    
  2. 实现 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 的实现也是很简单,直接通过 mongooseapi
    export function findJobDetail (id) {
      return JobModel.findById(id)
    }
    

后端接口实现后,还是老样子,通过 postman 测试一下接口: 全栈 webSocket 实战获取 jenkins 构建日志

后端接口实现后,就可以进入到前端界面开发了。只需要在组件 onMounted 阶段发起请求,并将返回的数据丢到一个响应式对象中即可。具体的前端代码实现笔者就跳过了,因为相对比较简单,就是 vue + element-plus 一把梭。直接看一下当前的界面效果: 全栈 webSocket 实战获取 jenkins 构建日志

如上图所示,整个配置详情页分为三块。第一块是配置信息区域;第二块是操作区域;第三块是日志区域。到这一阶段整个前端发起构建的准备工作就完成了,接下来我们进入一个阶段: 实战 webSocket

二、实战 webSocket

实战 webSocket 阶段,最为主要的就是为了在前端实时显示出 jenkins 的当前构建日志。回顾之前实战 node + jenkins 的文章中,笔者那时候是通过一个 buildpost 接口去实现触发 jenkins 的构建的。我们这一步接着在之前的基础上继续完善。

1. 初始化 Socket

首先!先装包!!给前后端项目都安装一个 socket.io(v4) 的包~

  • 前端项目:
    pnpm install socket.io-client
    
  • 后端项目:
    pnpm install socket.io
    

安装完成后,我们先要初始化 Socket 并测试一下能否正常通信。万事开头难!只要联通了前后端通信,后续只需针对业务逻辑码业务代码而已(也就是各种 onemit !!!如下图所示),所以初始化这块要去踩踩坑啦,看文档去~

全栈 webSocket 实战获取 jenkins 构建日志

  1. 前端初始化:

    由于整个构建步骤在配置详情页中进行,所以笔者这里直接在配置详情页 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 () {})
    }
    
  2. 后端初始化:

    上面前端初始化中有提到后端也要实现一个 /jenkins/buildroute,因此后端的可以在路由目录中编写初始化代码,再在入口文件中 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实例onemit 监听、触发事件跟前端实现通信了!

    // controler 层的 socketConnect 伪代码实现
    export function socketConnect (socket) {
      // 这里打印一下以验证成功连接
      console.log('connection suc');
      socket.on('', function () {})
    }
    

这样,大概就是整个前后端sokcet.io 初始化了,但是你以为就此完了吗?当然没有。需要注意的是,目前前后端的 server 端口号是不同的,所以会存在跨域问题,所以还需要解决一下开发环境的跨域问题。当然,笔者这里就以开发环境的场景进行配置前端的 devServer ,至于如果是要发布到生产上的话,还需要自行配置一些 cors 的配置的~

之前在实现前端调用后端接口的时候,已经在前端项目的 vite.config 中配置了以 '/api' 为标识的 proxy 规则,所以我们目前需要对 '/jenkins' 标识进行 proxy 配置: 全栈 webSocket 实战获取 jenkins 构建日志

如上图所示,这里需要给 /jenkins 为首的请求路径也加上 proxy 配置就能解决 ws 的跨域问题啦。

'/jenkins': {
  target: 'http://localhost:3200',
  changeOrigin: true
}

一切准备工作完成,启动前端项目看看效果:

  • netWork 的 fetch/xrh 界面出现了多条 http 的轮询请求
  • 全栈 webSocket 实战获取 jenkins 构建日志
  • ws 界面出现了我们的请求连接 '/jenkins/build'
  • 全栈 webSocket 实战获取 jenkins 构建日志
  • Node调试工具成功打印 'connection suc'
  • 全栈 webSocket 实战获取 jenkins 构建日志

2. Sokcet 同步构建日志

这一步可以说就是整个前端实战中的核心部分了,所以!不多说,直接进入实战。之前在 node 端触发 jenkins 构建是提供了一个 post 接口给 postman 调用模拟前端触发的,而这里我们直接通过 socket 去触发,并且完成构建日志同步。

于是,笔者这里对构建触发进行改写,从原来的 build 接口迁移到 socket 这里触发。具体的构建流程笔者就不在这里展开了,如果想详细了解、回顾的话,可以回到笔者的 第三篇文章node+jenkins实战构建中详细查看 build 的实现。

简要回顾一下之前的代码: 全栈 webSocket 实战获取 jenkins 构建日志 上述代码是 触发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全栈 webSocket 实战获取 jenkins 构建日志 点击后结果如图所示,可以成功获取 buildNumber 和控制台中成功打印出构建日志: 全栈 webSocket 实战获取 jenkins 构建日志

既然成功触发构建,也就意味着我们现在只需要把 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 接收日志后,再次点击构建看看效果! 全栈 webSocket 实战获取 jenkins 构建日志 如图所示,成功在前端界面中展示构建日志信息,这样一来,使用发布平台的用户就能实时获取到当前项目的构建进度、构建状态、构建错误的提示信息等等...这一步总算大功告成啦,紧接着我们进入最后一个阶段,实现全局唯一的构建实例。

三、全局唯一构建实例

什么叫全局唯一构建实例呢!可能很多小伙伴第一次读到这句的时候都是懵的!(当然,这都不是你们的问题,是笔者的语言功底太菜了)笔者现在展开说说:jenkins 同个 free style job 一次就执行一个,即使连续发起同个 job 的构建,当前只有一个构建任务执行,其余都在队列中等待。基于此,如果同个项目配置在构建中,所有进入到这个构建配置的前端页面的用户应该都是看到相同的状态。好吧,笔者也说不下去了,赶紧画个图!

全栈 webSocket 实战获取 jenkins 构建日志

如上图所示,如果当前项目正在构建中,那在不同终端打开这个配置的界面时,应该展示的是同样的信息、同样的构建进度!很显然,当前的实现是没办法做到这一点的,因为我们没有对构建状态进行一个统一的管理,所以多开浏览器tab看到的前端界面是不一致的: 全栈 webSocket 实战获取 jenkins 构建日志

那接下来,我们接着改造我们实战代码去实现这个功能!先来捋一捋思路,首先我们要自己掌控构建状态,因此每个配置构建时都应该生成一个实例,实例有一个构建中的状态;其次我们需要把构建的状态进行一个保存;然后让每个接入的终端能找到当前的构建状态。我们当前的 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
  }
}

相关核心点:

  1. Build类 笔者定义为管理整个构建生命周期的类,它的实例在整个构建的生命周期中只会创建唯一一个
  2. 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()

相关核心点:

  1. 通过 Admin 类全局管理 Build实例创建、删除 Build 实例都在这里进行
  2. 每个 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 构建日志

到这里为止,整个 webSocket 实战获取 jenkins 构建日志的内容就讲完了,不知道是否能给大家带来一丝丝的收获,反正自己是写麻了~这里还是要重点提醒一下,整篇文章笔者个人认为最最最难的部分就是全局构建实例这里,如果大家自己也想实战开发的话一定要注意如何处理多个socket连接的问题构建类、管理构建类的设计等等,因为笔者这里只是 demo 级别的。再提一次,多个 socket实例 笔者本文是通过闭包存起来的,可能有些隐晦,大家自己干的时候得注意一下~

写在最后

到这一步,整个全栈开发前端发布平台的核心功能就算是完工了。现在我们已经可以实现在前端创建、编辑配置、发起构建、获取构建日志... ...当然,笔者的实战代码都是 demo 级别的,如果是需要开发企业级别的还是要多点的设计,然后注意内存的回收问题(我这 demo 基本没怎么处理内存回收,很可能有内存泄露的问题哈哈哈)各种各样的问题吧。然后好像还有很多功能点都没讲到,不过其实掌握了核心功能后,很多细节点都可以自己去完善了,比如停止构建、回滚这种,你觉得呢!!!好啦,本文到这里吧就,后会有期~