如何创建基于 Web 的终端
本文详细介绍了如何使用 Web 技术构建终端并在浏览器中使用。创建终端应用(例如 VSCode 内置终端和Hyper )也使用了相同的技术。
我们需要创建服务器和客户端。并且,我们将使用Socket.IO来发送和接收数据。如果您需要 Electron 的此功能,则无需使用socket.io。请查看文章末尾的 Electron 相关信息。
我们将要使用的主要库:
客户端
服务器端
客户端和服务器端的运行应用程序均可在以下 codesandbox 链接中找到。如果它们无法运行,请打开链接并快速刷新以唤醒它们(如果应用程序已被 Codesandbox 休眠)。
代码也可在Github上找到
创建服务器
首先让我们设置一下基础设置。从 NodeJShttp
模块创建一个服务器,并将其传递给socket.io服务器。
//index.js
const http = require("http");
const SocketService = require("./SocketService");
/*
Create Server from http module.
If you use other packages like express, use something like,
const app = require("express")();
const server = require("http").Server(app);
*/
const server = http.createServer((req, res) => {
res.write("Terminal Server Running.");
res.end();
});
const port = 8080;
server.listen(port, function() {
console.log("Server listening on : ", port);
const socketService = new SocketService();
// We are going to pass server to socket.io in SocketService.js
socketService.attachServer(server);
});
接下来,我们需要创建一个包装类来添加socket.io事件的事件监听器。
//SocketService.js
const socketIO = require("socket.io");
const PTYService = require("./PTYService");
class SocketService {
constructor() {
this.socket = null;
this.pty = null;
}
attachServer(server) {
if (!server) {
throw new Error("Server not found...");
}
const io = socketIO(server);
console.log("Created socket server. Waiting for client connection.");
// "connection" event happens when any client connects to this io instance.
io.on("connection", socket => {
console.log("Client connect to socket.", socket.id);
this.socket = socket;
this.socket.on("disconnect", () => {
console.log("Disconnected Socket: ", socket.id);
});
// Create a new pty service when client connects.
this.pty = new PTYService(this.socket);
// Attach event listener for socket.io
this.socket.on("input", input => {
// Runs this listener when socket receives "input" events from socket.io client.
// input event is emitted on client side when user types in terminal UI
this.pty.write(input);
});
});
}
}
module.exports = SocketService;
最后,在服务器端,我们使用 创建一个伪终端进程node-pty
。我们输入的内容将被传递给 的一个实例node-pty
,输出将被发送到已连接的socket.io客户端。我们稍后会添加socket.io客户端。
// PTYService.js
const os = require("os");
const pty = require("node-pty");
class PTY {
constructor(socket) {
// Setting default terminals based on user os
this.shell = os.platform() === "win32" ? "powershell.exe" : "bash";
this.ptyProcess = null;
this.socket = socket;
// Initialize PTY process.
this.startPtyProcess();
}
/**
* Spawn an instance of pty with a selected shell.
*/
startPtyProcess() {
this.ptyProcess = pty.spawn(this.shell, [], {
name: "xterm-color",
cwd: process.env.HOME, // Which path should terminal start
env: process.env // Pass environment variables
});
// Add a "data" event listener.
this.ptyProcess.on("data", data => {
// Whenever terminal generates any data, send that output to socket.io client
this.sendToClient(data);
});
}
/**
* Use this function to send in the input to Pseudo Terminal process.
* @param {*} data Input from user like a command sent from terminal UI
*/
write(data) {
this.ptyProcess.write(data);
}
sendToClient(data) {
// Emit data to socket.io client in an event "output"
this.socket.emit("output", data);
}
}
module.exports = PTY;
创建客户端
现在开始 UI 部分。非常简单。我们现在只需创建一个终端,xterm
并将其附加到 DOM 中的一个容器中。然后,将终端中的输入传递给已连接的socket.io服务器。我们还将为 socket.io-client 添加一个事件监听器,它将把来自socket.io服务器的回复写入 xtermjs 终端。
在 html 页面上,创建一个div
xtermjs 将附加终端的位置。
<!DOCTYPE html>
<html>
<head>
<title>Terminal in Browser</title>
<meta charset="UTF-8" />
</head>
<body>
<div id="terminal-container"></div>
<script src="src/index.js"></script>
</body>
</html>
在启动socket.io客户端之前,让我们创建一个包装类来包含 xtermjs 相关函数以及 socket.io-client 所需的事件监听器。
// TerminalUI.js
// You will need a bundler like webpack or parcel to use these imports.
// The example in codesandboxes and github uses parcel.
import { Terminal } from "xterm";
import "xterm/css/xterm.css"; // DO NOT forget importing xterm.css
export class TerminalUI {
constructor(socket) {
this.terminal = new Terminal();
/* You can make your terminals colorful :) */
this.terminal.setOption("theme", {
background: "#202B33",
foreground: "#F5F8FA"
});
this.socket = socket;
}
/**
* Attach event listeners for terminal UI and socket.io client
*/
startListening() {
this.terminal.onData(data => this.sendInput(data));
this.socket.on("output", data => {
// When there is data from PTY on server, print that on Terminal.
this.write(data);
});
}
/**
* Print something to terminal UI.
*/
write(text) {
this.terminal.write(text);
}
/**
* Utility function to print new line on terminal.
*/
prompt() {
this.terminal.write(`\\r\\n$ `);
}
/**
* Send whatever you type in Terminal UI to PTY process in server.
* @param {*} input Input to send to server
*/
sendInput(input) {
this.socket.emit("input", input);
}
/**
*
* container is a HTMLElement where xterm can attach terminal ui instance.
* div#terminal-container in this example.
*/
attachTo(container) {
this.terminal.open(container);
// Default text to display on terminal.
this.terminal.write("Terminal Connected");
this.terminal.write("");
this.prompt();
}
clear() {
this.terminal.clear();
}
}
xtermjs支持各种酷炫的功能。您可以为终端创建主题,也可以使用插件实现其他功能。详情请查看xtermjs 的 GitHub 仓库TerminalUI.js
。如果您希望在此示例中进行更多自定义,可以更新上述文件并自定义this.terminal
对象。此处添加了一个基本的深色主题选项作为示例。
最后,我们需要初始化我们的socket.io客户端来从服务器的node-pty进程发送/接收事件。
// index.js
import { TerminalUI } from "./TerminalUI";
import io from "socket.io-client";
// IMPORTANT: Make sure you replace this address with your server address.
const serverAddress = "http://localhost:8080";
function connectToSocket(serverAddress) {
return new Promise(res => {
const socket = io(serverAddress);
res(socket);
});
}
function startTerminal(container, socket) {
// Create an xterm.js instance.
const terminal = new TerminalUI(socket);
// Attach created terminal to a DOM element.
terminal.attachTo(container);
// When terminal attached to DOM, start listening for input, output events.
// Check TerminalUI startListening() function for details.
terminal.startListening();
}
function start() {
const container = document.getElementById("terminal-container");
// Connect to socket and when it is available, start terminal.
connectToSocket(serverAddress).then(socket => {
startTerminal(container, socket);
});
}
// Better to start on DOMContentLoaded. So, we know terminal-container is loaded
start();
当服务器和客户端都运行时,您将在浏览器中看到一个终端。有关其他样式自定义(例如高度、宽度),请参阅 xtermjs 文档。
对于 Electron 用户
在 Electron 中使用xtermjs和node-pty更加简单。由于渲染进程可以运行 Node 模块,因此您可以直接在xtermjs和node-pty之间创建和传递数据,而无需使用任何套接字库。一个简单的示例如下:
// In electronjs renderer process
// Make sure nodeIntegration is enabled in your BrowserWindow.
// Check github repo for full example (link given at the beginning of this article).
// Choose shell based on os
const shell = os.platform() === "win32" ? "powershell.exe" : "bash";
// Start PTY process
const ptyProcess = pty.spawn(shell, [], {
name: "xterm-color",
cwd: process.env.HOME, // Which path should terminal start
env: process.env // Pass environment variables
});
// Create and attach xtermjs terminal on DOM
const terminal = new Terminal();
terminal.open(document.getElementById("terminal-container"));
// Add event listeners for pty process and terminal
// we don't need to use any socket to communicate between xterm/node-pty
ptyProcess.on("data", function(data) {
terminal.write(data);
});
terminal.onData(data => ptyProcess.write(data));
Github 存储库中添加了一个可运行的电子示例。
其他信息
如果你只需要一个可以打印 NodeJS 输出的终端 UI child_process
,则不需要node-pty。你可以将child_process
stdout 直接发送到xtermjs UI。
我的一个开源项目https://github.com/saisandeepvaddi/ten-hands就是以这种方式运行的。查看Ten Hands来深入了解如何使用xtermjs
、socket.io
和ReactJS
来构建基于终端的应用程序。
谢谢🙏
文章来源:https://dev.to/saisandeepvaddi/how-to-create-web-based-terminals-38d