前端 终端组件vue xterm.js websocket

1,046 阅读1分钟

前端 终端组件vue + xterm.js + websocket

安装xterm

npm install xterm --save // 4.6.0、 5.3.0
npm install xterm-addon-fit --save  // 0.4.0、9.8.0

定义组件

新建xterm.vue 使用websocket进行通信

使用xterm.onData 根据输入内容是否发送数据,使用websocket接受返回数据并展示

<template>
  <div ref="xtermRef" class="xterm"></div>
</template>

<script lang="tsx" setup>
import { onActivated, onDeactivated, ref } from 'vue'

import 'xterm/css/xterm.css'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'

defineOptions({
  name: 'IXterm'
})
const props = withDefaults(
  defineProps<{
    socketUrl: string
    prefix?: string
    xtermOption?: Object
    commands?: Object
  }>(),
  {
    socketUrl: '',
    prefix: '$',
    commands: () => {
      return {}
    }
  }
)

const xtermRef = ref<HTMLElement>()
const xterm = ref<any>()
const socket = ref<any>()
const command = ref<string>('')

const initScoket = () => {
  socket.value = new WebSocket(props.socketUrl)
  socketOnClose()
  socketOnOpen()

  socketOnMessage()
  socketOnError()
}

const socketOnOpen = () => {
  socket.value.onopen = () => {
    // 链接成功后
    initXterm()
  }
}

const socketOnMessage = () => {
  socket.value.onmessage = (e: any) => {
    // console.log('socket 收到消息', e.data)
    const data = e.data
    // const term = unref(term)
    if (xterm.value) {
      xterm.value.write(data)

      setTimeout(() => {
        prompt()
      }, 1000 * 2)
    }
  }
}

const socketOnClose = () => {
  socket.value.onclose = () => {
    // console.log('close socket')
  }
}

const socketOnError = () => {
  socket.value.onerror = () => {
    // console.log('socket 链接失败')
  }
}

// 发送消息
const onSend = (text: string) => {
  text = text.replace(/\\\\/, '\\')
  socket.value?.send(text)
  // 失去焦点
  xterm.value.blur()
}
let fitAddon: any
// 初始化xterm
const initXterm = () => {
  const options = {
    fontSize: 14,

    // rendererType: 'canvas', //渲染类型
    // rows: _this.rows, //行数
    // cols: _this.cols, // 不指定行数,自动回车后光标从下一行开始
    convertEol: true, //启用时,光标将设置为下一行的开头
    // scrollback: 50, //终端中的回滚量
    disableStdin: false, //是否应禁用输入
    // cursorStyle: "underline", //光标样式
    cursorBlink: true, //光标闪烁
    theme: {
      foreground: '#ECECEC', //字体
      background: '#000000', //背景色
      cursor: 'help' //设置光标
    }
  }

  const xtermOptions = Object.assign(options, props.xtermOption)
  xterm.value = new Terminal(xtermOptions)

  xtermData()

  fitAddon = new FitAddon()
  xterm.value.loadAddon(fitAddon)
  xterm.value.open(xtermRef.value!)
  fitAddon.fit()
  xterm.value.focus()
}

// 输入内容
const xtermData = () => {
  xterm.value.onData((e: any) => {
    console.log(e)

    switch (e) {
      case '\u0003': // Ctrl+C
        xterm.value.write('^C')
        prompt()
        break
      case '\r': // Enter
        if (command.value === '') {
          prompt()
          break
        }
        runCommand(command.value)
        xterm.value.write('\r\n')
        command.value = ''
        break
      case '\u007F': // Backspace (DEL)
        // Do not delete the prompt
        if (xterm.value._core.buffer.x > 2) {
          xterm.value.write('\b \b')
          if (command.value.length > 0) {
            command.value = command.value.substr(0, command.value.length - 1)
          }
        }
        break
      default: // Print all other characters for demo
        if ((e >= String.fromCharCode(0x20) && e <= String.fromCharCode(0x7e)) || e >= '\u00a0') {
          command.value += e
          xterm.value.write(e)
        }
    }
  })
}

const prompt = () => {
  command.value = ''
  // 换行
  xterm.value.write(`\r\n${props.prefix} `)
  // 获取焦点
  xterm.value.focus()
}

const defaultCommand = {
  clear: {
    f: () => {
      xterm.value?.clear()
      prompt()
    }
  }
}

// 执行命令 发送消息
const runCommand = (text: string) => {
  const commandStr = text.trim().split(' ')[0]
  if (commandStr.length > 0) {
    const commandsObj = { ...defaultCommand, ...props.commands }
    if (commandStr in commandsObj) {
      ;(commandsObj as any)[commandStr].f()
      return
    }
  }
  onSend(commandStr)
}

const resize = () => {
  // 适应父容器的大小
  fitAddon.fit()

  // console.log(xterm.value)
  // // 如果需要,可以获取终端的宽度和高度
  // const { cols, rows } = xterm.value
  // console.log(`Terminal size: cols = ${cols}, rows = ${rows}`)
}

onActivated(() => {
  initScoket()
  window.addEventListener('resize', resize)
})

onDeactivated(() => {
  socket.value?.close()
  window.removeEventListener('resize', resize)
})

defineExpose({
  resize
})
</script>

<style lang="scss">
.xterm {
  width: 100%;
  height: 100%;
}
</style>

组件中使用

<IXterm socketUrl="ws://124.222.224.186:8800"></IXterm>