← Sandbox SDK / sandbox / guides
Терминалы в браузере
В этом руководстве показано, как подключить терминал на основе браузера к оболочке песочницы. Вы можете использовать SandboxAddon с xterm.js либо подключайтесь напрямую через WebSockets.
Предварительные требования
Потребуется уже существующий Cloudflare Worker с привязкой sandbox. См. Начало работы если у вас его нет.
Установите зависимости терминала в вашем фронтенд-проекте:
npm install @xterm/xterm @xterm/addon-fit @cloudflare/sandboxЕсли вы не используете xterm.js, вам нужен только @cloudflare/sandbox для типов.
Обработка обновлений WebSocket в Worker
Добавьте маршрут, который проксирует WebSocket-соединения к терминалу sandbox. Приведённый ниже пример поддерживает как сессию по умолчанию, так и именованные сессии через параметр запроса:
import { getSandbox } from "@cloudflare/sandbox";
export { Sandbox } from "@cloudflare/sandbox";
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (
url.pathname === "/ws/terminal" &&
request.headers.get("Upgrade") === "websocket"
) {
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
const sessionId = url.searchParams.get("session");
if (sessionId) {
const session = await sandbox.getSession(sessionId);
return await session.terminal(request);
}
return await sandbox.terminal(request, { cols: 80, rows: 24 });
}
return new Response("Not found", { status: 404 });
},
};import { getSandbox } from '@cloudflare/sandbox';
export { Sandbox } from '@cloudflare/sandbox';
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/ws/terminal' && request.headers.get('Upgrade') === 'websocket') {
const sandbox = getSandbox(env.Sandbox, 'my-sandbox');
const sessionId = url.searchParams.get('session');
if (sessionId) {
const session = await sandbox.getSession(sessionId);
return await session.terminal(request);
}
return await sandbox.terminal(request, { cols: 80, rows: 24 });
}
return new Response('Not found', { status: 404 });
}
};Подключайтесь с помощью xterm.js и SandboxAddon
Создайте терминал в коде браузера и подключите SandboxAddon. Дополнение управляет соединением WebSocket, автоматическим переподключением и передачей изменений размера.
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { SandboxAddon } from "@cloudflare/sandbox/xterm";
import "@xterm/xterm/css/xterm.css";
const terminal = new Terminal({ cursorBlink: true });
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
const addon = new SandboxAddon({
getWebSocketUrl: ({ sandboxId, sessionId, origin }) => {
const params = new URLSearchParams({ id: sandboxId });
if (sessionId) params.set("session", sessionId);
return `${origin}/ws/terminal?${params}`;
},
onStateChange: (state, error) => {
console.log(`Terminal ${state}`, error ?? "");
},
});
terminal.loadAddon(addon);
terminal.open(document.getElementById("terminal"));
fitAddon.fit();
// Connect to the default session
addon.connect({ sandboxId: "my-sandbox" });
// Or connect to a specific session
// addon.connect({ sandboxId: 'my-sandbox', sessionId: 'development' });
window.addEventListener("resize", () => fitAddon.fit());import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { SandboxAddon } from '@cloudflare/sandbox/xterm';
import '@xterm/xterm/css/xterm.css';
const terminal = new Terminal({ cursorBlink: true });
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
const addon = new SandboxAddon({
getWebSocketUrl: ({ sandboxId, sessionId, origin }) => {
const params = new URLSearchParams({ id: sandboxId });
if (sessionId) params.set('session', sessionId);
return `${origin}/ws/terminal?${params}`;
},
onStateChange: (state, error) => {
console.log(`Terminal ${state}`, error ?? '');
}
});
terminal.loadAddon(addon);
terminal.open(document.getElementById('terminal'));
fitAddon.fit();
// Connect to the default session
addon.connect({ sandboxId: 'my-sandbox' });
// Or connect to a specific session
// addon.connect({ sandboxId: 'my-sandbox', sessionId: 'development' });
window.addEventListener('resize', () => fitAddon.fit());Полный API дополнений см. в Справочник Terminal API.
Подключайтесь без xterm.js
Если вы создаёте собственный интерфейс терминала или работаете в среде без xterm.js, подключайтесь напрямую через WebSocket. Протокол использует бинарные фреймы для данных терминала и текстовые фреймы JSON для управляющих сообщений.
const ws = new WebSocket("wss://example.com/ws/terminal?id=my-sandbox");
ws.binaryType = "arraybuffer";
const decoder = new TextDecoder();
const encoder = new TextEncoder();
ws.addEventListener("message", (event) => {
if (event.data instanceof ArrayBuffer) {
// Terminal output (binary) — includes ANSI escape sequences
const text = decoder.decode(event.data);
appendToDisplay(text);
return;
}
// Control message (JSON text)
const msg = JSON.parse(event.data);
switch (msg.type) {
case "ready":
// Terminal is accepting input — send initial resize
ws.send(JSON.stringify({ type: "resize", cols: 80, rows: 24 }));
break;
case "exit":
console.log(`Shell exited: code ${msg.code}`);
break;
case "error":
console.error("Terminal error:", msg.message);
break;
}
});
// Send keystrokes as binary
function sendInput(text) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(encoder.encode(text));
}
}const ws = new WebSocket('wss://example.com/ws/terminal?id=my-sandbox');
ws.binaryType = 'arraybuffer';
const decoder = new TextDecoder();
const encoder = new TextEncoder();
ws.addEventListener('message', (event) => {
if (event.data instanceof ArrayBuffer) {
// Terminal output (binary) — includes ANSI escape sequences
const text = decoder.decode(event.data);
appendToDisplay(text);
return;
}
// Control message (JSON text)
const msg = JSON.parse(event.data);
switch (msg.type) {
case 'ready':
// Terminal is accepting input — send initial resize
ws.send(JSON.stringify({ type: 'resize', cols: 80, rows: 24 }));
break;
case 'exit':
console.log(`Shell exited: code ${msg.code}`);
break;
case 'error':
console.error('Terminal error:', msg.message);
break;
}
});
// Send keystrokes as binary
function sendInput(text: string): void {
if (ws.readyState === WebSocket.OPEN) {
ws.send(encoder.encode(text));
}
}Ключевые детали протокола:
- Задайте
binaryTypeкarraybufferперед подключением. - Буферизованный вывод предыдущего соединения приходит в виде бинарных фреймов перед
readyсообщение. - Отправляйте нажатия клавиш в бинарном формате (UTF-8). Отправляйте управляющие сообщения (
resize) в виде текста JSON. - PTY остаётся активным при отключении клиента. При повторном подключении воспроизводится буферизованный вывод.
Полную спецификацию протокола см. в Раздел о протоколе WebSocket в справочнике API.
Рекомендации
- Всегда используйте FitAddon : Без этого размеры терминала не совпадают с контейнером, и текст переносится некорректно.
- Обработка событий изменения размера : вызов
fitAddon.fit()при изменении размера окна, чтобы терминал и PTY оставались синхронизированы. - Очистка при размонтировании : вызов
addon.disconnect()при удалении терминала со страницы. - Привязывайте терминалы к пользовательской песочнице : Используйте сессии для нескольких терминальных контекстов в одном и том же workspace. Для разных пользователей используйте отдельные песочницы.
Дополнительные материалы
- Справочник Terminal API : сигнатуры методов, addon API и протокол WebSocket
- Подключения терминала : как работают подключения терминала
- Управление сессиями : как работают сессии