1.前言
随着云服务、DevOps等兴起,我们经常在网站中看到远程登录
功能,点击之后会出现一个类似Shell的终端,可以查看相关服务状态,编写代码等功能。web termial让我们和实际的机器通过网络联通了。
在生产环境中进行登录和调试都非常方便,那么这是如何实现的呢,要解决两个问题:实现 Shell 界面以及浏览器与服务器进行通信。
2.如何实现Shell 界面
在react 中有很多模拟 Terminal 组件库,比如 [react-terminal]
1
[terminal-in-react]
2等,笔者推荐的是使用xterm
3, 其可以满足以下需求:
- 丰富的指令
- 支持多种编码
- 支持大多数终端程序如:vim,bash等
日常开始使用的IDE VS Code也是用xterm,可见应用的广泛性。使用如下。
2.1 Terminal 构造函数
Terminal 对象作为一个构造函数,用于新建 Terminal 实例。
代码语言:javascript复制import { Terminal } from "xterm";
let terminal = new Terminal({ cursorBlink: true });
2.2 Terminal.open
将terminal实例附加到指定的dom 元素上。
代码语言:javascript复制terminal.open(document.getElementById("terminal"));
2.3 Terminal.loadAddon
使用terminal 提供的插件扩展功能
代码语言:javascript复制terminal.loadAddon(fitAddon);
3.如何进行通信
日常开发中大多数使用的是http协议,但是有个缺点是只能客户端向服务端发送请求,服务器返回结果;无法web terminal 也会出现服务端推送消息的情况。
WebSocket
4 协议在2008年诞生,现在浏览器都已经支持。其特点是服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息。
3.1 与服务器建立连接
客户端提供 WebSoket API,通创建一个实例就可以将客户端与服务器端连接。
代码语言:javascript复制ws = new WebSocket("wss:xxx");
3.2 向服务端发送消息
websocket.send 方法可以发送消息给服务器
代码语言:javascript复制ws.send("Hello WebSockets!");
3.3 接收服务端消息
websocket.onmessage 可接收服务端消息
代码语言:javascript复制ws.onmessage = function(event) {
var data = event.data;
// 处理数据
};
4. 具体实现
在解决展示和通信问题之后,将两者结合起来,就可实现一个在线web terminal 组件了,大体代码如下。
4.1 创建通信类
代码语言:javascript复制//TerminalConnector.js
export class TerminalConnector {
private ws;
constructor({
command,
handleOpen,
handleMessage,
handleClose,
handleError,
}) {
this.handleOpen = handleOpen;
this.handleMessage = handleMessage;
this.handleClose = handleClose;
this.handleError = handleError;
this.ws = new WebSocket("wss:xxx");
this.ws.onopen = this.onConnOpen;
this.ws.onmessage = this.onMessage;
this.ws.onclose = this.onConnClose;
this.ws.onerror = this.onConnError;
}
handleSend(data) {
this.ws.send(data);
}
detachTerminal() {
try {
this.ws.close();
this.ws = null;
} catch (e) {
console.warn(e);
}
}
private onConnOpen = () => {
//建立连接后身份验证
this.ws.send('authToken相关数据')
}
private onMessage = (event) => {
const dat = event.data || '';
this.handleMessage(data);
}
private onConnClose = (e) => {
console.log("conn close", e);
this.handleClose();
}
private onConnError = (e) => {
console.error("conn error", e);
this.handleError(e);
}
}
4.2 实现 Teraminal组件
代码语言:javascript复制import React from "react";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import {
TerminalConnector
} from "./TerminalConnector";
export function Xterm(props) {
let { command } = props;
const [curTerminal, setTerminal] = React.useState(null);
const [curConnector, setConnector] = React.useState(null);
React.useEffect(() => {
if (curTerminal) {
curTerminal.dispose();
}
if (curConnector) {
curConnector.detachTerminal();
}
let terminal = new Terminal({ cursorBlink: true });
let fitAddon = new FitAddon();
let connector = new TerminalConnector({
command,
handleOpen,
handleMessage,
handleClose,
handleError,
});
setTerminal(terminal);
setConnector(connector);
function handleOpen() {
fitAddon.fit();
terminal.focus();
window.onresize = function () {
fitAddon.fit();
};
}
function handleMessage(data) {
// 接收服务器数据,将数据写入shell
terminal.write(data);
}
function handleClose() {
terminal.writeln("Exit");
}
function handleError(e) {
terminal.writeln("");
terminal.writeln("Sorry, connection has some error.");
}
terminal.loadAddon(fitAddon);
terminal.open(document.getElementById("terminal"));
terminal.onResize(({ cols, rows }) => {
connector.handleSend(
JSON.stringify({ columns: cols, rows })
);
});
terminal.onData((data) => {
connector.handleSend(data);
});
}, [command]);
return <div id="terminal"></div>;
}
[1]react-terminal: https://www.npmjs.com/package/react-terminal
[2]terminal-in-react: https://github.com/nitin42/terminal-in-react
[3]xterm:https://xtermjs.org/
[4]WebSoket: https://www.ruanyifeng.com/blog/2017/05/websocket.html