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通道返回给客户端进行展示

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

相关推荐
恋猫de小郭1 小时前
Flutter 官方多窗口体验 ,为什么 Flutter 推进那么慢,而 CMP 却支持那么快
android·前端·flutter
云边有个稻草人2 小时前
智启未来:当知识库遇见莫奈的调色盘——API工作流重构企业服务美学
前端·数据库
仟濹7 小时前
【HTML】基础学习【数据分析全栈攻略:爬虫+处理+可视化+报告】
大数据·前端·爬虫·数据挖掘·数据分析·html
小小小小宇8 小时前
前端WebWorker笔记总结
前端
小小小小宇8 小时前
前端监控用户停留时长
前端
小小小小宇9 小时前
前端性能监控笔记
前端
烛阴9 小时前
Date-fns教程:现代JavaScript日期处理从入门到精通
前端·javascript
全栈小59 小时前
【前端】Vue3+elementui+ts,TypeScript Promise<string>转string错误解析,习惯性请出DeepSeek来解答
前端·elementui·typescript·vue3·同步异步
穗余9 小时前
NodeJS全栈开发面试题讲解——P6安全与鉴权
前端·sql·xss
穗余11 小时前
NodeJS全栈开发面试题讲解——P2Express / Nest 后端开发
前端·node.js