最近公司项目要用到 web termianl, 先提前在家里做一个 🐶
效果图
准备
需求是自动化测试的日志,要实时的展现在前端,所以少不了 webSocket
在前端需要有个终端能显示出来,也有可能后续会需要在前端直接操作服务端的终端
所以,一次性到位,直接做个 web termianl
用到的库有 xterm、ahooks 等
具体过程写在代码注释里,一看就懂
上代码 👇
客户端
1import { useEffect, useLayoutEffect, useRef } from "react";2import { Terminal } from "xterm";3import { AttachAddon } from "xterm-addon-attach";4import { FitAddon } from "xterm-addon-fit";5import { SearchAddon } from "xterm-addon-search";6import { WebLinksAddon } from "xterm-addon-web-links";7import { AdventureTime } from "xterm-theme";8import { useSize, useWebSocket } from "ahooks";9import "xterm/css/xterm.css";10import "./index.less";1112const socketURL = "ws://127.0.0.1:4000/socket";13const height = 500;14const fontSize = 12;1516export default function HomePage() {17 const termRef = useRef<any>(null);18 const containerRef = useRef<any>(null);19 const insDomRef = useRef<any>(null);20 // 监听容器尺寸,用于做自适应21 const size = useSize(containerRef);2223 // 直接使用封装好的useWebSocket24 const {25 readyState,26 sendMessage,27 latestMessage,28 disconnect,29 connect,30 webSocketIns,31 } = useWebSocket(socketURL);3233 useEffect(() => {34 if (!webSocketIns) {35 return;36 }3738 // 创建终端实例39 var term = new Terminal({40 fontFamily: 'Menlo, Monaco, "Courier New", monospace',41 fontWeight: 400,42 fontSize,43 theme: AdventureTime,44 rows: Math.floor(height / (fontSize + 2)),45 });4647 // 添加终端插件48 // An addon for xterm.js that enables attaching to a web socket49 const attachAddon = new AttachAddon(webSocketIns as WebSocket);50 // 自适应容器插件51 const fitAddon = new FitAddon();52 // 搜索插件53 const searchAddon = new SearchAddon();54 // 超链接显示插件55 const webLinksAddon = new WebLinksAddon();5657 term.loadAddon(attachAddon);58 term.loadAddon(fitAddon);59 term.loadAddon(searchAddon);60 term.loadAddon(webLinksAddon);6162 // 把示例挂载给ref63 termRef.current = {64 term,65 searchAddon,66 fitAddon,67 };6869 // render 终端到容器70 term.open(insDomRef.current);71 // 适用容器(发现只能适应宽度)72 fitAddon.fit();7374 return () => {75 //组件卸载,清除 Terminal 实例76 term.dispose();77 termRef.current = null;78 };79 }, [webSocketIns]);8081 // 响应容器尺寸副作用82 useLayoutEffect(() => {83 if (!size) {84 return;85 }86 // 想做响应式高度、不过这个方法调用报错说rows只能在构造函数里指定,暂时没想到好的办法处理87 // termRef.current.term.setOption(88 // "rows",89 // Math.floor(size.height / (fontSize + 2))90 // );91 termRef.current?.fitAddon?.fit();92 }, [size]);9394 return (95 <>96 <input97 type="text"98 placeholder="查询关键字"99 onChange={(e) => termRef.current.searchAddon?.findNext(e.target.value)}100 style={{ marginBottom: 10 }}101 />102 <div style={{ height, width: "100%" }} ref={containerRef}>103 <div104 style={{105 background: "#1F1D45",106 borderRadius: 10,107 overflow: "hidden",108 padding: 10,109 }}110 ref={insDomRef}111 />112 </div>113 </>114 );115}
定制下滚动条,让其透明
1.xterm .xterm-viewport {2 &::-webkit-scrollbar {3 width: 10px;4 height: 10px;5 }67 &::-webkit-scrollbar-track {8 background-color: transparent;9 border-radius: 10px;10 }1112 &::-webkit-scrollbar-thumb {13 background-color: rgba(255, 255, 255, 0.1);14 border-radius: 10px;15 }16}
服务端
1const express = require("express");2const expressWs = require("express-ws");3const pty = require("node-pty");4const os = require("os");5const example = require("./data");6const app = express();7const port = 4000;89expressWs(app);1011// 创建终端子进程12const shell = os.platform() === "win32" ? "powershell.exe" : "bash";13const term = pty.spawn(shell, ["--login"], {14 name: "xterm-color",15 cols: 80,16 rows: 24,17 cwd: process.env.HOME,18 env: process.env,19});2021// 暴露socket22app.ws("/socket", (ws, req) => {23 term.write(example);24 // 编码转换25 term.onData(function (data) {26 ws.send(data);27 });28 // 收到输入29 ws.on("message", (data) => {30 term.write(data);31 });32 ws.on("close", function () {33 term.kill();34 });35});3637app.listen(port, "127.0.0.1", () => {38 console.log(`Example app listening on port ${port}`);39});