随笔之 react 接入 @xterm 的踩坑记录

前言

最近公司有一个在 web 上调用终端的需求。我们使用的技术栈 react + ts,调研了一下决定用 xterm 这个库来实现。做的过程中踩了一些坑,稍作整理分享一下。

看一下最终的效果

错误理解重灾区

在开始之前,你是否遇到了这些问题:

  • 一片空白
  • 编辑器最前面出现了一个框
  • 无法删除,删除就报错
  • 光标莫名跑到最前方,覆盖最前方的字符
  • 很奇怪的换行,在中间部分折断了
  • 接收到的消息没法正常处理和展示
  • 不能输入数字
  • ...

所以必须要说清楚几个错误点,否则要折腾很久

一、引入组件就开干

要额外引入css...否则你会看到莫名其妙的样式

、直接将其当做一个 input 来 交互

绝大部分同学看到这个终端需求的第一个想法一定是:

  1. 找一个库,它提供一个类似输入框一样的组件,只是稍作封装
  2. 这个输入框负责输入、记录、回车后 ws 发送消息
  3. ws 接收到消息时显示响应

但实际上完全不是!!!差的非常远,官方也不推荐这种使用方案。如果按这种方案做,会遇到相当多的问题,说多了都是泪。

正确的做法应该是:

  1. 用户按下键盘,不管按什么都立刻传给后端,也就是每按一下就传到后端一下
  2. 后端使用 pty,并且也直接传到对应服务器
  3. 绑定 ws 的 onmessage,后端向前端推送消息时,立刻更新终端

这样就 ok 了,不需要考虑什么删除、换行、组合键等等一系列操作。如果按这种思路,能躲开大多数的坑。

如果迫不得已需要按刚才那种错误观点来做,可以参考这篇文章,需要做很多额外处理,非必须情况还是不推荐的juejin.cn/post/708156...

三、将其当做普通的标签来调长宽大小

一旦按这种想法,你会发现诡异的换行,莫名的覆盖,删除可以删掉前面的提示等等一系列坑。

如果想调长宽的样式,必须要后端同时来支持。 这可太新鲜了,最开始完全没想到。

最大的几个坑就是这里,搞清楚以后就容易了。

步骤

  1. 引入
js 复制代码
npm i @xterm/xterm

import { Terminal } from '@xterm/xterm';
import '@xterm/xterm/css/xterm.css';
  1. 放一个容器供注入
html 复制代码
<div ref={terminalDomRef}></div>
  1. 初始化终端
js 复制代码
  const terminalDomRef = useRef<HTMLDivElement>(null);

  const initTerminal = () => {
    if (termRef.current) {
      termRef.current.dispose();
    }

    termRef.current = new Terminal(options);
    if (terminalDomRef.current) {
      termRef.current.open(terminalDomRef.current);
      termRef.current.loadAddon(fitAddon.current);
    }
  }
  
  useEffect(() => {
    initTerminal();
  }, [])

到这一步就可以看到完整的终端了,不过只是终端的 dom,我们还需要考虑连接 websocket 部分。

这一部分有个插件,但是我最开始没发现,所以没有使用,

  1. 初始化 websocket
js 复制代码
  const initSocket = async () => {
    try {
      // 这里我做了封装,具体情况具体改造,只要连接上就行
      wsRef.current = await wss('xxxx'});

      wsRef.current.onopen = () => {
        setLogStatus('success');
        resetTerminal();
      };

      wsRef.current.onmessage = (event) => {
        if (!termRef.current) return;
        try {
          // 对象/数字,数字没法输入的同学可以看这里的逻辑
          const data = JSON.parse(event.data);
          termRef.current.write(data.output || String(data));
        } catch {
          // 字符串
          termRef.current.write(event.data);
        }
      };
      
    } catch (error) {
      Message.error('连接终端失败');
    }
  };
  
  useEffect(() => {
    initSocket();
  })

此时,只要后端按部就班,那你的终端已经可以正常读写了。但我们还需要考虑一下样式问题,当该终端的长宽发生变化时,页面会变的非常诡异,主要原因是后端的行数、列数没有匹配上,所以我们修改长宽时,要计算出对应的行数、列数并告知后端。

js 复制代码
const fitAddon = useRef<FitAddon>();

// initTerminal 时
{
    ...
    fitAddon.current = new FitAddon();      
    termRef.current.loadAddon(fitAddon.current);
    ...
}
    
const resize = debounce ( function  onResize () { fitAddon. current ?. fit ();  const { cols } = termRef. current !;  const windowHeight = window . innerHeight ;  const rows = Math . floor (windowHeight / 19 ); // 这个需要自己看下自己每行多高  termRef. current ?. resize (cols, rows); wsRef. current ?. send ( JSON . stringify ({ type : 'resize' , cols, rows })); }, 500 );

  useEffect(() => {
    initTerminal();
    window.addEventListener('resize', resize);

    return () => {
      if (termRef.current) {
        termRef.current.dispose();
      }
      window.removeEventListener('resize', resize);
    };
  }, []);

至此,大功告成。


其他

做的过程中主要参考了这两篇文章

blog.csdn.net/qq_51287641...

juejin.cn/post/691891...

相关推荐
八了个戒4 分钟前
「数据可视化 D3系列」入门第八章:动画效果详解(让图表动起来)
开发语言·前端·javascript·数据可视化
拉不动的猪1 小时前
无缝适配 PC 和移动端‌我们要注意哪些点呢
前端·javascript·面试
酱酱们的每日掘金2 小时前
🔥 4 月精选:AICoding Cursor上新与 MCP 实战揭秘!- AI Coding 周刊第 5 期
前端·ai编程·mcp
天天扭码2 小时前
一分钟解决 | 高频面试算法题——和为 K 的子数组(前缀和)
前端·算法·面试
搞瓶可乐2 小时前
鸿蒙ArkUI之布局实战,线性布局(Column,Row)、弹性布局(Flex)、层叠布局(Stack),详细用法
前端·harmonyos·鸿蒙系统·arkui·弹性布局·布局实战·堆叠布局
Aphasia3113 小时前
小厂面试常考算法题整合(一)✍🏻
前端·算法·面试
五月仲夏3 小时前
React基础知识(补充中)
前端·react.js·前端框架
王富贵的记录3 小时前
React 函数组件和类组件的区别
前端·javascript·react.js
yuhaiqiang3 小时前
在公司写代码是工作,在开源社区写代码是生活
前端·后端
左耳咚3 小时前
Egg.js 服务端 HTML 强缓存问题排查与解决
前端·egg.js