xterm.js:
一个用 TypeScript 编写的前端组件,可让应用程序在浏览器中为用户提供功能齐全的终端。
- xterm-addon-fit: 自动调整终端尺寸适应容器
- xterm-addon-webgl: 使用 WebGL 加速渲染 需要浏览器支持 WebGL
- xterm-addon-canvas: 使用 Canvas 兼容性更好的备选方案
- xterm-addon-web-links: 自动识别终端输出中的 URL, 点击链接自动打开新标签页
Socket.io:
将使用 WebSocket 建立连接,在服务器和客户端之间提供低开销的通信通道(替代传统 WebSocket 的复杂处理),如无法建立 WebSocket 连接,它将回退到 HTTP 长轮询。如果连接丢失,客户端将自动尝试重新连接,需客户服务端保持同一个版本
在script 使用
1. npm 安装通过运行添加 xterm.js 作为依赖项
bash
npm install @xterm/xterm
2. 将xterm.js
和xterm.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通道返回给客户端进行展示
在跨域时服务端需要开启跨域请求代理,