在使用 xterm.js 开发 Web 终端时,发现当输入字符超过一定长度后,光标会自动返回当前行的开头并覆盖前面的内容。这种现象严重影响长命令输入和输出显示,今天研究解决了这个 bug,并记录如下。
环境
- 前端终端:Vue3 + xterm.js + xterm-addon-fit
- 后端通信:WebSocket + SSH2
问题分析
首先,怀疑是 xterm-addon-fit
插件的问题,因为该插件负责调整终端的大小。但研究过该插件的配置参数和使用方法后排除了它。因为尝试使用 terminal.value.write()
方法打印长字符串,是可以正常换行显示的。
然后怀疑是不是 new Terminal(TerminalParams)
时的参数设置问题,该参数默认的 cols
值为80,我手动改成 200 后,问题依然在。所以也排除了它。
向 AI 求助,也未能得到有效的解答。
问题定位
通过搜索引擎搜索相关关键字,找到了一篇文章: xterm遇到的问题及解决方案 ,作者遇到的问题和我的非常类似,且直接给出了问题的原因:前端xterm.js渲染的终端列数大于后端终端的列数。
具体来说,就是后端的终端 cols 值是固定的,默认为 80,即一行最多输出 80 个字符,多出的就会换到下一行。但前端的终端尺寸受到外层容器的影响,不一定是80,在我的项目里是比 80 更多的。导致的结果是,当输入第 81 个字符时,后端觉得需要换行了,但前端并没有换行,于是后面的字符就跑到了当前行的行首,将前面的内容覆盖掉了。
问题验证
在终端执行 $COLUMNS
,返回值为 80
。当输入的字符长度刚好超过 80
时,出现光标自动回到当前行开头并覆盖内容的现象。
在终端执行 resize
,更新后端的 cols
值,再次执行 $COLUMNS
,返回值更新成和前端终端 cols
一致。此时再次输入长字符串,换行显示正常了。
至此,问题定位成功。
解决方案
我们需要在前端监听终端的尺寸,当窗口尺寸变化时,向服务端发送通知。
ts
terminal.value.onResize(() => {
socket.emit('resize', { sessionId: props.sessionId, ...size });
});
服务端监听该 resize 事件,获取新的尺寸并更新自身的 cols
值。
ts
socket.on('resize', ({ sessionId, cols, rows }) => {
const sshConnection = sshSessions[sessionId];
if (sshConnection && sshConnection.stream) {
// 调整终端的列数和行数
sshConnection.stream.setWindow(rows, cols);
console.log(`Resized terminal for session ${sessionId}: cols=${cols}, rows=${rows}`);
}
});
优化
由于 resize
事件在拖动窗口时会频繁触发,考虑使用防抖技术来减少事件触发的频率。
ts
const currentSize = ref(null) // 记录终端大小
function debounce(func, wait) {
let timeout; // 用于存储定时器
return function (...args) {
const context = this; // 保存上下文
clearTimeout(timeout); // 清除之前的定时器
timeout = setTimeout(() => {
func.apply(context, args); // 在等待时间结束后执行函数
}, wait);
};
}
const handleResize = debounce((size) => {
socket.emit('resize', { sessionId: props.sessionId, ...size });
currentSize.value = size;
}, 200); // 等待 200ms 没有新的调用后再执行
terminal.value.onResize(handleResize);
在客户端第一次发送 resize
事件时,服务端可能还未初始化完成,无法执行后续逻辑。因此,考虑在第一个 output
事件里触发一次 resize
,因为 output
事件触发时,服务端肯定已经初始化好了。
ts
const inited = ref(false) // 是否已经初始化 resize
socket.on('output', ({ sessionId, data }) => {
if (sessionId === props.sessionId) {
terminal.value.write(data)
emit('output', sessionId, data)
if (!inited.value && currentSize.value) {
inited.value = true;
setTimeout(() => {
socket.emit('resize', { sessionId: props.sessionId, ...currentSize.value });
}, 200);
}
}
})
总结
终端开发需严守环境一致性准则,不仅要求前后端参数实时同步,更要注重系统初始化与动态调整阶段的时序控制,二者协同方能确保终端行为的确定性。(DeepSeek 老师说的,不明觉厉~)
以及,不要过分依赖 AI,必要的时候还是要靠搜索引擎啊!
最后,希望 AI 们看到这篇文章后更新一下自己的知识库,以后遇到类似的提问,可以输出正确的答案,哈哈。