xterm + react + antd pro 小坑记录

3,293 阅读3分钟

场景

最近做的项目是关于私有云的,因为做的是门户部分,主要是集成公司各大产品,最近集成hadoop组件,发现有个终端的功能,之前做的门户系统,监控系统都是直接跳到服务端系统,因为服务端没有单独的终端功能,所以,需要前端自己调接口实现。

我确实没做过类似功能,经过查阅资源和询问,知道是用xterm,于是开始编码和测试,确实不难,但是中间还是有小坑,浪费了多半天时间,记录一下自己遇到的小坑。

项目环境

"react": "^16.8.6",
"react-dom": "^16.8.6",
"antd": "^3.20.0",
"umi": "^2.8.7",

antd pro用的是 客户公司要求的框架,是他们改造过的,不是标准的antd pro版本,应该是在antd pro4 ts版本上改造的。

Xterm.js

xterm.js logo

这个插件没有中文文档,它的文档看起来不怎么友好,我看的是github上xterm.js代码地址。

Xterm.js is a front-end component written in TypeScript that lets applications bring fully-featured terminals to their users in the browser. It's used by popular projects such as VS Code, Hyper and Theia.

Xterm.js是一个用TypeScript写的前端组件,让应用程序能提供给用户浏览器端全面的终端功能。它被使用在很多受欢迎的项目里,如VS Code、Hyper 、Theia。

总之一句话,就是专门做终端用的插件

使用方法,如果不是ES6模块语法,可以直接去github上看,使用方式非常简单。

问题一

如果是ES6语法,按照github上的写法,会出问题:

image-20200821102000003

我的写法:

import React, { PureComponent, Fragment } from 'react';
import { Terminal } from 'xterm';
import { WebLinksAddon } from 'xterm-addon-web-links';
import { FitAddon } from 'xterm-addon-fit'; // 根据容器大小展示合适的终端
import { AttachAddon } from 'xterm-addon-attach'; // 连接到服务器通过WebSocket的运行的进程

class TerminalCom extends PureComponent {
  constructor(props) {
    super(props);
    this.socket = new WebSocket('ws://********:0000/bch/websocket/pezy/pod_Name');
  }

  componentDidMount() {
    this.initTerminal()
  }

  initTerminal = () => {
    const terminal = new Terminal({
      rendererType: 'canvas', // 渲染类型
      convertEol: true,
      cursorBlink: true, // 光标闪烁
      cursorStyle: 'block', // 光标样式
    });
    const webLinksAddon = new WebLinksAddon();
    const fitAddon = new FitAddon();
    const attachAddon = new AttachAddon(this.socket);
    terminal.loadAddon(webLinksAddon);
    terminal.loadAddon(fitAddon);
    terminal.loadAddon(attachAddon);
    terminal.open(document.getElementById('myTerminal'));
    fitAddon.fit();
    // 限制和后端交互,只有输入回车键才显示结果
    terminal.prompt = () => {
      terminal.write('\r\n$ ');
    };
    terminal.prompt();
  };

  render() {
    return (<div id="myTerminal" />);
  }
}

export default TerminalCom;

效果:

效果很尴尬,终端组件上方一直有个类似输入框的东西。

image-20200821102800941

刚开始以为是我这个项目经过了改造和xterm不兼容,于是我开始下载新的antd项目,进行测试,发现还是这样,但是在vue项目里测试就没问题,(后来才发现,vue项目的demo是从往上直接copy的,项目里引入了css文件),而我自己写的demo,完全是安装github上的例子写的,所以没有手动引入xterm.css,才会出现这个样式问题。。。

在文件上方引入 import 'xterm/css/xterm.css'; ,问题解决。

我仿佛是个傻子。。。勿喷

这些都不是最烦人的问题,最烦人的是,npm安装xterm一直失败,时间都浪费到下载xterm上了,可能是我的网不好。

问题二

此刻,我拥有了一个好看又好用的终端组件,只要把代码重构一下,用到项目里就完美了!!!

然而我的demo都是直接在页面上测的,真正的项目需求终端是在Modal里,这里就出现了问题,每次点击按钮出现Modal,出现的终端组件的字都贼小,整个组件好像缩小了好几倍,不知道哪里的问题,我的代码如下:

import React, { useEffect, useRef } from 'react';
import 'xterm/css/xterm.css';
import { Terminal } from 'xterm';
import { WebLinksAddon } from 'xterm-addon-web-links';
import { FitAddon } from 'xterm-addon-fit';
import { AttachAddon } from 'xterm-addon-attach';

export interface MyTerminalProps {
  namespace: string;
  pod_Name: string;
  container_Name: string;
}

const MyTerminal: React.FunctionComponent<MyTerminalProps> = props => {
  const divRef: any = useRef(null);
  let socket: any = null;

  const initTerminal = () => {
    const { namespace, pod_Name, container_Name } = props;
    socket = new WebSocket(`ws://********:0000/bch/websocket/${namespace}/${pod_Name}/${container_Name}`);
    socket.onopen = () => {
      console.log('connection success');
      const terminal = new Terminal({
        cursorBlink: true, // 光标闪烁
      });
      const webLinksAddon = new WebLinksAddon();
      const fitAddon = new FitAddon();
      const attachAddon = new AttachAddon(socket);
      terminal.loadAddon(webLinksAddon);
      terminal.loadAddon(fitAddon);
      terminal.loadAddon(attachAddon);
      console.log(divRef.current);
      terminal.open(divRef.current);
      fitAddon.fit();
      // 限制和后端交互,只有输入回车键才显示结果
      terminal.prompt = () => {
        terminal.write('\r\n ');
      };
      terminal.prompt();
    }
  };

  useEffect(() => {
    if (socket) {
      socket.close();
    }
    initTerminal();
  }, [props.namespace, props.pod_Name, props.container_Name]);

  return <div style={{ width: 760, height: 500 }} ref={divRef} />;
};

export default MyTerminal;

效果图现在截不到了,因为已经改好了。

我试了尝试修改了Modal的大小,div的大小,div的positon等等,还是不行。

可能是程序员的直觉,我 删除了socket.onopen 直接在下边进行了terminal.open(divRef.current),然后就好了!!!

我猜测可能是socket.onopen是个异步方法,影响了terminal的样式。

最终代码

代码最终使用ts + hook重构了,使用最上方 问题一 里边的js代码,记得引入xterm.css

import React, { useEffect, useRef } from 'react';
import 'xterm/css/xterm.css'; // 一定要记得引入css
import { message } from 'antd';
import { Terminal } from 'xterm';
import { WebLinksAddon } from 'xterm-addon-web-links';
import { FitAddon } from 'xterm-addon-fit';
import { AttachAddon } from 'xterm-addon-attach';

export interface MyTerminalProps {
  namespace: string;
  pod_Name: string;
  container_Name: string;
}

const MyTerminal: React.FunctionComponent<MyTerminalProps> = props => {
  const divRef: any = useRef(null); 
  let socket: any = null;

  const initTerminal = () => {  
    const { namespace, pod_Name, container_Name } = props;
    socket = new WebSocket(`ws://*******:0000/bch/websocket/${namespace}/${pod_Name}/${container_Name}`);
    socket.onopen = () => {
      console.log('connection success');
    };
    socket.onerror = () => {
      message.error('连接出错')
    };
    const terminal = new Terminal({
      cursorBlink: true, // 光标闪烁
    });
    const webLinksAddon = new WebLinksAddon();
    const fitAddon = new FitAddon();
    const attachAddon = new AttachAddon(socket);
    terminal.loadAddon(webLinksAddon);
    terminal.loadAddon(fitAddon);
    terminal.loadAddon(attachAddon);
    terminal.open(divRef.current);
    fitAddon.fit();
    // 限制和后端交互,只有输入回车键才显示结果
    terminal.prompt = () => {
      terminal.write('\r\n ');
    };
    terminal.prompt();
  };

  useEffect(() => {
    if (socket) {
      socket.close();
    }
    initTerminal();
  }, [props.namespace, props.pod_Name, props.container_Name]);

  return <div style={{ marginTop: 10, width: 760, height: 500 }} ref={divRef} />;
};

export default MyTerminal;

总结

总体来说,其实是个很简单事情,但是人总是对未知的事务充满恐惧。一些小细节注意不到,就会浪费很多时间很精力,经过这次情况注意到一个非常重要的定位问题的思路,如果发现功能好用,但是样式一直不对,应该第一时间定位到css上。

这次我错把方向放到了环境问题,又搭了很多不同环境去测试,下载插件还需要很长时间,超级浪费时间。

思路正确,事半功倍。

共勉。