前言
我們在做CI/CD時,最常用的做法就是使用Jenkins或gitlab的流水線的功能,先由運維寫好流水線腳本,然后人工執行或由平台調用流水線接口去執行。
發布進度展示
以Gitlab為例,它的后台后流水線發布進度如下圖所示:
流水線是由多個Stage組成,而每個Stage下又可以有多個Job,因此前端常用的el-step組件就無法實現這種展示效果,筆者從網上找到一款名叫 vue-super-flow 的前端組件 https://caohuatao.github.io/,可以用來畫流程圖。
測試代碼:

<template> <div> <div class="super-flow-base-demo"> <super-flow :draggable= true ref="superFlow" :node-list="nodeList" :link-list="linkList" :origin="origin" :node-menu="nodeMenuList" :link-menu="linkMenuList" :link-desc="linkDesc"> <template v-slot:node="{meta}"> <div :class="`flow-node flow-node-${meta.prop}`" @click="show"> <header> {{meta.name}} </header> <section> {{meta.desc}} </section> </div> </template> </super-flow> </div> </div> </template> <script> const drawerType = { node: 0, link: 1 } export default { data() { return { drawerType, drawerConf: { title: '', visible: false, type: null, info: null, open: (type, info) => { const conf = this.drawerConf conf.visible = true conf.type = type conf.info = info if (conf.type === drawerType.node) { conf.title = '節點' if (this.$refs.nodeSetting) this.$refs.nodeSetting.resetFields() this.$set(this.nodeSetting, 'name', info.meta.name) this.$set(this.nodeSetting, 'desc', info.meta.desc) } else { conf.title = '連線' if (this.$refs.linkSetting) this.$refs.linkSetting.resetFields() this.$set(this.linkSetting, 'desc', info.meta ? info.meta.desc : '') } }, cancel: () => { this.drawerConf.visible = false if (this.drawerConf.type === drawerType.node) { this.$refs.nodeSetting.clearValidate() } else { this.$refs.linkSetting.clearValidate() } } }, linkSetting: { desc: '' }, nodeSetting: { name: '', desc: '' }, origin: [0, 0], nodeList: [], linkList: [], nodeMenuList: [ [ { label: '刪除', disable: false, hidden(node) { return node.meta.prop === 'start' }, selected(node, coordinate) { node.remove() } } ], [ { label: '編輯', selected: (node, coordinate) => { console.log(node, coordinate) } } ] ], linkMenuList: [ [ { label: '刪除', disable: false, selected: (link, coordinate) => { link.remove() } } ], [ { label: '編輯', disable: false, selected: (link, coordinate) => { console.log(link, coordinate) } } ] ] } }, created() { const nodeList = [ { 'id': '開始節點', 'width': 100, 'height': 80, 'coordinate': [0, 0], 'meta': { 'prop': 'start', 'name': '開始節點', 'desc': '111' } }, { 'id': '條件節點1', 'width': 100, 'height': 80, 'coordinate': [150, 0], 'meta': { 'prop': 'condition', 'name': '條件節點1' } }, { 'id': '條件節點2', 'width': 100, 'height': 80, 'coordinate': [150, 100], 'meta': { 'prop': 'condition', 'name': '條件節點2' } }, { 'id': '抄送節點1', 'width': 100, 'height': 80, 'coordinate': [300, 0], 'meta': { 'prop': 'ccc', 'name': '抄送節點1' } }, { 'id': '結束節點', 'width': 100, 'height': 80, 'coordinate': [450, 0], 'meta': { 'prop': 'end', 'name': '結束節點' } }, ] const linkList = [ { 'id': 'linkcs9ZhumWeTHrtUy8', 'startId': '開始節點', 'endId': '條件節點1', 'startAt': [100, 40], 'endAt': [0, 40], 'meta': null }, { 'id': 'linknL75dQV0AWZA85sq', 'startId': '開始節點', 'endId': '條件節點2', 'startAt': [100, 40], 'endAt': [0, 40], 'meta': null }, { 'id': 'linkA0ZZxRlDI9AOonuq', 'startId': '條件節點2', 'endId': '抄送節點1', 'startAt': [160, 40], 'endAt': [0, 40], 'meta': null }, { 'id': 'linkhCKTpRAf89gcujGS', 'startId': '條件節點1', 'endId': '抄送節點1', 'startAt': [160, 40], 'endAt': [0, 40], 'meta': null }, { 'id': 'link2o7VZ7DRaSFKtB0g', 'startId': '抄送節點1', 'endId': '結束節點', 'startAt': [160, 40], 'endAt': [0, 25], 'meta': null }, ] setTimeout(() => { this.nodeList = nodeList this.linkList = linkList }, 100) }, methods: { linkDesc(link) { return link.meta ? link.meta.desc : '' }, show(){ alert(1) } } } </script> <style lang="less"> .super-flow-base-demo { width : 100%; height : 800px; margin : 0 auto; background-color : #f5f5f5; .super-flow__node { .flow-node { > header { font-size : 14px; height : 32px; line-height : 32px; padding : 0 12px; color : #ffffff; } > section { text-align : center; line-height : 20px; overflow : hidden; padding : 6px 12px; word-break : break-all; } &.flow-node-start { > header { background-color : #55abfc; } } &.flow-node-condition { > header { background-color : #BC1D16; } } &.flow-node-approval { > header { background-color : rgba(188, 181, 58, 0.76); } } &.flow-node-ccc { > header { background-color : #30b95c; } } &.flow-node-end { > header { height : 50px; line-height : 50px; background-color : rgb(0, 0, 0); } } } } } </style>
其原理就是定義好每個節點的位置、長度、寬度及連接線的起始點和終點。
我們自己的實現效果圖如下:
job詳情顏色顯示
gitlab流水線獲取job的接口返回的是帶ANSI格式的內容,比如 [0KRunning with gitlab-runner 13.4.1 (e95f89a0)↵[0;m[0K,如果直接展示在前端肯定是不行的。筆者在網上對比了很多工具,發現有一款名叫 Xterm.js的前端組件,用來模擬終端,天生支持ansi格式的輸出。
使用方式:
1、安裝xterm:
npm install xterm
2、測試代碼:

<!doctype html> <html> <head> <link rel="stylesheet" href="node_modules/xterm/css/xterm.css" /> <script src="node_modules/xterm/lib/xterm.js"></script> </head> <body> <div id="terminal"></div> <script> var term = new Terminal(); term.open(document.getElementById('terminal')); term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ') </script> </body> </html>
展示效果如下:
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="node_modules/xterm/css/xterm.css" />
<script src="node_modules/xterm/lib/xterm.js"></script>
</head>
<body>
<div id="terminal"></div>
<script>
var term = new Terminal();
term.open(document.getElementById('terminal'));
term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
</script>
</body>
</html>