Real‑Time Jenkins Build Log Retrieval with WebSocket in a Frontend Deployment Platform
This article demonstrates how to extend a front‑end deployment platform by adding dynamic routing, a configuration‑detail page, backend APIs, and a full‑stack Socket.IO integration that streams Jenkins build logs to the browser in real time, while also managing a globally unique build instance to synchronize multiple clients.
The tutorial continues the series on building a full‑stack front‑end deployment platform, focusing on the final stage: fetching Jenkins build logs via WebSocket.
1. Front‑end Dynamic Routing
By adding a clickable el-button inside the el-table-column , each row becomes a link that navigates to a dynamic route /configDetail/:id . The new ConfigDetail folder under pages holds the detail component.
<el-table-column label="项目名称" prop="projectName">
<!-- add @click, pass rowData, navigate by id -->
<template #default="scope">
<el-button type="primary" link @click="handleToDetail(scope.row)">
{{ scope.row.projectName }}
</el-button>
</template>
</el-table-column>The navigation function:
const handleToDetail = (rowData) => {
router.push({
name: "ConfigDetail",
params: { id: rowData._id }
})
}2. Configuration‑Detail Page
The page displays configuration information, provides a build button, and shows a log area. The required backend API /jobDetail is added:
router.get('/jobDetail', controller.getConfigDetail)Controller implementation:
export async function getConfigDetail(ctx, next) {
try {
const { id } = ctx.request.query
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()
}3. Socket.IO Integration
Both front‑end and back‑end install socket.io-client and socket.io (v4). The front‑end initializes the socket in onMounted :
const initSocket = () => {
const { id } = route.params
const ioInstance = io({
path: '/jenkins/build',
query: { id }
})
// event listeners will be added later
}The back‑end creates a Socket.IO server on the same path:
export default function initBuildSocket(httpServer) {
const io = new Socket(httpServer, { path: '/jenkins/build' })
io.on('connection', controller.socketConnect)
}Connection handler logs the connection and sets up event listeners.
4. Build Trigger via Socket
Front‑end emits build:start when the user clicks the build button:
const handleBuild = () => {
ioInstance.value.emit('build:start')
}Back‑end receives the event, creates a Build instance, configures the Jenkins job, triggers the build, and streams logs:
socket.on('build:start', async () => {
console.log('build start')
const jobName = 'test-config-job'
const config = await jobConfig.findJobById(id)
await jenkins.configJob(jobName, config)
const { buildNumber, logStream } = await jenkins.build(jobName)
// forward logStream events to the client
logStream.on('data', text => socket.emit('build:data', text))
logStream.on('error', err => socket.emit('build:error', err))
logStream.on('end', () => socket.emit('build:end'))
})5. Global Unique Build Instance
To keep all connected clients in sync, a singleton Build object is stored in a global map keyed by configuration ID. The Build class tracks build state, log text, and provides initLogStream(socket) to attach additional sockets.
export default class Build {
constructor(id, delBuilderFn) {
this.id = id
this.isBuilding = false
this.logStream = null
this.logStreamText = ''
this.buildNumber = ''
this.delBuilderFn = delBuilderFn
}
async build(socket) { /* Jenkins build logic */ }
initLogStream(socket) { /* attach socket listeners */ }
destroy() { /* cleanup */ }
}A manager class Admin holds the map and creates/retrieves Build instances:
class Admin {
constructor() { this.map = {} }
getBuilder(id, socket) {
if (Reflect.has(this.map, id)) {
this.map[id].initLogStream(socket)
return this.map[id]
}
return this.createBuilder(id)
}
createBuilder(id) {
const builder = new Build(id, this.delBuilder.bind(this))
this.map[id] = builder
return builder
}
delBuilder(id) { this.map[id] && this.map[id].destroy(); Reflect.deleteProperty(this.map, id) }
}
export default new Admin()The socket connection handler now obtains the singleton builder:
export function socketConnect(socket) {
console.log('connection suc')
const { id } = socket.handshake.query
const builder = adminInstance.getBuilder(id, socket)
socket.on('build:start', async () => {
console.log('build start')
await builder.build(socket)
})
}6. Result
After the integration, clicking the build button in one browser tab starts a Jenkins build, streams the log to the server, and the server broadcasts the log to every connected client. Multiple tabs display identical, synchronized logs, proving the global unique build instance works.
The article concludes that the core functions of the full‑stack front‑end deployment platform are now complete, while noting that production‑grade implementations would need additional concerns such as memory cleanup, error handling, and advanced features like build cancellation or rollback.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.