技术干货 |看我如何来解Web Terminal假性输入框

编者按

写在前面的话

在介绍本篇文章的时候,先说一下本篇文章的一些背景。笔者是基于公司的基础建设哆啦 A 梦(Doraemon)

(https://github.com/DTStack/doraemon)一些功能背景写的这篇文章。不了解或者有兴趣的同学可以去袋鼠云

(https://github.com/DTStack) 的 github 下面了解一下百宝箱哆啦 A 梦。

在哆啦 A 梦中可以配置代理,我们在配置中心的配置详情下,可以找到主机对应的 nginx 配置文件或者其他文件,可以在这里对其进行编辑,但是这个功能模块下的 Execute shell 其实只是一个输入框,这给使用者会造成一种,这个输入框是一个 Web Terminal 的假象。

因此,为了解决这个问题,我们打算做一个简易版的 Web Terminal 去解决这个问题。笔者就是在这个背景之下开始了对于 Web Terminal 的调研,写下了这篇文章。

图片

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

 Xterm.js 

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

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

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

基本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 term
term.loadAddon(attachAddon);

基本使用

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

1、首先第一步是安装 Xterm

npm install xterm / yarn add xterm

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

// webTerminal.tsx
import 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 对象的尺寸与它的父元素的尺寸匹配。

图片

以上是 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.ts
export const TERMINAL_INPUT_KEY = {
BACK: 8, // 退格删除键
ENTER: 13, // 回车键
UP: 38, // 方向盘上键
DOWN: 40, // 方向盘键
LEFT: 37, // 方向盘左键
RIGHT: 39 // 方向盘右键
}

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

图片

讲解之前先说一下这个 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
...

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

图片

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

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

2、清除当前的光标位置到末尾的字符

3、根据偏移量拼接剩余字符

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

回车操作

// webTerminal.tsx
...
let inputText = ''
let currentIndex = 0
let inputTextList = []


const handleInputText = () => {
terminal.write('\r\n')
if (!inputText.trim()) {
terminal.prompt()
return
}

if (inputTextList.indexOf(inputText) === -1) {
inputTextList.push(inputText)
currentIndex = inputTextList.length
}

terminal.prompt()
}

...
case TERMINAL_INPUT_KEY.ENTER:
handleInputText()
inputText = ''
break
...

按下回车键后,需要将输入的字符文本存入数组中,记录当前文本位置,以便后续利用

向上/向下操作

// webTerminal.tsx
...
case TERMINAL_INPUT_KEY.UP: {
if (!inputTextList[currentIndex - 1]) break

const offsetLength = getCursorOffsetLength(inputText.length, '\x1b[D')

inputText = inputTextList[currentIndex - 1]
terminal.write(offsetLength + '\x1b[?K' )
terminal.write(inputTextList[currentIndex - 1])
terminal._core.buffer.x = totalOffsetLength
currentIndex--

break
}
...

其中主要的步骤如下

图片

相对于其他,向上或向下按键就是将之前存储的字符拿出来,先全部删除,再进行写入。

向左/向右操作

// webTerminal.tsx
...
case TERMINAL_INPUT_KEY.LEFT:
if (currentOffsetLength > prefix.length) {
terminal.write(key) // '\x1b[D'
}
break

case TERMINAL_INPUT_KEY.RIGHT:
if (currentOffsetLength < totalOffsetLength) {
terminal.write(key) // '\x1b[C'
}
break
...

待完善的点

1、接入 websocket,实现服务端和客户端之间的通信

2、接入 ssh,目前只是添加了终端的输入操作,我们最终的目的还是需要让它能够登陆到服务器上面

设想中的最后实现的效果应该是这样的

图片

笔者也对与当前的代码进行了 socket.io 的接入,哆啦 A 梦的话是基于 egg 的这个框架的,可以使用这个 egg.socket.io 建立 socket 通信,笔者在这里列了一下大概的步骤,但是准备作为本文的补充,会在下一篇文章中完善。

总结

首先,这个终端写到这里并没写完,由于时间的原因,暂未写完。上面也列了一些待完善的点,笔者也会在后面添加本文的第二或第三篇,陆续陆续的补充完善。笔者在这个星期也尝试了接入 socket,但是还是有点问题,没有完善好,所以最终还是决定,本篇文章还是着重描写一些输入操作的处理。最后,如果大家对于本篇文章有疑惑,欢迎踊跃发言。

更多

  • 官方文档:https://xtermjs.org/

  • Socket.IO 文档:https://eggjs.org/zh-cn/tutorials/socketio.html

  • 终端特殊字符:https://blog.csdn.net/sunjiajiang/article/details/8513215

 

阅读终点,创作起航,您可以撰写心得或摘录文章要点写篇博文。去创作
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: Spark Streaming 和 Flink 都是流处理框架,但在一些方面有所不同。 1. 数据处理模型 Spark Streaming 基于批处理模型,将流数据分成一批批进行处理。而 Flink 则是基于流处理模型,可以实时处理数据流。 2. 窗口处理 Spark Streaming 的窗口处理是基于时间的,即将一段时间内的数据作为一个窗口进行处理。而 Flink 的窗口处理可以基于时间和数据量,可以更加灵活地进行窗口处理。 3. 状态管理 Spark Streaming 的状态管理是基于 RDD 的,需要将状态存储在内存中。而 Flink 的状态管理是基于内存和磁盘的,可以更加灵活地管理状态。 4. 容错性 Flink 的容错性比 Spark Streaming 更加强大,可以在节点故障时快速恢复,而 Spark Streaming 则需要重新计算整个批次的数据。 总的来说,Flink 在流处理方面更加强大和灵活,而 Spark Streaming 则更适合批处理和数据仓库等场景。 ### 回答2: Spark Streaming 和 Flink 都是流处理框架,它们都支持低延迟的流处理和高吞吐量的批处理。但是,它们在处理数据流的方式和性能上有许多不同之处。下面是它们的详细比较: 1. 处理模型 Spark Streaming 采用离散化流处理模型(DPM),将长周期的数据流划分为离散化的小批量,每个批次的数据被存储在 RDD 中进行处理,因此 Spark Streaming 具有较好的容错性和可靠性。而 Flink 采用连续流处理模型(CPM),能够在其流处理过程中进行事件时间处理和状态管理,因此 Flink 更适合处理需要精确时间戳和状态管理的应用场景。 2. 数据延迟 Spark Streaming 在处理数据流时会有一定的延迟,主要是由于对数据进行缓存和离散化处理的原因。而 Flink 的数据延迟比 Spark Streaming 更低,因为 Flink 的数据处理和计算过程是实时进行的,不需要缓存和离散化处理。 3. 机器资源和负载均衡 Spark Streaming 采用了 Spark 的机器资源调度和负载均衡机制,它们之间具有相同的容错和资源管理特性。而 Flink 使用 Yarn 和 Mesos 等分布式计算框架进行机器资源调度和负载均衡,因此 Flink 在大规模集群上的性能表现更好。 4. 数据窗口处理 Spark Streaming 提供了滑动、翻转和窗口操作等灵活的数据窗口处理功能,可以使用户更好地控制数据处理的逻辑。而 Flink 也提供了滚动窗口和滑动窗口处理功能,但相对于 Spark Streaming 更加灵活,可以在事件时间和处理时间上进行窗口处理,并且支持增量聚合和全量聚合两种方式。 5. 集成生态系统 Spark Streaming 作为 Apache Spark 的一部分,可以充分利用 Spark 的分布式计算和批处理生态系统,并且支持许多不同类型的数据源,包括Kafka、Flume和HDFS等。而 Flink 提供了完整的流处理生态系统,包括流SQL查询、流机器学习和流图形处理等功能,能够灵活地适应不同的业务场景。 总之,Spark Streaming 和 Flink 都是出色的流处理框架,在不同的场景下都能够发挥出很好的性能。选择哪种框架取决于实际需求和业务场景。 ### 回答3: Spark Streaming和Flink都是流处理引擎,但它们的设计和实现方式有所不同。在下面的对比中,我们将比较这两种流处理引擎的主要特点和差异。 1. 处理模型 Spark Streaming采用离散流处理模型,即将数据按时间间隔分割成一批一批数据进行处理。这种方式可以使得Spark Streaming具有高吞吐量和低延迟,但也会导致数据处理的粒度比较粗,难以应对大量实时事件的高吞吐量。 相比之下,Flink采用连续流处理模型,即数据的处理是连续的、实时的。与Spark Streaming不同,Flink的流处理引擎能够应对各种不同的实时场景。Flink的实时流处理能力更强,因此在某些特定的场景下,它的性能可能比Spark Streaming更好。 2. 窗口计算 Spark Streaming内置了许多的窗口计算支持,如滑动窗口、滚动窗口,但支持的窗口计算的灵活性较低,只适合于一些简单的窗口计算。而Flink的窗口计算支持非常灵活,可以支持任意窗口大小或滑动跨度。 3. 数据库支持 在处理大数据时,存储和读取数据是非常重要的。Spark Streaming通常使用HDFS作为其数据存储底层的系统。而Flink支持许多不同的数据存储形式,包括HDFS,以及许多其他开源和商业的数据存储,如Kafka、Cassandra和Elasticsearch等。 4. 处理性能 Spark Streaming的性能比Flink慢一些,尤其是在特定的情况下,例如在处理高吞吐量的数据时,在某些情况下可能受制于分批处理的架构。Flink通过其流处理模型和不同的调度器和优化器来支持更高效的实时数据处理。 5. 生态系统 Spark有着庞大的生态系统,具有成熟的ML库、图处理库、SQL框架等等。而Flink的生态系统相对较小,但它正在不断地发展壮大。 6. 规模性 Spark Streaming适用于规模小且不太复杂的项目。而Flink可扩展性更好,适用于更大、更复杂的项目。Flink也可以处理无限制的数据流。 综上所述,Spark Streaming和Flink都是流处理引擎,它们有各自的优缺点。在选择使用哪一个流处理引擎时,需要根据实际业务场景和需求进行选择。如果你的业务场景较为复杂,需要处理海量数据并且需要比较灵活的窗口计算支持,那么Flink可能是更好的选择;如果你只需要简单的流处理和一些通用的窗口计算,Spark Streaming是更为简单的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

袋鼠云数栈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值