如何创建基于 Web 的终端

2025-06-07

如何创建基于 Web 的终端

最初发布在我的博客上

本文详细介绍了如何使用 Web 技术构建终端并在浏览器中使用。创建终端应用(例如 VSCode 内置终端和Hyper )也使用了相同的技术。

我们需要创建服务器和客户端。并且,我们将使用Socket.IO来发送和接收数据。如果您需要 Electron 的此功能,则无需使用socket.io。请查看文章末尾的 Electron 相关信息。

我们将要使用的主要库:

客户端

  1. Socket.io客户端
  2. xterm.js - 终端的 UI

服务器端

  1. Socket.io服务器
  2. node-pty - 创建伪终端。我们需要向其发送输入。如果您需要更多关于伪终端的信息,请查看此处。

客户端和服务器端的运行应用程序均可在以下 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);
});


Enter fullscreen mode Exit fullscreen mode

接下来,我们需要创建一个包装类来添加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;


Enter fullscreen mode Exit fullscreen mode

最后,在服务器端,我们使用 创建一个伪终端进程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;


Enter fullscreen mode Exit fullscreen mode

创建客户端

现在开始 UI 部分。非常简单。我们现在只需创建一个终端,xterm并将其附加到 DOM 中的一个容器中。然后,将终端中的输入传递给已连接的socket.io服务器。我们还将为 socket.io-client 添加一个事件监听器,它将把来自socket.io服务器的回复写入 xtermjs 终端。

在 html 页面上,创建一个divxtermjs 将附加终端的位置。



<!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>


Enter fullscreen mode Exit fullscreen mode

在启动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();
  }
}


Enter fullscreen mode Exit fullscreen mode

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();


Enter fullscreen mode Exit fullscreen mode

当服务器和客户端都运行时,您将在浏览器中看到一个终端。有关其他样式自定义(例如高度、宽度),请参阅 xtermjs 文档。

最终输出

对于 Electron 用户

在 Electron 中使用xtermjsnode-pty更加简单。由于渲染进程可以运行 Node 模块,因此您可以直接在xtermjsnode-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));


Enter fullscreen mode Exit fullscreen mode

Github 存储库中添加了一个可运行的电子示例。

其他信息

如果你只需要一个可以打印 NodeJS 输出的终端 UI child_process,则不需要node-pty。你可以将child_processstdout 直接发送到xtermjs UI。

我的一个开源项目https://github.com/saisandeepvaddi/ten-hands就是以这种方式运行的。查看Ten Hands来深入了解如何使用xtermjssocket.ioReactJS来构建基于终端的应用程序。

谢谢🙏

文章来源:https://dev.to/saisandeepvaddi/how-to-create-web-based-terminals-38d
PREV
在 React 中设置 Tailwind - 最快捷的方法!🚀 大家好 👋
NEXT
React 中的代码拆分