当前位置: 首页 > Web前端 > JavaScript

如何搭建一个简单的WebTerminal(一)

时间:2023-03-27 13:33:26 JavaScript

前言在介绍这篇文章的时候,先说一下这篇文章的一些背景。作者基于公司基础建设哆啦A梦(哆啦A梦)的一些功能背景写下这篇文章。不懂有兴趣的可以去袋鼠云的github了解宝箱哆啦A梦。可以在Doraemon中配置代理。我们可以在配置中心的配置详情下找到主机对应的nginx配置文件或者其他文件,并在这里进行编辑,但是这个功能模块下的Executeshell其实只是一个Input框,会给用户一种错觉这个输入框是一个Web终端。因此,为了解决这个问题,我们打算做一个简易版的WebTerminal来解决这个问题。正是在这样的背景下,笔者开始了对WebTerminal的研究,并写下了这篇文章。本文名为《如何搭建一个简单的Web终端》,将主要围绕这个主题,结合哆啦A梦进行描述,逐步推导出其中涉及的要点,以及笔者思考的一些要点。当然,WebTerminal的实现方式可能有很多种。笔者也在研究过程中。同时,写这篇文章的时间比较仓促,涉及的点也比较多。本文如有不妥之处,欢迎同学们指出。作者必须及时更正。Xterm.js首先我们需要一个组件来帮助我们快速搭建WebTerminal的基础框架,它就是——Xterm.js。那么什么是Xterm.js,官方解释如下Xterm.js是一个用TypeScript编写的前端组件,它可以让应用程序在浏览器中为用户带来一个功能齐全的终端。它被VSCode、Hyper和Theia等流行项目使用。由于本文主要围绕搭建一个WebTerminal,所以与Xterm.js相关的详细API就不做介绍了,只简单介绍一下基本的API。现在你只需要知道它是一个组件,我们就需要使用它,感兴趣的同学可以点击官方文档阅读。可以生成终端实例的基本APITerminal构造函数import{Terminal}from'xterm';constterm=newTerminal();onKey,onData监听Terminal实例输入事件的函数writeTerminal实例写文本方法的loadAddonTerminal实例加载plugins方法attach,fit插件fit插件可以调整Terminal的大小,使其适合Terminal的父元素.attach插件提供了一种将终端附加到WebSocket流的方法。下面是官网使用的例子。从'xterm'导入{Terminal};从'xterm-addon-attach'导入{AttachAddon};constterm=newTerminal();constsocket=newWebSocket('wss://docker.example.com/containers/mycontainerid/attach/ws');constattachAddon=newAttachAddon(socket);//将套接字附加到termterm.loadAddon(attachAddon);基本使用作为一个组件,我们首先要了解它的基本使用,如何快速搭建WebTerminal的基本框架。下面以哆啦A梦的代码为例1.第一步安装Xtermnpminstallxterm/yarnaddxterm2,使用xterm生成一个Terminal实例对象,挂载到dom元素上//webTerminal.tsximportReact,{useEffect,useState}from'react'import{Terminal}from'xterm'import{FitAddon}from'xterm-addon-fit'import从'@/components/loading'import'./style.scss';import'xterm/css/xterm.css'constWebTerminal:React.FC=()=>{const[terminal,setTerminal]=useState(null)constinitTerminal=()=>{constprefix='admin$'constfitAddon=newFitAddon()constterminal:any=newTerminal({cursorBlink:true})terminal.open(document.getElementById('terminal-container'))//终端的大小匹配父元素terminal.loadAddon(fitAddon)fitAddon.fit()terminal.writeln('\x1b[1;1;32mwellcomtowebterminal!\x1b[0m')terminal.write(prefix)setTerminal(terminal)}useEffect(()=>{initTerminal()},[])return(<Loading>

)}exportdefaultWebTerminal//style.scss.c-webTerminal__container{width:600px;height:350px;}如下图,我们可以得到一个WebTerminalshelf。在上面的代码中,我们需要引入xterm-addon-fit模块,用它来匹配生成的终端对象的大小与其父元素的大小。以上就是xterm最基本的使用。这时候我们已经有了一个生成终端的实例,但是如果我们要实现一个Web终端,这还不够。接下来,我们需要逐步给它添砖加瓦。输入操作当我们尝试输入的时候,应该有同学发现这个架子不能输入字段,我们还需要添加终端实例对象来处理输入操作。下面介绍输入操作的处理。处理这个Terminal的输入操作的思路也很简单,就是我们需要给刚刚生成的Terminal实例添加一个监听事件。当捕获到键盘的输入操作时,根据输入的值对不同的数字进行处理。由于时间仓促,我们就粗略写一些比较常用的操作进行处理,比如最基本的字母或数字输入,删除操作,以及光标上下左右操作的处理。基本输入是最基本的输入操作,代码如下//webTerminal.tsx...constWebTerminal:React.FC=()=>{const[terminal,setTerminal]=useState(null)constprefix='admin$'letinputText=''//输入字符constonKeyAction=()=>{terminal.onKey(e=>{const{key,domEvent}=econst{keyCode,altKey,altGraphKey,ctrlKey,metaKey}=domEventconstprintAble=!(altKey||altGraphKey||ctrlKey||metaKey)//禁止相关键consttotalOffsetLength=inputText.length+prefix.length//总偏移量constcurrentOffsetLength=terminal._core.buffer.x//当前x偏移量开关(keyCode){...默认值:如果(!printAble)如果(totalOffsetLength>=terminal.cols)中断如果(currentOffsetLength>=totalOffsetLength){terminal.write(key)inputText+=keybreak}constcursorOffSetLength=getCursorOffsetLength(totalOffsetLength-currentOffsetLength,'\x1b[D')terminal.write('\x1b[?K'+`${key}${inputText.slice(currentOffsetLength-prefix.length)}`)//在当前坐标写入键和坐标后的字符terminal.write(cursorOffSetLength)//将光标移动到当前位置inputText=inputText.slice(0,currentOffsetLength)+key+inputText.slice(totalOffsetLength-currentOffsetLength)}})}useEffect(()=>{if(terminal){onKeyAction()}},[terminal])......}//const.tsexportconstTERMINAL_INPUT_KEY={BACK:8,//退格删除KeyENTER:13,//回车键UP:38,//方向盘键DOWN:40,//方向盘键LEFT:37,//方向盘左键RIGHT:39//方向盘右键}其中,代码\x1b[D'和'\x1b[?K'中的'为终端的特殊字符,分别表示为将光标向左移动一位并从中擦除字符当前光标到行尾。因为作者对很多特殊字符不认识,就不展开了。解释其中,如果直接在文本末尾输入,则将拼接的字符写入文本。如果字符是在末尾以外的位置输入的,则主要过程将解释如下。当我们从左到右时,它从0开始增加。当我们从右到左时,它在原来的基础上增加1,然后逐渐减少,直到达到0,用于标记当前光标位置。假设现在输入的字符有两个字符,光标在第三个位置。主要步骤如下:1、将光标移动到第二个位置,按键盘输入字符s2,删除光标位置到字符末尾的字符3、改变输入字符拼接写入字符从光标位置到原字符文本行尾4.将光标移动到原输入位置,删除操作//webTerminal.tsx...constgetCursorOffsetLength=(offsetLength:number,subString:string='')=>{letcursorOffsetLength=''for(letoffset=0;offsetprefix.length){OffstLcurSetLength=getCursorOffsetLength(totalOffsetLength-currentOffsetLength,'\x1b[D')//保持原来的光标位置terminal._core.buffer.x=currentOffsetLength-1terminal.write('\x1b[?K'+inputText.slice(currentOffsetLength-prefix.length))terminal.write(cursorOffSetLength)inputText=`${inputText.slice(0,currentOffsetLength-prefix.length-1)}${inputText.slice(currentOffsetLength-prefix.length)}`}break...其中,如果在末尾直接输入文本,则删除光标位置字符,如果删除字符文本操作在非结束位置进行,主要过程如下假设有三个字符abc,光标在第二个位置,当删除执行操作,流程如下:1.光标移动到第二位,按键盘删除字符2,清除当前光标位置到结束字符3,将剩余字符拼接3根据偏移量,移动将光标移动到原始输入位置并按Enter//webTerminal.tsx...letinputText=''letcurrentIndex=0letinputTextList=[]consthandleInputText=()=>{terminal.write('\r\n')if(!inputText.trim()){terminal.prompt()return}if(inputTextList.indexOf(inputText)===-1){inputTextList.push(inputText)currentIndex=inputTextList.length}terminal.prompt()}...caseTERMINAL_INPUT_KEY.ENTER:handleInputText()inputText=''break...按下回车键后,需要输入字符文本存储在一个数组中,并记录当前文本位置,以供后续上/下操作使用输入文本.l长度,'\x1b[D')inputText=inputTextList[currentIndex-1]terminal.write(offsetLength+'\x1b[?K')terminal.write(inputTextList[currentIndex-1])terminal._core.buffer.x=totalOffsetLengthcurrentIndex--break}...主要步骤如下和其他的相比,上下键是把之前存储的字符取出来,先全部删除,然后写入到left/right//webTerminal。tsx...案例TERMINAL_INPUT_KEY.LEFT:if(currentOffsetLength>prefix.length){terminal.write(key)//'\x1b[D'}breakcaseTERMINAL_INPUT_KEY.RIGHT:if(currentOffsetLength