随笔之 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...

相关推荐
哟哟耶耶1 天前
component-svg圆环进度百分比图(顶部文本,中间图形,底部文本)
前端·css·echarts
不想秃头的程序员1 天前
Vue3 中的 <keep-alive> 详解
前端·vue.js
其尔Leo1 天前
Vue3可动态添加行el-table组件
前端
紫小米1 天前
webpack详解和实操
前端·webpack·node.js
不想秃头的程序员1 天前
JavaScript 中的深拷贝与浅拷贝详解
前端·面试
风止何安啊1 天前
用 10 行代码就能当 “服务器老板”+“网络小偷”+“文件管家”?Node.js:别不信!
前端·javascript·node.js
昨晚我输给了一辆AE861 天前
react-hook-form 初始化值为异步获取的数据的最佳实践
前端·react.js·强化学习
PieroPC1 天前
NiceGUI 内置Material Design图标库
前端
Cache技术分享1 天前
276. Java Stream API - 使用 flatMap 和 mapMulti 清理数据并转换类型
前端·后端
inferno1 天前
CSS 基础(第一部分)
前端·css