前言
公司的AI Pass平台需要能在网页上查看一些跑在k8s容器中的服务相关的信息或进行一些操作。
思路
在前端页面上打开一个终端,前后端建立websocket通信,前端每输入一个字符,后端都与k8s进行交互然后把返回信息封装了发送到前端并展示在页面终端上。
实现代码
<script setup name="Terminal">
import 'xterm/css/xterm.css'
import 'xterm/lib/xterm.js'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import { WebsocketManager } from '@/util/websocket'
import { useDebounceFn } from '@vueuse/core'
import { terminalTypeEnum, ThemeEnum } from '@/enums/applicationEnum'
const { proxy } = getCurrentInstance()
const props = defineProps({
//连接ur1
socketUrl: {
type: String,
require: true
},
//终端类型
type: {
type: Number,
default: terminalTypeEnum.TERMINAL,
validator: value => Object.values(terminalTypeEnum).includes(value)
},
// 展示用户需知信息
showInfo: {
type: Boolean,
default: false
},
//主题
theme: {
type: String,
default: ThemeEnum.DARK,
validator: value => Object.values(ThemeEnum).includes(value)
}
})
const terminalSocket = ref(null)
const message = (command, type = 'stdin') => {
return JSON.stringify({
type,
command
})
}
const resizeMessage = (width, height) => {
return JSON.stringify({
type: 'resize',
width,
height
})
}
const getTheme = () => {
const themeOption = {
[ThemeEnum.DARK]: {
foreground: '#ddd'
},
[ThemeEnum.LIGHT]: {
background: 'white',//设置背景色为白色
// 设置前景色为黑色
foreground: 'black'
}
}
return themeOption[props.theme]
}
class TerminalWebsocket extends WebsocketManager {
constructor(url) {
super(url)
this.term = null
this.fitAddon = null
this.resizeTerm = useDebounceFn(() => {
//自适应大小(使终端的尺寸和几何尺寸适合于终端容器的尺寸)
this.fitAddon.fit()
const { rows, cols } = this.term
this.socket.send(resizeMessage(cols, rows))
this.term.scrollToBottom()
}, 300)
}
init() {
super.init()
//设置心跳保持websocket活跃,间隔15s,nginx超时时间1min
this.setHeartbeat(true, 15 * 1000, message(' '))
}
//接受处理后端推送的信息
handleSocketMessage() {
this.socket.onmessage = event => {
const { command, type } = JSON.parse(event.data)
// console. log ('command', command)
if (props.type === 1) {
// 日志
if (!this.term)
this.initTerm()
this.term.writeln(command)
} else if (props.type === 0) {
//终端
//连接成功后等后端推送了初始化信息先初始化,初始化后再处理后端发过来的其他信息
if (type === 'init') {
// 初始化
this.initTerm()
} else {
// 处理返回信息
if (command.length === 3 && encodeURIComponent(command).slice(1) === '$20%0D') {
//暂时好像没用处
this.term.write(command.slice(0, 1))
} else {
this.term.write(command)
}
}
}
}
}
// websocket关闭
handleSocketClose() {
super.handleSocketClose()
}
// 手动关闭websocket
closeWebSocket() {
super.closeWebSocket()
window.removeEventListener('resize', this.resizeTerm)
}
initTerm() {
// let element = document.querySelector ('#xterm' )
let element = proxy.$refs.xterm
//设置了cols或者fitAddon.fit();当一行字符超过80个过会替换现在的内容,否则换行
this.term = new Terminal({
cursorBlink: true, //
cursorStyle: 'bar', //5t 'block' | 'underline' | 'bar'
scrollback: 3000,//当行的滚动超过初始值时保留的行视窗,越大回滚能看的内容越多,
bufferSize: 999999, //设置终端缓冲区大小为9999999
convertEol: true,//光标设为每一行开头
fontSize: 14,
lineHeight: 1.5,
minimumContrastRatio: 21, //所有字体显示白色高亮
theme: getTheme(),
cursorWidth: 3
})
// 自适应插件
this.fitAddon = new FitAddon()
this.term.loadAddon(this.fitAddon)
this.term.open(element)
this.fitAddon.fit()
// !!! 一开始自适应不成功可能是因为我终端组件加载了,但是当前页面页签在jupyter,自适应就没成功,所以现在给终端加了缓存,第一次切进进来的时候才加载
//此处datas一定不要改 !!!
this.term.onData(datas => {
// console.log ('data', datas, message (datas) )
if (props.type === 0) {
this.socket.send(message(datas))
}
})
window.addEventListener('resize', this.resizeTerm)
if (props.showInfo) {
this.writeExtraInfo()
}
//向后端发送一些初始化的信息
const { rows, cols } = this.term
this.socket.send(resizeMessage(cols, rows))
this.term.focus()
}
writeExtraInfo() {
this.term.writeln('\n用户需知:请提前阅读本文档\n......')
}
}
const key = ref(0)
watch(
() => props.socketUrl,
val => {
if (val) {
if (terminalSocket.value) {
terminalSocket.value.closeWebSocket()
//由于xtermjs有缓冲区,上一次websocket连接后端推过来太多的数据,哪怕关了websocket,还有很多未写入终端的数据仍在写入
key.value += 1
}
terminalSocket.value = new TerminalWebsocket(props.socketUrl)
terminalSocket.value.init()
}
}, { immediate: true }
)
onBeforeUnmount(() => {
terminalSocket.value.closeWebSocket()
})
defineExpose({
resize
})
</script>
<template>
<div class="terminal-container">
<div :key="key" ref="xterm" class="xterm" />
</div>
</template>
<style lang="scss" scoped>
.terminal-container {
background-color: #000;
width: 100%;
height: 100%;
padding: 0 20px;
.xterm {
width: 100%;
height: 100%;
}
}
</style>