کتابخانه Xterm.js یکی از ابزارهای کاربردی برای ساخت ترمینال تحت وب است که این امکان را میدهد بدون دردسر زیاد، یک رابط ترمینال تعاملی را داخل مرورگر پیادهسازی کنیم. این کتابخانه برای شبیهسازی محیطهای CLI، اتصال به سرورها، یا مدیریت سرویسها از طریق وب استفاده میشود.
تجربهای که در این مطلب به آن میپردازم، مربوط به پروژهای است که در آن نیاز داشتیم کنسول یک Pod از Docker را به صفحه وب اکسپوز کنیم تا کاربران بتوانند مستقیماً از طریق مرورگر به آن دسترسی داشته باشند. برای پیادهسازی این قابلیت، از Xterm.js بهعنوان هسته اصلی رابط ترمینال استفاده کردیم.
در ادامه چند نمونه از استفاده از این پکیج (نسخه مورد استفاده 6.0.0) به همراه یک استایل CSS پایه برای ظاهر ترمینال را مرور میکنیم.
نمونه کد با HTML و Vanilla JS و CDN
کد JS برای ایجاد یک instance از ترمینال در صفحه وب و متصل کردن آن به DOM:
const CONSOLE_PREFIX = "DigitalFlow";
let xTerminal = null;
const terminalContainer = document.getElementById("terminal-container");
function createTerminal() {
xTerminal = new Terminal({
cursorBlink: true,
cols: 140,
rows: 30,
});
xTerminal.write(
`[\x1B[1;35m${CONSOLE_PREFIX}\x1B[0m.\x1B[33mjs\x1B[0m]: Connecting... `
);
return xTerminal;
}
function connect() {
if (!terminalContainer) return;
if (!xTerminal) {
createTerminal();
}
if (xTerminal.element) return;
xTerminal.open(terminalContainer);
}
window.addEventListener("DOMContentLoaded", () => {
connect();
});
و کد CSS برای ظاهر تمام صفحه و پسزمینه سیاه:
body {
margin: 0;
background: black;
}
#terminal-container {
width: 100%;
height: 100vh;
}
کد HTML شامل لینک stylesheet مربوط به Xterm، استایل بالا، و همچنین بارگذاری کتابخانه Xterm و اسکریپت پروژه است.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Vanilla Xterm Input</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm/css/xterm.css" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="terminal-container"></div>
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm/lib/xterm.js"></script>
<script src="script.js"></script>
</body>
</html>
در مرحله بعدی میتوان ورودی کاربر را مدیریت کرد، حالت تایپ را شبیهسازی نمود و چند دستور local را اجرا کرد. اما برای داشتن یک ترمینال واقعی، لازم است از پروتکلهایی مانند WebSocket برای انتقال stream متن و برقراری ارتباط تعاملی با سمت سرور استفاده شود.
const CONSOLE_PREFIX = "APP_NAME";
let term = null;
let inputBuffer = "";
const container = document.getElementById("terminal-container");
function createTerminal() {
term = new Terminal({
cursorBlink: true,
cols: 140,
rows: 30,
});
term.open(container);
term.write(
`[\x1B[1;35m${CONSOLE_PREFIX}\x1B[0m.\x1B[33mjs\x1B[0m]: Connecting...\r\n`
);
prompt();
setupInputHandler();
}
function prompt() {
term.write("\r\n$ ");
inputBuffer = "";
}
function setupInputHandler() {
term.onData((data) => {
switch (data) {
case "\r": // Enter
handleCommand(inputBuffer);
break;
case "\u007F": // Backspace
if (inputBuffer.length > 0) {
inputBuffer =
inputBuffer.slice(0, -1);
term.write("\b \b");
}
break;
default:
if (isPrintable(data)) {
inputBuffer += data;
term.write(data);
}
break;
}
});
}
function handleCommand(command) {
term.write("\r\n");
// Example command handling
if (command === "help") {
term.write("Available commands: help, clear, echo\r\n");
}
else if (command === "clear") {
term.clear();
}
else if (command.startsWith("echo ")) {
term.write(
command.substring(5) + "\r\n"
);
}
else if (command.trim() !== "") {
term.write(
"Command not found: " +
command +
"\r\n"
);
}
prompt();
}
function isPrintable(str) {
return str.length === 1 &&
str >= " " &&
str <= "~";
}
window.addEventListener(
"DOMContentLoaded",
createTerminal
);
نمونه کد با React و TypeScript
ابتدا پکیج @xterm/xterm را از طریق npm نصب میکنیم.
در سادهترین حالت، تنها به دو reference نیاز داریم:
یکی برای instance ترمینال و دیگری برای container مربوط به رندر آن در DOM:
import { useRef, useCallback, useEffect } from "react";
import { Terminal as TerminalView } from '@xterm/xterm';
const CONSOLE_PREFIX = "APP_NAME";
export default function () {
const xTerminal = useRef<TerminalView>(null);
const terminalContainerRef = useRef<HTMLElement>(null);
const createTerminal = useCallback(() => {
xTerminal.current = new TerminalView({
cursorBlink: true,
cols: 140,
rows: 30,
});
xTerminal.current.write(`[\x1B[1;35m${CONSOLE_PREFIX}\x1B[0m.\x1B[33mjs\x1B[0m]: Connecting... `);
return xTerminal.current;
}, []);
const connect = useCallback(() => {
if (!terminalContainerRef.current) return;
if (!xTerminal.current) {
xTerminal.current = createTerminal();
}
if (xTerminal.current?.element) return;
xTerminal.current.open(terminalContainerRef.current);
}, [createTerminal]);
useEffect(() => {
connect();
}, []);
return (
<div ref={terminalContainerRef} />
);
}