xterm + socket.io 实现 Web Terminal

‌xterm.js‌:

一个用 TypeScript 编写的前端组件,可让应用程序在浏览器中为用户提供功能齐全的终端。

  1. xterm-addon-fit: 自动调整终端尺寸适应容器
  2. xterm-addon-webgl: 使用 WebGL 加速渲染 需要浏览器支持 WebGL
  3. xterm-addon-canvas: 使用 Canvas 兼容性更好的备选方案
  4. xterm-addon-web-links: 自动识别终端输出中的 URL, 点击链接自动打开新标签页

Socket.io‌:

将使用 WebSocket 建立连接,在服务器和客户端之间提供低开销的通信通道(替代传统 WebSocket 的复杂处理),如无法建立 WebSocket 连接,它将回退到 HTTP 长轮询。如果连接丢失,客户端将自动尝试重新连接,需客户服务端保持同一个版本

在script 使用

1. npm 安装通过运行添加 xterm.js 作为依赖项
bash 复制代码
npm install @xterm/xterm
2. 将xterm.jsxterm.css添加到 html 页面的头部,实例化该Terminal对象,然后open使用 的 DOM 对象调用该函数div。在html中使用周边插件太过复杂,所以使用基本的xterm
xml 复制代码
<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="node_modules/@xterm/xterm/css/xterm.css" />
    <script src="node_modules/@xterm/xterm/lib/xterm.js"></script>
    <!--  引入socket.io  -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
  </head>
  <body>
    <div id="terminal"></div>
    <script>
      var term = new Terminal();
      term.open(document.getElementById('terminal'));
      // 建立 socket 链接
      let socket = io(`${window.location.protocol}//${window.location.host}`, {
            transports: ['websocket'],
            query: {
                termId: termId,
            },
        }); 
      socket.on('connect', function(data){
            console.log('websocket 连接成功', data)
        
            socket.emit('init_terminal', {
                project: project,
                pod: pod,
            });
        })
      socket.on(`output:${termId}`, (data) => {
            
            term.write(data);
            
        })
      let currentCommand = '';
      // 监听键盘事件
        term.onData(data => {
            console.log('data', data)
            socket.emit('exec_command', {command: data,namespace: 'ad',
            container: 'adsrv-agent',
            pod: 'test-adsrv-agent-684968554f-94dth',
            project: 'test-adsrv-agent', });  // 发送命令到服务器
            
            // 一个错误的示范
            // if (data === '\r') {  // 如果按下回车键
                
            //     term.write('\r\n');  // 换行
            //     currentCommand = '';  // 清空当前命令
            // } else if (data === '\u007f') {  // 处理退格键(Backspace)
            //     currentCommand = currentCommand.slice(0, -1);  // 删除最后一个字符
            //     console.log('currentCommand', currentCommand.length)
            //     if(currentCommand.length > 0){
            //         term.write('\b \b');  // 在终端中删除字符
            //     }
                
            // } else {
            //     currentCommand += data;  // 更新当前命令
            //     term.write(data);  // 显示用户输入的字符
            // }
        });
      // 默认xterm 宽度只有一半,需要计算修改cols
      window.addEventListener('resize', () => {
            console.log(
            '变化了',
            computeCols()
            )
            term.resize(computeCols(), 24)
        })
      // term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
    </script>
  </body>
</html>

在ESM 使用

安装 xterm.js 以及插件
sql 复制代码
// esm 中是无法使用 sokcet.io 改为 socket.io-client
yarn add @xterm/xterm @xterm/addon-fit @xterm/addon-webgl @xterm/addon-canvas @xterm/addon-web-links @xterm/addon-search
arduino 复制代码
// 前端 配置跨域请求代理
'/socket.io': {
   target: 'http://192.168.41.247:4001',
    ws: true,
    changeOrigin: true, // 解决 host 头部不匹配的问题
    logLevel: "debug", // 用于调试代理是否生效 
    secure: false,
},
// 服务端需要打开 CORS 允许跨域,或者使用 Nginx 代理 WebSocket
socketio = SocketIO( cors_allowed_origins="*") 
// 如果不配置 cors_allowed_origins,当前端尝试连接 WebSocket 时,服务器可能会拒绝连接,导致 WebSocket connection to 'ws://...' failed 的错误。
// socket.io: 默认监听的路径是 /socket.io,
// 如果自定义的话服务端需要加上: path: 'xxx',
typescript 复制代码
import React, {useEffect, useRef} from 'react'

import {Terminal} from '@xterm/xterm'

import { FitAddon } from '@xterm/addon-fit'
import { WebglAddon } from '@xterm/addon-webgl'
import { CanvasAddon } from '@xterm/addon-canvas'
import { WebLinksAddon } from '@xterm/addon-web-links'

import io from 'socket.io-client'

export interface TerminalTabProps {
  tabId: string
  pod: string
  container: string
  project: string
  namespace: string
}
const TerminalTab = ({
  tab,
  terminals,
  sockets,
}: {
  tab: TerminalTabProps
  terminals: React.MutableRefObject<Record<string, Terminal>>
  sockets: React.MutableRefObject<Record<string, any>>
}) => {
  const containerRef = useRef<HTMLDivElement>(null)
  const fitAddon = useRef<FitAddon>(new FitAddon()) 
  const webglAddon = useRef<WebglAddon | null>(null)
  const canvasAddon = useRef<CanvasAddon | null>(null)

  useEffect(() => {
    if (!containerRef.current) return

    // 初始化终端
    const term = new Terminal({
      cursorBlink: true,
      fontSize: 12,
      allowProposedApi: true
    })

    term.loadAddon(fitAddon.current) 

    // const weblinksAddon = new WebLinksAddon({
    //   hover: (event: any, uri: any) => {
    //     term.element.title = uri; // 鼠标悬停时显示链接
    //   },
    //   regex: /(myapp://\S+)|([\w-]+.example.com)/g // 自定义匹配规则
    // });
    term.loadAddon(new WebLinksAddon())

    try {
      webglAddon.current = new WebglAddon()
      term.loadAddon(webglAddon.current)
    } catch (e) {
      canvasAddon.current = new CanvasAddon()
      term.loadAddon(canvasAddon.current)
    }
    term.open(containerRef.current)
    fitAddon.current.fit()

    const resizeObserver = new ResizeObserver(() => fitAddon.current.fit())
    resizeObserver.observe(containerRef.current)
    terminals.current[tab.tabId] = term

    const termId = `${tab.pod}-${Math.random().toString(36).substring(2, 10)}`
    const socket = io(`${window.location.origin}`, {
      transports: ['websocket'],
      query: {termId},
    })
    sockets.current[tab.tabId] = socket

    // 事件处理
    socket.on('connect', () => {
      socket.emit('init_terminal', {
        ...tab,
        termId,
      })
    })

    socket.on(`output:${termId}`, (data: string) => {
      term.write(data)
    })

    term.onData((data: string) => {
      console.log('namespace, container, pod, project', tab)
      socket.emit('terminal_input', {termId, data})
    })

    // 清理函数
    return () => {
      resizeObserver.disconnect()
      term.dispose()
      socket.disconnect()
      webglAddon.current?.dispose()
      canvasAddon.current?.dispose()
    }
  }, [])

  return <div ref={containerRef} style={{height: 630}} />
  }
export default TerminalTab

总结

客户端输入实时发送给服务端,服务端通过socket通道返回给客户端进行展示

在跨域时服务端需要开启跨域请求代理,

相关推荐
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte5 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc
NEXT066 小时前
前端算法:从 O(n²) 到 O(n),列表转树的极致优化
前端·数据结构·算法