Реализуйте простой веб-терминал с помощью xterm.js!

JavaScript
Реализуйте простой веб-терминал с помощью xterm.js!

предисловие

Всех с наступающим Новым годом~ Из-за моей занятости я некоторое время не обновлялся (на самом деле я ленивый).веб-терминалЯ наступил на много ям и, наконец, понял все. Я хотел написать статью, чтобы отблагодарить сообщество, поэтому я просто сделал то, что сказал, и поехали~

Предварительное изучение xterm.js

знать, что нужно сделатьweb-terminal, первое, что нужно сделать, это изучить конкретные технологии, необходимые в Интернете, и, наконец, найтиxterm.jsдля большинстваweb-terminalраствор, знаменитыйvscodeОн все еще используется, и кажется, что надежность все еще гарантирована. Итак, я с радостью постучалОфициальный сайтНаходитьdemo, на первый взгляд хороший парень выглядит довольно просто, просто установите его и инициализируйте экземпляр следующим образом:

npm install xterm
<!doctype html>
  <html>
    <head>
      <link rel="stylesheet" href="node_modules/xterm/css/xterm.css" />
      <script src="node_modules/xterm/lib/xterm.js"></script>
    </head>
    <body>
      <div id="terminal"></div>
      <script>
        var term = new Terminal();
        term.open(document.getElementById('terminal'));
        term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
      </script>
    </body>
  </html>

Поскольку наш проект основан наReact, поэтому я собираюсь использоватьcreate-react-appзаписыватьdemoПопробуйте, после еды скопируйте пример с официального сайтаdemo, а затем запустите его, чтобы увидеть эффект. Затем страница выходит из терминального стиля следующим образом: Я собираюсь написать персонажей, чтобы увидеть эффект, молодец. . Я не могу в него войти, я когда-то подозревал, что это яdemoКопия неправильная После тщательного сравнения выясняется, что она действительно правильная? ? Затем я поискал документацию и обнаружил, что вход все равно нужно вызыватьapiЯ впервые вижу пример эмоционального официального сайта, который нельзя запустить напрямую. скорректировал код

  term.open(document.getElementById("terminal"));
  term.write("Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ");
+ term.onData((val) => {
+     term.write(val);
+   }); 

Ввод, наконец, в порядке, но снова возникает новая проблема, и сообщается об ошибке, как только я ее удаляю.

И как только вводишь, курсор возвращается к началу, этому. . Я не мог не погрузиться в глубокие размышления, вернулся к документации для повторного поиска и обнаружил, что документацияonDataописание

contains real string data with any valid Unicode codepoint, thus the payload should be treated as UTF-16/UCS-2. For OS interaction this data should be converted to UTF-8 bytes (automatically done by node-pty). If you need legacy encoding support, see below.

оказалосьonDataвозвращает всеUTF-16/UCS-2Кодирование, пусть система распознает вывод какUTF-8Кодирование, неудивительно, что у меня проблемы с прямым вводом, и мне приходится самому крутить кодировку... Это сложно для меня, мне что, столько ключей разбирать?

К счастью, чиновник предложил решение, которое заключается в использованииnode-ptyВыполнить автоматический разбор. Способ использования тоже очень простой, на официальном сайте есть следующий код

pty.onData(recv => terminal.write(recv));
terminal.onData(send => pty.write(send));

дело в том, чтобы позволитьonDataвернутьUTF-16/UCS-2для строкиnode-ptyРазобрать в системно читаемыйUTF-8Закодированные строки для завершения ввода, очевидно, нам нужно создать связь между ними, положитьxterm.jsКогда интерфейс рендеринга графики браузера,node-ptyКогда сервер прослушивает инструмент ввода и транскодирования, он может передатьwebsocketЧтобы связать отношения между двумя сторонами, кажется возможным!

Разбирать сигналы ввода с клавиатуры с помощью node-pty

Поскольку вы знаетеnode-ptyЕго можно разобрать, нам его сначала нужно установить, по описанию на официальном сайте устанавливаются разные системыnode-ptyТребуются разные препараты, что понятно, поскольку разные системы будут иметь разные отличия.

Linux/Ubuntu

sudo apt install -y make python build-essential
Node.JS 10+

macOS

Xcode is needed to compile the sources, this can be installed from the App Store.

Windows

npm install --global --production windows-build-tools
Windows SDK - only the "Desktop C++ Apps" components are needed to be installed
Node.JS 10+

После установки создадим наши серверные файлыserver.js, для сокаexpress express-wsстроитьnodeобслуживать и включатьwebsocketСлужить.

const express = require("express");
const expressWs = require("express-ws");
const app = express();
expressWs(app);
app.listen(4000, "127.0.0.1");

тогда присоединяйтесьnode-ptyисходный код.

const express = require("express");
const expressWs = require("express-ws");
const app = express();
expressWs(app);
const pty = require("node-pty");
const os = require("os");
const shell = os.platform() === "win32" ? "powershell.exe" : "bash";
const term = pty.spawn(shell, ["--login"], {
  name: "xterm-color",
  cols: 80,
  rows: 24,
  cwd: process.env.HOME,
  env: process.env,
});
app.ws("/socket", (ws, req) => {
  term.on("data", function (data) {
    ws.send(data);
  });
  ws.on("message", (data) => {
    term.write(data);
  });
  ws.on("close", function () {
    term.kill();
  });
});

В реальном сценарии также будут сценарии, когда несколько терминалов работают вместе, поэтому нас явно не устраивает инициализация сразу после запуска сервера.Что нам делать? После некоторых размышлений.. вот оно! Идея сока в том, что когда клиент инициализирует экземпляр терминала, он инициализирует и серверptyНапример, разные терминалы инициализируют разныеptyнапример, черезpidЧтобы различать, чтобы, если есть сценарий расширения нескольких терминалов, он также мог быть удовлетворен.

Сторона клиента отправляет запрос на инициализацию на сторону сервера, и сторона сервера инициализируетсяptyэкземпляр, возвращает текущий экземплярpid, а затем клиент и сервер каждый разwebsocketБерите с собой при общенииpid, сервер проходитpidчтобы получить соответствующийptyНапример, вернуть проанализированное значение клиенту, таким образом реализуя мультитерминальный сценарий!

Измените наш предыдущий код

...
const termMap = new Map(); //存储 pty 实例,通过 pid 映射
function nodeEnvBind() {
  //绑定当前系统 node 环境
  const term = pty.spawn(shell, ["--login"], {
    name: "xterm-color",
    cols: 80,
    rows: 24,
    cwd: process.env.HOME,
    env: process.env,
  });
  termMap.set(term.pid, term);
  return term;
}
//服务端初始化
app.post("/terminal", (req, res) => {
  const term = nodeEnvBind(req);
  res.send(term.pid.toString());
  res.end();
});
app.ws("/socket/:pid", (ws, req) => {
  const pid = parseInt(req.params.pid);
  const term = termMap.get(pid);
  term.on("data", function (data) {
    ws.send(data);
  });
  ws.on("message", (data) => {
    term.write(data);
  });
  ws.on("close", function () {
    term.kill();
    termMap.delete(pid);
  });
});

Веб-сокет для подключения клиента

Сервер готов! Далее мы начинаем писать клиентский код, клиент должен создатьwebsocketсоединять.

const socketURL = "ws://127.0.0.1:4000/socket/";
const ws = new WebSocket(socketURL);

Этого недостаточно, нам также нужно получить серверptyпримерpid, как уникальный идентификатор соединения, это просто, достаточно получить его прямо через интерфейс.

import axios from "axios";
...
//初始化当前系统环境,返回终端的 pid,标识当前终端的唯一性
const initSysEnv = async (term: Terminal) =>
  await axios
    .post("http://127.0.0.1:4000/terminal")
     .then((res) => res.data)
     .catch((err) => {
       throw new Error(err);
    });
const pid = await initSysEnv(term),
ws = new WebSocket(socketURL + pid);   

xterm.jsПредоставляет возможность расширения самого пакета, здесь мы используем один из его пакетов расширенияxterm-addon-attach, это может помочь нам автоматически иwebsocketВзаимодействуйте, спасите нас, написав это сами.

Примечание. Для xterm-addon-attach требуется xterm.js v4+.

import { AttachAddon } from "xterm-addon-attach";
...
attachAddon = new AttachAddon(ws);
term.loadAddon(attachAddon);

Это завершает клиентский код~ Давайте начнем и посмотрим.

Фактически междоменный. . Что ж, тогда давайте добавим анти-междоменный код на сервер.

// //解决跨域问题
app.all("*", function (req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "Content-Type");
  res.header("Access-Control-Allow-Methods", "*");
  next();
});

Перезапустите сервер, чтобы увидеть эффект~

Видно, что мы успешно побежали, и сок прошелweb-terminalУспешно авторизовались дома удаленномалиновый пирог, похоже, хороший опыт

Суммировать

Как видно из количества кода, реализующегоweb-terminalЭто не особо сложно, главное идея. Для тех, кто хочет исходный код, я передал кодgithubвверх,порталвот, пройди мимоотличный👍Это самая большая поддержка для меня, значит, увидимся в следующий раз~