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

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

相关推荐
G_G#9 小时前
纯前端js插件实现同一浏览器控制只允许打开一个标签,处理session变更问题
前端·javascript·浏览器标签页通信·只允许一个标签页
@大迁世界9 小时前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路10 小时前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug10 小时前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu1213810 小时前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中10 小时前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路10 小时前
GDAL 实现矢量合并
前端
hxjhnct10 小时前
React useContext的缺陷
前端·react.js·前端框架
前端 贾公子11 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端
菩提小狗11 小时前
Sqlmap双击运行脚本,双击直接打开。
前端·笔记·安全·web安全