vue3+xterm5.3.0+webSocket+监听浏览器刷新操作,实现web terminal终端功能

3,638 阅读6分钟

一、前言

为了实现web terminal终端需求,调研了几款web terminal插件,还是xterm社区最活跃,技术最成熟,网络资源最多,就是官方文档很让人吐槽,所以开发完后,自己也整理一下,当笔记写。

具体需求是先点击按钮,打开xterm小黑框,初始化xterm和webSocket,并且需要在连接没有断的情况下监听浏览器刷新事件,需要弹出二次确认框阻断当前的刷新操作。

二、开发流程

  1. 引入xterm插件
  2. xterm+webSocket实现双向交互
  3. 屏幕适配

三、代码实现

1.准备xterm展示容器

<div v-if="state.showEndButton" style="height: 100%; background: #002833">
    <div v-loading="loading" id="terminal" ref="terminal"></div>
  </div>

代码块中的state.showEndButton是控制Xterm显示时机的状态,是我需求里特定的,需要点击按钮后才开始渲染xterm,而这里加上v-if后,控制台会有warn,所以在点击按钮触发的函数里用了nextTick,开始初始化。

  // nextTick:xterm父元素用了v-if,要保证先渲染dom 才能初始化Xterm
  nextTick(() => {
    initWS();
    initTerm();
    onRefreshListener();
  });

2.Xterm+websocket实现双向交互

// webSocket初始化
const initWS = () => {
  if (!terminalSocket.value) {
    createWS();
  }
  if (terminalSocket.value && terminalSocket.value.readyState > 1) {
    terminalSocket.value.close();
    createWS();
  }
};

// 创建WS
const createWS = () => {
  terminalSocket.value = new WebSocket(contentValue.value.wsAddr);
  terminalSocket.value.onopen = runRealTerminal; //WebSocket 连接已建立
  terminalSocket.value.onmessage = onWSReceive; //收到服务器消息
  terminalSocket.value.onclose = closeRealTerminal; //WebSocket 连接已关闭
  terminalSocket.value.onerror = errorRealTerminal; //WebSocket 连接出错
};

// WebSocket 连接已建立
const runRealTerminal = () => {
  // message是首次建立链接后,存与后端交互的第一条message信息--需要与后端确认
  let message = {
    side: xxx,
    token: xxx,
    tunnelId: xxx,
    terminalMetadata: { width:xx, height: xx }// 看第三步-适配
  };
  let newMessage = JSON.stringify(message) + '\n';
  terminalSocket.value.send(newMessage); // 建立连接后给后端发送的第一条json数据
  ws_heartCheck.start(); // 启动心跳
  loading.value = false; // 连接建立后关闭loading效果
};

// WebSocket收到服务器消息
const onWSReceive = (message) => {
  const data = message.data;
  // base64解密
  const reader = new FileReader();
  reader.onload = function (e) {
    const base64Content = e.target.result;
    term.value.write(base64Content);// 回显在xterm上
  };
  reader.readAsText(data); // 以text文本显示readAsText
  term.value.element && term.value.focus();
};

//WebSocket 连接出错
const errorRealTerminal = (ex) => {
  let message = ex.message;
  if (!message) message = 'disconnected';
  term.value.write(`\x1b[31m${message}\x1b[m\r\n`);
};

//WebSocket 连接已关闭
const closeRealTerminal = (e) => {
  term.value.write('~本次连接已断开,请重新建立连接');
  ws_heartCheck.reset();// 关闭心跳机制
  loading.value = false;
};

// 初始化Terminal
const initTerm = () => {
  term.value = new Terminal({
    cols: xx, //看第三步-适配
    rows: xx,// 看第三步-适配
    fontSize: 14,
    wraparoundMode: true, // 自动换行
    fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
    theme: {
      background: '#181d28'
    },
    cursorBlink: true,
    cursorStyle: 'underline',
    scrollback: 999999
  });
  term.value.open(terminal.value); // 挂载dom窗口
  termData(); // Terminal 事件挂载
};

// 终端输入触发事件-把用户输入的内容全部传给后端,让后端控制
const termData = () => {
  term.value.onData((data) => {
    if (isWsOpen()) {
      terminalSocket.value.send(data);
    }
  });
};

// 是否连接中0 1 2 3 状态
const isWsOpen = () => {
  const readyState = terminalSocket.value && terminalSocket.value.readyState;
  return readyState === 1;
};

// 刷新提示
const beforeUnloadHandler = (event) => {
  event.preventDefault();
  event.returnValue = '';
};
// xterm开启后,开始监听浏览器刷新事件,浏览器会弹出默认的二次确认框
const onRefreshListener = () => {
  window.addEventListener('beforeunload', beforeUnloadHandler);
};
// 移除浏览器刷新监听事件的监听器
onBeforeUnmount(() => {
  window.removeEventListener('beforeunload', beforeUnloadHandler);
  terminalSocket.value && terminalSocket.value.close();
});
</script>

3.屏幕适配

屏幕适配,根据我查到的资料,应该是把尺寸信息,传给后端,让后端控制格式,比如在这里是通过terminalMetadata: { width:xx, height: xx }让后端拿到前端的xterm屏幕尺寸信息,然后统一虚拟终端的屏幕大小,进行前后端适配,这样,在前端终端尺寸改变后,才不会有吃字的情况出现。还需要用到fitAddon插件。

但是!!因为需求比较特殊,后端无法修改虚拟终端的大小,需要前端独立适配屏幕,所以就会出现下图的吃字问题:👇

image.png

用户输入的数据超过一定长度后:

image.png

开始吃该行最前面的字了。好在虽然无法支持屏幕改变后的调整,但是初始化的宽高是可以兼容的。所以每次打开xterm之前,把当前浏览器中,容器可以适配的宽度和列计算出来做适配。 第2部分的代码中,就要做出如下修改:

// Xterm初始化宽度获取后-不再兼容后续窗口变动
let width = document.body.clientWidth;
let cols = parseInt((width - 70) / 8.5, 10);

// WebSocket 连接已建立部分
const runRealTerminal = () => {
  let message = {
    side: 'xx',
    token: xx,
    tunnelId: xx,
    terminalMetadata: { width: cols, height: 20 }// 此处要把计算出来的列传给后端
  };
  let newMessage = JSON.stringify(message) + '\n';
  terminalSocket.value.send(newMessage); 
  ws_heartCheck.start();
  loading.value = false; 
};
  
  
  const initTerm = () => {
  term.value = new Terminal({
    cols: cols, // 此处要同步前端xterm的列数
    rows: 20,
    fontSize: 14,
    wraparoundMode: true, 
    fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
    theme: {
      background: '#181d28'
    },
    cursorBlink: true,
    cursorStyle: 'underline',
    scrollback: 999999
  });
  term.value.open(terminal.value); 
  termData(); 
};

4.webSocket心跳机制

webSocket心跳机制是前后端约定一条信息,前端定时发送给后端,比如前端发“p”,后端回“q”,前端对这条数据进行过滤操作即可,只要保证前后端定时有数据交互,webSocket不会断就行。

实际上,前端给后端定时发数据后,后端不返信息,也能做到不断联。这里比较特殊的点是,如果和后端约定了发送的数据,后端会无差别的视为用户输入的信息,然后在xterm里面回显。换句话说:后端无法分离用户输入的信息和心跳机制发送的信息。所以在这里规定好发一个空的byte数组,后端不给反馈,保持长连接即可。本次需求不需要服务器超时定时器。

// WebSocket心跳检测机制- 规则:前端每20秒发送一个空的byte数组,保持长连接即可,后端不返数据(无法区分是心跳数据还是用户输入的数据)
var ws_heartCheck = {
  timeout: 20000, // 20秒一次心跳
  timeoutObj: null, // 执行心跳的定时器
  // serverTimeoutObj: null, // 服务器超时定时器
  reset: function () {
    // 重置方法
    clearInterval(this.timeoutObj);
    // clearTimeout(this.serverTimeoutObj);
    return this;
  },
  start: function () {
    // 启动方法
    var self = this;
    this.timeoutObj = setInterval(function () {
      const pingBuffer = new ArrayBuffer(0);
      terminalSocket.value.send(pingBuffer);
      // 如果超过一定时间还没重置,说明后端主动断开了
      // self.serverTimeoutObj = setTimeout(function () {
      // 如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
      // terminalSocket.value.close();
      // }, self.timeout);
    }, this.timeout);
  }
};

四、参考资料

参考的资料和遇到的问题,都记录在此,感谢各位大佬的分享和讨论 1.blog.csdn.net/weixin_3831… #xterm.js + vue + websocket实现终端功能(xterm 3.x+xterm 4.x) 2.blog.csdn.net/Myron_007/a… #xterm使用 3.blog.csdn.net/yzding1225/… #vue3 终端实现 (vue3+xterm+websocket) 4.blog.csdn.net/qq_41840688… #vue+xterm.js实现webssh踩坑之旅 5.www.cnblogs.com/goloving/p/… #浅析如何使用前端终端组件Xterm.js制作一个web terminal及遇到的元素自适应、字符删除与上下键切换命令等问题 6.zhuanlan.zhihu.com/p/571871543 #前端实现webssh 7.www.cnblogs.com/shymi/p/170… #vue2+xterm编写多开网页shell 8.blog.csdn.net/weixin_4632… # Vue websocket的封装及使用 9.blog.csdn.net/2303_770721… # Vue2/Vue3使用WebSocket 心跳检测(开箱即用,附带原理) 10.blog.csdn.net/weixin_5741… # vue3使用websoket长连接,心跳机制方法封装 11.cloud.tencent.com/developer/a… # WebSocket断开原因、心跳机制防止自动断开连接 12.github.com/xtermjs/xte… 13.github.com/xtermjs/xte… 14.github.com/xtermjs/xte… 15.github.com/xtermjs/xte… # xterm5.3.0 replaces the first character of a line when the input character is too long 当输入字符过长时候会他替换行首字符 #4892 16.github.com/facebookarc…