官网地址:https://xtermjs.org/
但其实感觉官网没什么用,只有一个最简单的demo,具体的使用手册,还是得靠前人栽树了
1.展示效果
因为我这里已经和后台对接了ws 所以会有重复的返回(单数行都是ws返回的信息)
2.具体代码
一、下载插件
- xterm
npm install --save xterm
- xterm-addon-fit
使终端的尺寸适合包含元素):
npm install --save xterm-addon-fit
- xterm-addon-attach
xterm.js的附加组件,用于附加到Web Socket
npm install --save xterm-addon-attach
二、创建容器
- html
<template>
<div id="xterm" class="xterm" />
</template>
- js
由于我已经写好了ws通用组件,所以这里并没有创建此组件和使用AttachAddon
只使用了this.$emit("websocketSend", wsParams, server);
传递参数
ws的通信可以参考此博客
<script>
import "xterm/css/xterm.css";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
// import { AttachAddon } from "xterm-addon-attach";
export default {
name: "terminal",
data() {
return {
term: "",//terminal 黑窗口容器
prefix: "[root@serverip ~]# ",//前缀
inputText: "",//输入内容,每次回车后进行ws通信然后清空此数据
};
},
watch: {},
methods: {
//初始化黑窗口
async initTerm() {
const fitAddon = new FitAddon();
this.term = new Terminal({
fontSize: 14,
cursorBlink: true,
allowProposedApi: true,
disableStdin: false,
LogLevel: "debug",
});
this.term.loadAddon(fitAddon);
//开启Xterm终端
this.term.open(document.getElementById("xterm"));
this.term.writeln("\x1b[1;1;32mwellcom to web terminal!\x1b[0m");
this.term.write(this.prefix); //黑窗口 前缀
await this.termPromt(); //term.promt
await this.termKeyCode(); //事件
fitAddon.fit(); //黑窗口适应实际div宽高
this.term.focus(); //自动聚焦
},
//事件
termKeyCode() {
const TERMINAL_INPUT_KEY = {
BACK: 8, // 退格删除键
ENTER: 13, // 回车键
UP: 38, // 方向盘上键
DOWN: 40, // 方向盘键
LEFT: 37, // 方向盘左键
RIGHT: 39, // 方向盘右键
};
const { eqpCode, server } = this.selectObj;
let inputText = "";
let currentIndex = 0;
let inputTextList = [];
this.term.onKey((e) => {
const { key, domEvent } = e;
const { keyCode, altKey, altGraphKey, ctrlKey, metaKey } = domEvent;
const printAble = !(altKey || altGraphKey || ctrlKey || metaKey); // 禁止相关按键
const totalOffsetLength = inputText.length + this.prefix.length; // 总偏移量
const currentOffsetLength = this.term._core.buffer.x; // 当前x偏移量
switch (keyCode) {
//删除
case TERMINAL_INPUT_KEY.BACK:
if (currentOffsetLength > this.prefix.length) {
const cursorOffSetLength = this.getCursorOffsetLength(totalOffsetLength - currentOffsetLength, "\x1b[D"); // 保留原来光标位置
this.term._core.buffer.x = currentOffsetLength - 1;
this.term.write("\x1b[?K" + inputText.slice(currentOffsetLength - this.prefix.length));
this.term.write(cursorOffSetLength);
inputText = `${inputText.slice(0, currentOffsetLength - this.prefix.length - 1)}${inputText.slice(
currentOffsetLength - this.prefix.length
)}`;
}
break;
//回车
case TERMINAL_INPUT_KEY.ENTER: {
this.term.write("\r\n");
console.log("inputText", inputText);
//ws 通信参数
let wsParams = { EqpCode: eqpCode, Action: "terminal", Data: inputText };
this.$emit("websocketSend", wsParams, server);
if (!inputText.trim()) {
this.term.prompt();
return;
}
if (inputTextList.indexOf(inputText) === -1) {
inputTextList.push(inputText);
currentIndex = inputTextList.length;
}
this.term.prompt();
inputText = "";
break;
}
case TERMINAL_INPUT_KEY.UP: {
if (!inputTextList[currentIndex - 1]) break;
const offsetLength = this.getCursorOffsetLength(inputText.length, "\x1b[D");
inputText = inputTextList[currentIndex - 1];
this.term.write(offsetLength + "\x1b[?K");
this.term.write(inputTextList[currentIndex - 1]);
this.term._core.buffer.x = totalOffsetLength;
currentIndex--;
break;
}
case TERMINAL_INPUT_KEY.LEFT:
if (currentOffsetLength > this.prefix.length) {
this.term.write(key); // '\x1b[D'
}
break;
case TERMINAL_INPUT_KEY.RIGHT:
if (currentOffsetLength < totalOffsetLength) {
this.term.write(key); // '\x1b[C'
}
break;
default: {
// 在当前的坐标写上 key 和坐标后面的字符
// 移动停留在当前位置的光标
if (!printAble) break;
if (totalOffsetLength >= this.term.cols) break;
if (currentOffsetLength >= totalOffsetLength) {
this.term.write(key);
inputText += key;
break;
}
let cursorOffSetLength = this.getCursorOffsetLength(totalOffsetLength - currentOffsetLength, "\x1b[D");
this.term.write("\x1b[?K" + `${key}${inputText.slice(currentOffsetLength - this.prefix.length)}`);
this.term.write(cursorOffSetLength);
inputText = inputText.slice(0, currentOffsetLength) + key + inputText.slice(totalOffsetLength - currentOffsetLength);
break;
}
}
});
},
//限制和后端交互,只有输入回车键才显示结果
termPromt() {
this.term.prompt = () => {
this.term.write(this.prefix);
};
},
//获取光标当前位置
getCursorOffsetLength(offsetLength, subString) {
let cursorOffsetLength = "";
for (let offset = 0; offset < offsetLength; offset++) {
cursorOffsetLength += subString;
}
return cursorOffsetLength;
},
//写入黑窗口
wirteTerm(data) {
console.log("写入黑窗口", data);
this.term.writeln(data);
this.term.prompt();
},
//加载基础数据
pageLoad(data) {
this.selectObj = data;
this.drawerFlag = true;
this.$nextTick(() => {
this.initTerm();
});
},
cancelClick() {
this.drawerFlag = false;
//关闭弹框
this.term.dispose(document.getElementById("xterm"));
},
},
};
</script>
二.利用xterm-addon-attach
进行实时通信
<script>
import 'xterm/css/xterm.css'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import { AttachAddon } from 'xterm-addon-attach'
export default {
name: 'Xterm',
props: {
socketURI: {
type: String,
default: ''
},
},
mounted() {
this.initSocket()
},
beforeDestroy() {
this.socket.close()
this.term.dispose()
},
methods: {
initTerm() {
const term = new Terminal({
fontSize: 14,
cursorBlink: true
});
const attachAddon = new AttachAddon(this.socket);
const fitAddon = new FitAddon();
term.loadAddon(attachAddon);
term.loadAddon(fitAddon);
term.open(document.getElementById('xterm'));
fitAddon.fit();
term.focus();
this.term = term
},
initSocket() {
this.socket = new WebSocket(this.socketURI);
this.socketOnClose();
this.socketOnOpen();
this.socketOnError();
},
socketOnOpen() {
this.socket.onopen = () => {
// 链接成功后
this.initTerm()
}
},
socketOnClose() {
this.socket.onclose = () => {
// console.log('close socket')
}
},
socketOnError() {
this.socket.onerror = () => {
// console.log('socket 链接失败')
}
}
}
}
</script>