本文将介绍如何使用Vue.js、xterm.js和WebSocket技术来搭建一个功能齐全的网页终端应用,让你能够在浏览器中模拟命令行操作,并与后端服务器进行实时通信。
一、xterm介绍
xterm是一个使用Typescript编写的前端终端组件,可以直接在浏览器中实现一个命令行终端应用,通常与websocket
一起使用。
xterm配置详解
配置选项 | 作用 | 示例 |
---|---|---|
cols | 终端的列数,决定了终端横向显示的字符数量 | cols: 80 |
rows | 终端的行数,决定了终端纵向显示的字符行数 | rows: 24 |
cursorStyle | 光标样式,如块状、下划线等,定义光标在终端中的显示外观 | cursorStyle: 'block' 'block' | 'underline' | 'bar' |
cursorInactiveStyle | 光标处于非活动状态时的样式。可以用来区分活动和非活动状态下的光标外观 | cursorInactiveStyle: 'underline' 'outline' | 'block' | 'bar' | 'underline' | 'none' |
cursorWidth | 光标的宽度,以像素为单位。可以调整光标在终端中的粗细程度 | cursorWidth: 2 |
fontFamily | 终端使用的字体家族 | fontFamily: 'Courier New' |
fontSize | 终端使用的字体大小,以像素为单位 | fontSize: 14 |
fontWeight | 字体的粗细权重。可以是数值(如 400 表示正常,700 表示粗体)或者字符串(如 'normal'、'bold') | fontWeight: 'normal' |
letterSpacing | 字符间距,以像素为单位。用于调整终端中字符之间的间距 | letterSpacing: 1 |
lineHeight | 行高,以像素为单位或者相对于字体大小的比例。用于定义终端中每行的高度 | lineHeight: '1.5' |
convertEol | 是否转换行结束符。不同操作系统可能使用不同的行结束符(如Windows使用 \r\n,Linux使用 \n),设置此属性可以将输入的行结束符转换为终端内部统一使用的格式 | convertEol: true |
cursorBlink | 是否让光标闪烁。用于设置光标在终端中的视觉效果 | cursorBlink: false |
wordSeparator | 单词分隔符。用于确定在终端中哪些字符被视为单词的分隔符,这在处理文本选择、单词操作等方面很重要 | wordSeparator: ' \t\n' |
theme | 终端的主题,设置终端的样式,他的子属性有: background-终端的背景色。可以是十六进制颜色代码、RGB颜色代码或颜色名称 cursor-光标的颜色 foreground-终端的前景色,即文本颜色 | theme: { background: '#000000', cursor: '#FFFFFF', foreground: '#FFFFFF' } |
termName | 终端的名称,可能用于标识终端类型或在特定的终端相关操作中作为名称使用 | termName: 'MyCustomTerminal' |
disableStdin | 是否禁用标准输入(用户输入)。这在某些情况下,如只用于显示输出而不需要用户交互的终端场景中可能会用到 | disableStdin: false |
scrollback | 滚动缓冲区的大小,即终端可以向上滚动查看的历史行数 | scrollback: 1000 |
cancelEvents | 是否取消某些默认的事件行为。这可以用于定制终端对用户操作的响应,避免一些默认的浏览器行为干扰终端操作 | cancelEvents: true |
fontWeightBold | 专门用于定义粗体字的权重值或样式 | fontWeightBold: 700 |
customGlyphs | 是否使用自定义的字形(字符图形)。可用于显示特殊字符或自定义的字符外观 | customGlyphs: false |
allowTransparency | 是否允许终端具有透明效果。这在创建具有特殊视觉效果的终端界面时可能会用到。 | allowTransparency: true |
altClickMovesCursor | 当按下 Alt 键并点击鼠标时是否移动光标。用于定义特定鼠标操作与光标的交互方式 | altClickMovesCursor: false |
drawBoldTextInBrightColors | 是否以明亮颜色绘制粗体文本。用于定义粗体文本的显示颜色模式 | drawBoldTextInBrightColors: true |
fastScrollModifier | 快速滚动的修饰键(如与 Ctrl 或 Shift 等键组合)。用于定义快速滚动终端内容时的操作方式 | fastScrollModifier: 'Ctrl' |
fastScrollSensitivity | 快速滚动的敏感度,决定了滚动速度等相关参数 | fastScrollSensitivity: 5 |
ignoreBracketedPasteMode | 是否忽略方括号粘贴模式。这在处理粘贴文本时可能会涉及到特殊的粘贴处理逻辑 | ignoreBracketedPasteMode: true |
linkHandler | 链接处理程序,用于处理终端中的链接点击事件。例如,可以定义点击链接时打开新页面或者执行特定的脚本 | linkHandler: function (url) { window.open(url); } |
logLevel | 日志记录的级别,如 'debug'、'info'、'warn'、'error' 等。用于控制终端内部日志输出的详细程度 | logLevel: 'info' |
logger | 日志记录器对象,用于处理终端内部的日志记录操作 | 较复杂,一般不直接设置,更多是由内部使用基于 logLevel 进行操作 |
macOptionClickForcesSelection | 在Mac系统中,按下 Option 键点击是否强制选择文本。用于特定操作系统下的鼠标操作与文本选择交互的定义 | macOptionClickForcesSelection: false |
macOptionIsMeta | 在Mac系统中,是否将 Option 键视为 Meta 键。这在处理快捷键等操作时,对于与Mac系统的交互逻辑定义很重要 | acOptionIsMeta: true |
minimumContrastRatio | 最小对比度比率,用于确保终端的颜色对比度满足一定的可读性要求 | minimumContrastRatio: 4.5 |
overviewRulerWidth | 概览标尺(如果存在的话)的宽度,以像素为单位。这在具有特定界面布局,如带有侧边栏或标尺功能的终端界面中可能会用到 | overviewRulerWidth: 10 |
rightClickSelectsWord | 右键点击是否选择单词。用于定义右键点击鼠标在终端中的操作效果 | rightClickSelectsWord: true |
screenReaderMode | 屏幕阅读器模式。这在使终端能够更好地与屏幕阅读器软件协作,为视力障碍者提供无障碍访问方面很重要 | screenReaderMode: false |
scrollOnUserInput | 是否在用户输入时滚动终端内容。这在处理用户输入和终端显示滚动逻辑方面很重要 | scrollOnUserInput: true |
scrollSensitivity | 滚动敏感度,决定了终端内容滚动的速度或距离与用户操作(如鼠标滚轮滚动)之间的关系 | scrollSensitivity: 3 |
smoothScrollDuration | 平滑滚动的持续时间,以毫秒为单位。用于定义终端内容平滑滚动时的动画效果持续时间 | smoothScrollDuration: 200 |
tabStopWidth | 制表符(Tab)的停止宽度,以字符数或像素为单位。用于定义终端中制表符的显示宽度 | tabStopWidth: 4 |
windowOptions | 启用各种窗口操作和报告功能。出于安全原因,所有功能默认禁用。 | 较复杂,通常由终端内部根据具体需求使用,一般不直接设置简单的值 |
windowsMode | 是否启用“Windows模式”。因为Windows后端是winpty和conty通过在它们的一侧进行换行操作,xterm.js不这样做可以访问换行。 当启用Windows模式时,执行如下操作: 1.关闭Reflow 2.如果该行的最后一个字符不是空白,则假定该行已换行 | windowsMode: false |
windowsPty | 与Windows系统中的伪终端(Pseudo - Terminal)相关的设置。这在处理Windows系统下的终端输入输出和进程交互方面可能会用到。 | 较复杂,通常由终端内部根据Windows系统相关逻辑使用,一般不直接设置简单的值 |
xterm的常用方法
方法 | 说明 |
---|---|
open(parent: HTMLElement) | 打开终端 |
dispose() | 清理和销毁终端 |
write(data: string | Uint8Array, callback?) | 向终端写入数据 -data:字符串或Uint8Array的byte数组 -callback可选回调,在解析器处理数据时触发 |
writeln(data: string | Uint8Array, callback?: ()) | 向终端写入数据,后跟一个换行符(\n) |
resize(columns: number, rows: number) | 手动调整终端大小 |
blur() | 终端失去焦点 |
focus() | 终端聚焦 |
paste(data: string) | 将文本写入终端,对粘贴的文本执行必要的转换 |
clear() | 清除整个缓冲区,使提示行成为新的第一行 |
hasSelection(): boolean | 判断终端是否有选中的文本 |
getSelection(): string | 获取终端选中的文本 |
getSelectionPosition(): IBufferRange | undefined | 获取选中文本的位置,如果没有选择,则返回未定义 |
clearSelection() | 清除终端选中的文本 |
select(column: number, row: number, length: number) | 在终端中选择文本 |
selectAll() | 选择终端中的所有文本 |
selectLines(start: number, end: number) | 在两行之间的缓冲区中选择文本 |
scrollLines(amount: number) | 滚动终端的显示 -amount:向下滚动的行数,为负时向上滚动 |
scrollPages(pageCount: number) | 将终端滚动若干页 -pageCount:要滚动的页面数,为负时向上滚动 |
scrollToTop() | 将终端滚动到顶部 |
scrollToBottom() | 将终端滚动到底部 |
refresh(start: number, end: number) | 告诉呈现器在下次时机刷新两行(包括两行)之间的终端内容 |
reset() | 重置终端到原始状态,执行完全复位 |
clearTextureAtlas() | 清除终端画布,如果它是活动的。(这样做将强制重新绘制所有的字形,这可以解决导致纹理损坏的问题,例如从睡眠中恢复操作系统时,纹理会变得混乱。) |
xterm的常用事件
onBell
当终端产生铃声事件(bell event)时触发该回调函数。铃声事件在终端中通常用于提示用户某些特定情况,例如有重要的通知或者错误发生
示例:
term.onBell((event) => {
// 这里可以添加当铃响时要执行的操作,例如播放声音或者显示特定的提示信息
console.log('终端铃响了,可能有重要通知!');
});
onBinary
当终端接收到二进制数据时触发该回调函数。这在处理一些特殊的二进制输入,如从某些设备或特定协议接收的数据时非常有用
示例:
term.onBinary((event) => {
// event可能包含接收到的二进制数据等信息
console.log('接收到二进制数据:', event.data);
})
onCursorMove
当终端中的光标位置发生移动时触发该回调函数。这可以用于跟踪用户在终端中的光标操作,例如实现一些与光标位置相关的交互逻辑或者记录用户的操作轨迹
示例:
term.onCursorMove((event) => {
// event可能包含新的光标位置信息,如列数(col)和行数(row)等
console.log('光标移动到:', event.col, event.row);
});
onData
当终端接收到任何数据(文本数据或其他可解释为数据的输入)时触发该回调函数。这是处理用户输入或者从其他来源接收到数据到终端的主要入口点
示例:
term.onData((data) => {
// data是接收到的数据内容,可以在这里对数据进行处理,如解析、执行命令等
console.log('接收到数据:', data);
});
onKey
当终端检测到键盘按键事件时触发该回调函数。这有助于处理用户的键盘输入,例如执行命令、控制终端功能或者处理快捷键操作
示例:
term.onKey((event) => {
// event包含按键相关的信息,如按键码(keyCode)、是否按下(isDown)等
console.log('按下了键盘按键:', event.keyCode);
// 根据按键信息可以执行相应的操作,如判断按下的是回车键就执行命令
if (event.keyCode === 13) {
// 假设这里执行一个自定义命令 term.write('执行自定义命令\n');
}
});
onLineFeed
当终端检测到行馈送(line feed)事件时触发该回调函数。行馈送通常表示一行文本的结束,这在处理文本输入和格式化方面非常有用
示例:
term.onLineFeed((event) => {
// 可以在这里对新的一行进行处理,如重新排版或者记录行数
console.log('发生行馈送事件');
});
onRender
当终端进行重新渲染(例如,由于数据更新、窗口大小改变等原因导致终端内容需要重新绘制)时触发该回调函数。这可以用于在终端重新渲染前后执行一些额外的操作,如更新与终端显示相关的状态或者进行性能优化
示例:
term.onRender((event) => {
// 可以在这里进行一些与渲染相关的操作,如更新自定义的显示效果或者统计渲染次数
console.log('终端正在重新渲染');
});
onResize
当终端的大小(例如,列数、行数或者整个窗口的大小)发生改变时触发该回调函数。这有助于在终端大小改变时调整相关的布局或者重新计算显示内容
示例:
term.onResize((event) => {
// 可以根据新的大小调整终端内的布局,如重新排列文本或调整元素的大小
console.log('终端大小发生改变,新的大小为:', event.cols, '列', event.rows, '行');
});
onScroll
当终端内容发生滚动时触发该回调函数。这对于跟踪用户的滚动操作、实现滚动相关的功能(如滚动到特定位置显示特定信息)或者优化滚动性能非常有用
示例:
term.onScroll((event) => {
// 可以根据滚动方向和位置执行相应操作,如加载更多内容或者显示滚动提示
console.log('终端内容正在滚动,滚动方向:', event.direction);
});
onSelectionChange
当终端中的文本选择发生改变(例如,用户通过鼠标或键盘操作选择了不同的文本区域)时触发该回调函数。这可以用于处理与文本选择相关的操作,如复制、粘贴或者显示选择的文本内容
示例:
term.onSelectionChange((event) => {
// 可以对选择的文本进行处理,如将其复制到剪贴板或者进行文本分析
console.log('文本选择发生改变,选择的文本为:', event.selectedText);
});
onTitleChange
当终端的标题(title)发生改变时触发该回调函数。终端标题可能用于标识终端的状态、正在执行的任务或者其他相关信息
示例:
term.onTitleChange((event) => {
// 可以根据新的标题更新界面上与终端标题相关的显示或者执行特定的操作
console.log('终端标题发生改变,新的标题为:', event.title);
});
onWriteParsed
当终端完成对写入数据的解析后触发该回调函数。这在处理复杂的输入数据或者对写入终端的数据进行特殊处理(如加密、格式化等)之后进行后续操作时非常有用
示例:
term.onWriteParsed((event) => {
// 可以根据解析后的数据进行进一步的操作,如存储到数据库或者发送到其他服务
console.log('写入的数据已解析,解析后的数据为:', event.parsedData);
});
二、代码实现
1.安装相关插件
npm install xterm xterm-addon-fit
2.具体代码
<template>
<div ref="terminal" id="terminal"></div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { debounce } from 'lodash'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import 'xterm/css/xterm.css'
const terminal = ref(null)
const fitAddon = new FitAddon()
const socket = ref(null)
const term = ref(null)
// 初始化WS
const initWebSocket = () => {
socket.value = new WebSocket(`wss://XXXX`)
socket.value.onopen = () => {
// WebSocket 连接已建立
console.log("Websocket open");
}
socket.value.onmessage = (event) => {
// WebSocket收到服务器消息
const data = JSON.parse(event.data)
console.log("WebSocket message:", data);
// 根据返回消息类型处理
if(data.type ==='output'){
// 解码 Base64 回到中文字符串
const decodedstring = new TextDecoder("utf-8").decode(
Uint8Array.from(atob(data.output), c=>c.charcodeAt(0))
)
// 服务端ssh输出, 写到web shell展示
term.value.write(decodedstring);
}
}
socket.value.onclose = () => {
// WebSocket 连接已关闭
console.log("Websocket close");
}
socket.value.onerror = (error) => {
// WebSocket 连接出错
console.log("Websocket error:", error);
}
}
// 初始化Terminal
const initTerm = () => {
// 创建终端
term.value = new Terminal({
rendererType: 'canvas', // 渲染类型
rows: 30, // 行数
cols: 80, // 列数
convertEol: true, // 启用时,光标将设置为下一行的开头
disableStdin: false, // 是否应禁用输入
windowsMode: true, // 根据窗口换行
cursorStyle: 'underline', // 光标样式
cursorBlink: true, // 光标闪烁
theme: {
foreground: '#ECECEC', // 字体颜色
background: '#000000', // 背景色
cursor: 'help', // 设置光标
lineHeight: 20, // 行高
},
});
term.value.open(document.getElementById('terminal')) // 挂载dom窗口
term.value.loadAddon(fitAddon) // 自适应尺寸
// 不能初始化的时候fit,需要等terminal准备就绪,可以设置延时操作
setTimeout(() => {
fitAddon.fit()
}, 5)
term.value.focus() // 自动聚焦
termData() // Terminal 事件挂载
}
// 终端输入触发事件
const termData = () => {
// 当向web终端敲入字符时候的回调
term.value.onData((data) => {
console.log(data, '传入服务器')
const msg = { type: 'input', input: data }
socket.value.send(JSON.stringify(msg))
})
// 终端尺寸变化触发
term.value.onResize(() => {
resizeRemoteTerminal()
})
}
// 终端字体写入样式
const 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:白色)
term.value.write(`\x1B[${fontCss}${bgColor}${txt}\x1B[0m`);
}
//尺寸同步 发送给后端,调整后端终端大小,和前端保持一致,不然前端只是范围变大了,命令还是会换行
const resizeRemoteTerminal = () => {
const { cols, rows } = term.value
const msg = { type: 'resize', rows: rows, cols: cols }
// 把web终端的尺寸term.rows和term.cols发给服务端,通知sshd调整输出宽度
socket.value.send(JSON.stringify(msg))
}
// 适应浏览器尺寸变化
const fitTerm = () => {
fitAddon.fit()
}
const onResize = debounce(() => fitTerm(), 500)
const onTerminalResize = () => {
window.addEventListener('resize', onResize)
}
const removeResizeListener = () => {
window.removeEventListener('resize', onResize)
}
// 销毁webSocket和Terminal
const closeSocketAndTerm = () => {
socket.value && socket.value.close()
term.value && term.value.dispose(document.getElementById('terminal'))
}
onMounted(() => {
initWebSocket()
initTerm()
onTerminalResize()
})
onBeforeUnmount(() => {
removeResizeListener()
closeSocketAndTerm()
})
</script>
<style lang="scss" scoped>
#terminal {
width: 100%;
height: 100%;
}
</style>