一、前言
为了实现web terminal终端需求,调研了几款web terminal插件,还是xterm社区最活跃,技术最成熟,网络资源最多,就是官方文档很让人吐槽,所以开发完后,自己也整理一下,当笔记写。
具体需求是先点击按钮,打开xterm小黑框,初始化xterm和webSocket,并且需要在连接没有断的情况下监听浏览器刷新事件,需要弹出二次确认框阻断当前的刷新操作。
二、开发流程
- 引入xterm插件
- xterm+webSocket实现双向交互
- 屏幕适配
三、代码实现
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插件。
但是!!因为需求比较特殊,后端无法修改虚拟终端的大小,需要前端独立适配屏幕,所以就会出现下图的吃字问题:👇
用户输入的数据超过一定长度后:
开始吃该行最前面的字了。好在虽然无法支持屏幕改变后的调整,但是初始化的宽高是可以兼容的。所以每次打开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…