场景
最近做的项目是关于私有云的,因为做的是门户部分,主要是集成公司各大产品,最近集成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

这个插件没有中文文档,它的文档看起来不怎么友好,我看的是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上的写法,会出问题:

我的写法:
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;
效果:
效果很尴尬,终端组件上方一直有个类似输入框的东西。

刚开始以为是我这个项目经过了改造和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上。
这次我错把方向放到了环境问题,又搭了很多不同环境去测试,下载插件还需要很长时间,超级浪费时间。
思路正确,事半功倍。
共勉。