2021年7月19日星期一

如何搭建一个简易的 Web Terminal(一)

前言

在介绍本篇文章的时候,先说一下本篇文章的一些背景。笔者是基于公司的基础建设哆啦 A 梦(Doraemon)一些功能背景写的这篇文章,不了解、有兴趣的同学可以去 袋鼠云 的 github 下面了解一下百宝箱哆啦 A 梦。 在哆啦 A 梦中可以配置代理,我们在配置中心的配置详情下,可以找到主机对应的 nginx 配置文件或者其他文件,可以在这里对其进行编辑,但是这个功能模块下的 Execute shell 其实只是一个输入框,这给使用者会造成一种,这个输入框是一个 Web Terminal 的假象。因此,为了解决这个问题,我们打算做一个简易版的 Web Terminal 去解决这个问题。笔者就是在这个背景之下开始了对于 Web Terminal 的调研,写下了这篇文章。

file

本篇文章取名如何搭建一个简易的 Web Terminal,主要还是会围绕这个主题,结合哆啦 A 梦去进行述说,逐步衍生出涉及到的点,笔者思考的一些点。当然,实现 Web Terminal 的方式可能有很多种,笔者也在调研过程中,同时,本篇文章写的时间也比较仓促,涉及到的点也比较多,如若本文有不对之处,欢迎同学指出,笔者一定及时改正。

Xterm.js

首先,我们需要一个组件帮助我们快速的搭建起来 Web Terminal 的基本框架,它就是--Xterm.js。那么 Xterm.js 是什么呢,官方的解释如下

Xterm.js 是一个用 TypeScript 编写的前端组件,它可以让应用程序在浏览器中为用户带来功能齐全的终端。它被 VS Code、Hyper 和 Theia 等流行项目使用。

因为本篇文章主要还是围绕着搭建一个 Web Terminal,所以涉及到 Xterm.js 的详细的 API 就不介绍了,只简单介绍一下基本的 API,大家现在只用知道它是一个组件,我们需要使用到它,有兴趣的同学可以点击 官方文档 进行阅读。

基本 API

  • Terminal

构造函数,可生成 Terminal 实例

import { Terminal } from 'xterm';const term = new Terminal();
  • onKey、onData

Terminal 实例上监听输入事件的函数

  • write

Terminal 实例上写入文本的方法

  • loadAddon

Terminal 实例上加载插件的方法

  • attach 、fit 插件

fit 插件可以适配调整 Terminal 的大小,使得其适配 Terminal 的父元素

attach 插件提供了将终端附加到 WebSocket 流的方法,以下是官网使用的例子

import { Terminal } from 'xterm';import { AttachAddon } from 'xterm-addon-attach';const term = new Terminal();const socket = new WebSocket('wss://docker.example.com/containers/mycontainerid/attach/ws');const attachAddon = new AttachAddon(socket);// Attach the socket to termterm.loadAddon(attachAddon);

基本使用

作为一个组件,我们需要先了解一下他的基本使用,如何能够快速的搭建起来 Web Terminal 的基本框架。以下使用哆啦 A 梦的代码为例

1、首先第一步是安装 Xterm

npm install xterm / yarn add xterm

2、使用 xterm 生成 Terminal 实例对象,将其挂载到 dom 元素上

// webTerminal.tsximport React, { useEffect, useState } from 'react'import { Terminal } from 'xterm'import { FitAddon } from 'xterm-addon-fit'import Loading from '@/components/loading'import './style.scss';import 'xterm/css/xterm.css'const WebTerminal: React.FC = () => { const [terminal, setTerminal] = useState(null) const initTerminal = () => {  const prefix = 'admin $ '  const fitAddon = new FitAddon()  const terminal: any = new Terminal({ cursorBlink: true })  terminal.open(document.getElementById('terminal-container'))  // terminal 的尺寸与父元素匹配  terminal.loadAddon(fitAddon)  fitAddon.fit()  terminal.writeln('\x1b[1;1;32mwellcom to web terminal!\x1b[0m')  terminal.write(prefix)  setTerminal(terminal) } useEffect(() => { initTerminal() }, []) return (  <Loading>   <div id="terminal-container" className='c-webTerminal__container'></div>  </Loading> )}export default WebTerminal
// style.scss.c-webTerminal__container { width: 600px; height: 350px;}

如下图所示,我们就此可以得到一个 Web Terminal 的架子。在上面的代码中,我们需要引入 xterm-addon-fit 模块,使用其将生成的 terminal 对象的尺寸与它的父元素的尺寸匹配。

file

以上是 xterm 最基本的使用,当在这个时候,我们就有生成的这个 terminal 的实例,但是如果要实现一个 Web terminal 的话,这还远远不够,接下来我们需要逐步的为其添砖加瓦。

输入操作

当我们尝试输入的时候,有的同学应该发现了,这个架子并不能输入字段,我们还需要增加 terminal 实例对象对输入操作的处理。下面介绍一下输入操作的处理,对这个 Terminal 的输入操作的处理的思路也很简单,就是我们需要对刚刚生成的这个 Terminal 实例添加监听事件,当捕捉到有键盘的输入操作的时候,根据输入的值对应不同的数字进行处理。

由于时间比较的仓促,我们就大致写一些比较常见的操作进行处理,比如最基本字母或数字的输入,删除操作,光标上下左右操作的处理。

基本输入

首先是最基本的输入操作,代码如下

// webTerminal.tsx...const WebTerminal: React.FC = () => { const [terminal, setTerminal] = useState(null) const prefix = 'admin $ '  let inputText = '' // 输入字符  const onKeyAction = () => {  terminal.onKey(e => {   const { key, domEvent } = e   const { keyCode, altKey, altGraphKey, ctrlKey, metaKey } = domEvent   const printAble = !(altKey || altGraphKey || ctrlKey || metaKey) // 禁止相关按键   const totalOffsetLength = inputText.length + prefix.length // 总偏移量   const currentOffsetLength = terminal._core.buffer.x  // 当前x偏移量   switch(keyCode) {   ...   default:    if (!printAble) break    if (totalOffsetLength >= terminal.cols) break    if (currentOffsetLength >= totalOffsetLength) {     terminal.write(key)     inputText += key     break    }    const cursorOffSetLength = getCursorOffsetLength(totalOffsetLength - currentOffsetLength, '\x1b[D')    terminal.write('\x1b[?K' + `${key}${inputText.slice(currentOffsetLength - prefix.length)}`) // 在当前的坐标写上 key 和坐标后面的字符    terminal.write(cursorOffSetLength) // 移动停留在当前位置的光标    inputText = inputText.slice(0, currentOffsetLength) + key + inputText.slice(totalOffsetLength - currentOffsetLength)   }  }) } useEffect(() => {  if (terminal) {   onKeyAction()  } }, [terminal]) ... ...}// const.tsexport const TERMINAL_INPUT_KEY = { BACK: 8, // 退格删除键 ENTER: 13, // 回车键 UP: 38, // 方向盘上键 DOWN: 40, // 方向盘键 LEFT: 37, // 方向盘左键 RIGHT: 39 // 方向盘右键}

其中,代码中的 '\x1b[D''\x1b[?K' 是终端的特殊字符,分别表示为光标向左移一位和擦除当前光标到行末的字符,特殊字符因为笔者了解也不是很多,就不展开说明了。其中,在文本末尾直接进行输入则拼接字符写入文本,如果在非末尾的位置输入字符,则主要过程如下

file

讲解之前先说一下这个 currentOffsetLength,也就是 terminal._core.buffer.x 这个的取值,当我们从左往右的时候他是从 0 开始增加,当我们从右往左的时候,他是在原有基础上+1,在逐次递减,递减到 0,用来标记当前光标的位置

假设现在输入的字符有两个字符,光标在第三位,主要发生有一下步骤:

1、光标移到第二位,按下键盘输入字符 s

2、删除光标位置到字符末尾的字符

3、将输入的字符与原有字符文本的光标位置到行末的字符拼接写入

4、将光标移到原有的输入位置

删除操作
// webTerminal.tsx...const getCursorOffsetLength = (offsetLength: number, subString: string = '') => { let cursorOffsetLength = '' for (let offset = 0; offset < offsetLength; offset++) {  cursorOffsetLength += subString } return cursorOffsetLength}...case TERMINAL_INPUT_KEY.BACK: if (currentOffsetLength > prefix.length) {  const cursorOffSetLength = getCursorOffsetLength(totalOffsetLength - currentOffsetLength, '\x1b[D') // 保留原来光标位置  terminal._core.buffer.x = currentOffsetLength - 1  terminal.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...

其中,在文本末尾直接进行输入则删除该光标位置字符,如果在非末尾的位置进行删除字符文本操作,则主要过程如下

file

假设现在有 abc 三个字符,其中光标在第二个位置,当其进行删除操作的时候,过程如下:

1、光标移到第二位,按下键盘删除字符

2、清除当前的光标位置到末尾的字符原文转载:http://www.shaoqun.com/a/887695.html

跨境电商:https://www.ikjzd.com/

www.made-in-china.com:https://www.ikjzd.com/w/2777

亚马逊全球开店:https://www.ikjzd.com/w/1299

淘粉吧怎么返利:https://www.ikjzd.com/w/1725


前言在介绍本篇文章的时候,先说一下本篇文章的一些背景。笔者是基于公司的基础建设哆啦A梦(Doraemon)一些功能背景写的这篇文章,不了解、有兴趣的同学可以去袋鼠云的github下面了解一下百宝箱哆啦A梦。在哆啦A梦中可以配置代理,我们在配置中心的配置详情下,可以找到主机对应的nginx配置文件或者其他文件,可以在这里对其进行编辑,但是这个功能模块下的Executeshell其实只是一个输入框,这
tenso:https://www.ikjzd.com/w/1552
省内自驾旅游攻略 :http://www.30bags.com/a/406755.html
省钱旅游小贴士:http://www.30bags.com/a/414429.html
省青旅蛙步网年终大会顺利召开 :http://www.30bags.com/a/406687.html
省文化和旅游厅从四个方面入手确保市场主体逐步走向正轨:http://www.30bags.com/a/434695.html
闺蜜把我下面摸出水了 闺蜜用黄瓜折磨我过程:http://lady.shaoqun.com/a/248345.html
口述我和两女玩双飞的经历 那一夜夹得我好紧好爽:http://lady.shaoqun.com/m/a/248286.html
口述:郊游那夜色妻和老外挤帐篷妻子老外郊游:http://lady.shaoqun.com/m/a/36701.html
如何增加亚马逊留评率?:https://www.ikjzd.com/articles/146720
8月15日起,HPL征收美线5000$/箱增值附加费!美西铁路进口转运暂停7天:https://www.ikjzd.com/articles/146729
今年中美航运成本飙涨翻倍!一些航线运费上涨近10倍!:https://www.ikjzd.com/articles/146728
小学女生被4个男生拖到男厕所。嫌疑人不满14岁,没有立案:http://lady.shaoqun.com/a/425284.html

没有评论:

发表评论