• xterm + WebSocket 实现 CLI


    npm安装

    npm i xterm xterm-addon-fit
    
    • 1

    引入(下方示例演示在react中的写法)

    import { Terminal } from "xterm"
    import { FitAddon } from 'xterm-addon-fit'
    
    import "xterm/css/xterm.css"
    
    import WSTerm from '@/ws/term' //WebSocket的封装,具体代码见下
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    使用

    const FWHFCLI = () => {
      const CLIEl = React.createRef();
      const termClass = useRef()
      const fitAddon = useRef()
      const curLineText = useRef()
      const initCLI = () => {
        curLineText.current = ''
    
        let termContainer = document.getElementsByClassName('fwhf-cli')[0]
        termClass.current = new Terminal({
            rendererType: "canvas", //渲染类型
            rows: Math.ceil(termContainer.offsetHeight / 16 - 9),
            cols: parseInt(termContainer.offsetWidth / 14),
            convertEol: true, //启用时,光标将设置为下一行的开头
            // scrollback: 50, //终端中的回滚量
            disableStdin: false, //是否应禁用输入
            // cursorStyle: "underline", //光标样式
            cursorBlink: true, //光标闪烁
            theme: {
                foreground: "#ECECEC", //字体
                background: "#000000", //背景色
                cursor: "help", //设置光标
            }
        })
        let term = termClass.current
        // 创建terminal实例
        term.open(CLIEl.current)
        // 换行并输入起始符
        term.prompt = _ => {
          term.write("\r\n>>> ")
        }
        // canvas背景全屏
        fitAddon.current = new FitAddon()
        term.loadAddon(fitAddon.current)
        fitAddon.current.fit()
    
        window.addEventListener("resize", resizeScreen)
        runFakeTerminal()
      }
      const resizeScreen = () => {
        fitAddon.current.fit()
      }
      const runFakeTerminal = () => {
        let term = termClass.current
        if (term._initialized) return
        // 初始化
        term._initialized = true
        // 添加事件监听器,支持输入方法
        term.onData((key) => {
          if (key.charCodeAt(0) == 13) { // 回车
            if(curLineText.current === 'clear') {
              term.clear()
            }
            if (curLineText.current.trim().length === 0) {
              term.prompt()
            } else {
              // 保存命令
              let commands = localStorage.getItem('commands') ? JSON.parse(localStorage.getItem('commands')) : []
              commands.push(curLineText.current)
              localStorage.setItem('commands', JSON.stringify(commands))
              localStorage.setItem('index', commands.length)
            }
            //发送请求。。。
            WSTermClass.current.sendData(curLineText.current)
            curLineText.current = ''
          } else if (key === '\u001b[A') {   // 向上方向
            let commands = localStorage.getItem('commands') ? JSON.parse(localStorage.getItem('commands')) : [] 
            let index = localStorage.getItem('index') ? localStorage.getItem('index') : commands.length
            index = parseInt(index)
            if (commands.length && index < commands.length + 1 && index > 0) {
              // 删除现有命令
              for (let i = 0; i < curLineText.current.length; i++) {
                if (term._core.buffer.x > 4) {
                  term.write('\b \b')
                }
              }
              curLineText.current = commands[index - 1]
              term.write(curLineText.current)
              localStorage.setItem('index', index - 1)
            }
          } else if (key  === '\u001b[B') {   // 向下方向
            let commands = localStorage.getItem('commands') ? JSON.parse(localStorage.getItem('commands')) : []
            let index = localStorage.getItem('index') ? localStorage.getItem('index') : commands.length
            index = parseInt(index)
            if (commands.length && index < commands.length - 1 && index > -1) {
              // 删除现有命令
              for (let i = 0; i < curLineText.current.length; i++) {
                if (term._core.buffer.x > 4) {
                  term.write('\b \b')
                }
              }
              curLineText.current = commands[index + 1]
              term.write(curLineText.current)
              localStorage.setItem('index', index + 1)
            }
          } else if (key.charCodeAt(0) === 127) {
            if (term._core.buffer.x > 4) {
              term.write('\b \b')
              curLineText.current = curLineText.current.substr(0, curLineText.current.length - 1)
            }
          } else{
            if(!curLineText.current){
              curLineText.current = ''
            }
            curLineText.current += key
            term.write(key)
          }
          // if (key.charCodeAt(0) === 9) {  // tab键
          //   let params = {
          //     cmd: curLineText.current
          //   }
          //   tab补全
          //   xxx(params).then((res) => {
          //     if (res.code === 200) {
          //       if (res.data.length) {
          //         for (let i = 0; i < curLineText.current.length; i++) {
          //           term.write('\b \b')
          //         }
          //         let data = res.data.join('\r\n')
          //         curLineText.current = res.data[res.data.length - 1]
          //         if (res.data.length > 1) {
          //           term.write('\r\n')
          //           term.write(data)
          //           term.prompt()
          //           term.write(res.data[res.data.length - 1])
          //         } else {
          //           term.write(that.command)
          //         }
          //       }
          //     }
          //   })
          // }
        })
      }
    
      const WSTermClass = useRef()
      useEffect(() => {
        WSTermClass.current = new WSTerm({
          openCb: () => {
            initCLI()
          },
          messageCb: (data) => {
            termClass.current.write(data)
          }
        })
        return () => {
          WSTermClass.current.close()
          window.removeEventListener("resize", resizeScreen)
        }
      },[])
    
      return <div className={"fwhf-cli"}>
          <div ref={CLIEl}></div>
      </div>
    }
    export default FWHFCLI
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156

    WSTerm

    export default class WSTerm{
        constructor(opt) {
            this.openCb = opt.openCb
            this.closeCb = opt.closeCb
            this.errorCb = opt.errorCb
            this.messageCb = opt.messageCb
            this.initSocket()
        }
        // 初始化websocket
        initSocket () {
            this.socket = new WebSocket((location.protocol === 'http:' ? 'ws://' : 'wss://') + '你的ws地址')
            this.openSocket()
            this.closeSocket()
            this.errorSocket()
            this.messageSocket()
        }
        // 监听打开连接
        openSocket () {
            this.socket.onopen = () => {
                this.openCb && this.openCb()
            }
        }
        // 监听关闭连接
        closeSocket () {
            this.socket.onclose = () => {
                this.closeCb && this.closeCb()
            }
        }
        // 监听连接错误
        errorSocket () {
            this.socket.onerror = (error) => {
                this.errorCb && this.errorCb()
            }
        }
        // 监听接收消息
        messageSocket () {
            this.socket.onmessage = (res) => {
                this.messageCb && this.messageCb(res.data)
            }
        }
        // 主动关闭链接
        close () {
            this.socket.close()
        }
        // 主动发送消息
        sendData (data) {
            this.socket.send(data)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
  • 相关阅读:
    博弈论——反应函数
    【配电网规划】配电网网架重构、DG位置选择容量配置(Matlab代码实现)
    Django框架之模板层
    i5 6500 HD530 台式机黑苹果记录
    shell 运算符
    《MLB棒球创造营》:走近棒球运动·华盛顿国民队
    外卖小程序系统:数字化餐饮的编码之道
    华为通过FTP 进行文件操作示例
    Mathematica (31)---学会用Mathematica与PPT联合绘制SCI论文图形
    《入门级-Cocos2dx4.0 塔防游戏开发》---第十课:游戏中餐单设置
  • 原文地址:https://blog.csdn.net/qq_42231248/article/details/126815070