基于 xterm + websocket + vue 实现网页版终端 terminal

发布时间 2023-04-13 11:04:35作者: 夏小夏吖

cdn形式html页面实现

在进行开发这个功能的时候,进行了百度,最后参考此博主的文章中的自定义版本,进行修改:https://blog.csdn.net/qq_25252769/article/details/127791918

开发过程中,由于后台数据返回的格式需要进行处理,根据自身需求,代码进行了少许修改

主要修改1:

根据后台返回的数据,需要传入一个对象,里边key值为message,并对这个对象进行序列化
主要修改2:
// this.ws.send("\n");//这边原来是放开状态,但由于后台数据返回原因会报错,就直接注释了
主要修改3:
接收信息时,
let message = "\n" + JSON.parse(event.data).message; //根据后台返回数据进行处理成对象形式,获取到message,进行显示到页面
const blob = new Blob([message], {
   type: "text/plain",
});
原内容是:
//将字符串转换成 Blob对象
const blob = new Blob([event.data], {
type: 'text/plain'
})

,最终修改后的代码如下:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>前端终端,操作后端的docker容器</title>
    <!-- 引入样式 -->
    <link rel="stylesheet" href="https://unpkg.com/xterm@4.18.0/css/xterm.css">
</head>

<body class="bodyCss">
    <div id="app">
        <div id="terminal" ></div>
    </div>
</body>
<script src="https://unpkg.com/xterm@4.18.0/lib/xterm.js"></script>
<script src="https://lib.baomitu.com/vue/2.6.14/vue.js"></script>
<script  src="https://unpkg.com/xterm-addon-fit@0.6.0/lib/xterm-addon-fit.js"></script>
  <script>
    let wsTime = null;
    new Vue({
      el: "#app",
      data() {
        return {
          // 终端
          term: {},
          // websocket
          ws: {},
          // 用户输入
          command: "",
        };
      },
      created() {
        // 初始化终端
        this.initTerminal();
      },
      mounted() {
        // 建立websocket连接
        this.websocket();
      },
      beforeDestroy() {
        this.ws.close();
        this.term.dispose();
      },
      methods: {
        // 初始化终端配置
        initTerminal() {
          this.term = new Terminal({
            rendererType: "canvas", //渲染类型
            // rows: 40, //行数,影响最小高度
            cols: 100, // 列数,影响最小宽度
            convertEol: true, //启用时,光标将设置为下一行的开头
            // scrollback: 50, //终端中的滚动条回滚量
            disableStdin: false, //是否应禁用输入。
            cursorStyle: "underline", //光标样式
            cursorBlink: true, //光标闪烁
            theme: {
              foreground: "#F8F8F8",
              background: "#2D2E2C",
              cursor: "help", //设置光标
              lineHeight: 16,
            },
            fontFamily: '"Cascadia Code", Menlo, monospace',
          });
        },
        // 自定义终端默认展示内容
        writeDefaultInfo() {
          let defaultInfo = [
            "┌\x1b[1m terminals \x1b[0m─────────────────────────────────────────────────────────────────┐ ",
            "│                                                                            │ ",
            "│  \x1b[1;34m 欢迎使用Web Docker SSH \x1b[0m                                                  │ ",
            "│                                                                            │ ",
            "└────────────────────────────────────────────────────────────────────────────┘ ",
          ];
          // 测试颜色区间
          // let arr = Array.from({length:100},(v,i)=>v = i)
          // console.log(arr)
          // arr.map((item,i) => {
          //     defaultInfo.push(`Hello from \x1B[1;3;${i}m ${i} \x1B[0m  \u2764\ufe0f   ${i}`)
          // })
          this.term.write(defaultInfo.join("\n\r"));
          this.writeOfColor("我是加粗斜体红色的字呀", "1;3;", "31m");
          // this.term.write('\n\r$ ')
        },
        //
        writeOfColor(txt, fontCss = "", bgColor = "") {
          // 在Linux脚本中以 \x1B[ 开始,中间前部分是样式+内容,以 \x1B[0m 结尾
          // 示例 \x1B[1;3;31m 内容 \x1B[0m
          // fontCss
          // 0;-4;字体样式(0;正常 1;加粗 2;变细 3;斜体 4;下划线)
          // bgColor
          // 30m-37m字体颜色(30m:黑色 31m:红色 32m:绿色 33m:棕色字 34m:蓝色 35m:洋红色/紫色 36m:蓝绿色/浅蓝色 37m:白色)
          // 40m-47m背景颜色(40m:黑色 41m:红色 42m:绿色 43m:棕色字 44m:蓝色 45m:洋红色/紫色 46m:蓝绿色/浅蓝色 47m:白色)
          this.term.write(`\x1B[${fontCss}${bgColor}${txt}\x1B[0m`);
        },
        // 监听输入
        userWrite() {
          this.term.onData((e) => {
            switch (e) {
              case "\u0003": // Ctrl+C
                this.term.write("^C ");
                this.term.write("\r\n$ ");
                break;
              case "\r": // Enter
                this.ws.send(JSON.stringify({ message: this.command }));//根据后台返回的数据,需要传入一个对象,里边key值为message,并对这个对象进行序列化
                // this.ws.send("\n");//这边原来是放开状态,但由于后台数据返回原因会报错,就直接注释了
                this.command = "";
                // this.term.write('\r\n$ ')
                break;
              case "\u007F": // Backspace (DEL)
                // Do not delete the prompt
                if (this.term._core.buffer.x > 2) {
                  this.term.write("\b \b");
                  if (this.command.length > 0) {
                    this.command = this.command.substr(
                      0,
                      this.command.length - 1
                    );
                  }
                }
                break;
              default: // Print all other characters for demo
                if (
                  (e >= String.fromCharCode(0x20) &&
                    e <= String.fromCharCode(0x7e)) ||
                  e >= "\u00a0"
                ) {
                  this.command += e;
                  this.writeOfColor(e, "2;3;", "33m");
                  console.log("用户输入command", this.command);
                }
            }
          });
        },
        // 建立websocket连接
        websocket() {
          // WebSocket start
          if ("WebSocket" in window) {
            //需要修改ip和id
            //例如:const url = `ws://192.168.111.222:2375/v1.41/containers/0eb8aafb4e6e/attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1`
            const url = `ws:172.16.188.58:5000/ws/chat/?host=172.16.188.58&username=root&password=admin`;
            const ws = new WebSocket(url);
            this.ws = ws;
            this.$nextTick(() => {
              this.userWrite();
            });
            ws.onopen = (event) => {
              console.log("已建立连接:", event);
              // 输入换行符可让终端显示当前用户的工作路径
              // ws.send("\n");//这边原来是放开状态,但由于后台数据返回原因会报错,就直接注释了
              // 窗口自适应插件
              const fitAddon = new FitAddon.FitAddon();
              // 窗口尺寸变化时,终端尺寸自适应
              window.onresize = () => {
                fitAddon.fit();
              };
              this.term.loadAddon(fitAddon);
              this.term.open(document.getElementById("terminal"));
              this.term.focus();
              // 自定义终端默认展示内容
              this.writeDefaultInfo();
            };
            ws.onmessage = (event) => {
              console.log("接收信息:", event.data);
              let message = "\n" + JSON.parse(event.data).message; //根据后台返回数据进行处理成对象形式进行显示到页面,如果不需要处理,需要直接
              //将字符串转换成 Blob对象
              const blob = new Blob([message], {
                type: "text/plain",
              });
              //将Blob 对象转换成字符串
              const reader = new FileReader();
              reader.readAsText(blob, "utf-8");
              reader.onload = (e) => {
                // 可以根据返回值判断使用何种颜色或者字体,不过返回值自带了一些字体颜色
                this.writeOfColor(reader.result, "0;", "37m");
              };
            };
            ws.onerror = (event) => {
              console.log("错误信息:", event);
              if (wsTime) {
                window.clearTimeout(wsTime);
                wsTime = null;
              }
              wsTime = window.setTimeout(() => {
                this.websocket();
              }, 3000);
            };
            ws.onclose = (event) => {
              console.log("已关闭连接:", event);
              // if (wsTime) {
              //    window.clearTimeout(wsTime)
              //    wsTime = null
              // }
              // wsTime = window.setTimeout(() => {
              //    this.websocket()
              // }, 3000)
            };
          } else {
            console.log("浏览器不支持 WebSocket..");
          }
          // WebSocket end
        },
      },
    });
  </script>
</html>