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

相关推荐
codingandsleeping7 分钟前
前端工程化之webpack(万字)
前端·javascript
Jiude36 分钟前
UnoCSS presetWind4() 背景色使用 color-mix() 的原因及解决方案
前端·css
无名之逆1 小时前
Hyperlane:Rust 生态中的轻量级高性能 HTTP 服务器库,助力现代 Web 开发
服务器·开发语言·前端·后端·http·面试·rust
范哥来了1 小时前
python web开发django库安装与使用
前端·python·django
烛阴1 小时前
JavaScript 的 “new Function”:你不知道的黑魔法,让代码更灵活!
前端·javascript
ConardLi2 小时前
发布第五天,我的开源项目突破 1.7 K Star!
前端·javascript·人工智能
Moment2 小时前
京东一面:postMessage 如何区分不同类型的消息 🤪🤪🤪
前端·javascript·面试
鱼樱前端2 小时前
🔥 Vue2 vs Vue3 的 h 函数终极指南:从入门到源码级深度解析
前端·vue.js
Moment2 小时前
💯 铜三铁四,我收集整理了这些大厂面试场景题 (一)
前端·后端·面试
不能只会打代码2 小时前
六十天前端强化训练之第二十二天之React 框架 15天深度学习总结(大师版)
前端·react.js·前端框架