技术干货 |看我如何来解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
技术干货 |看我如何来解Web Terminal假性输入框的更多相关文章
- 5月份值得一看的 Java 技术干货!
5月又即将要离我们远去了,这个月有小长假51劳动节,有54青年节,有513母亲节,更有坑爹的520神马节?!! 废话不说,又到了总结上个月干货的时候了,这个月我们带来了各种Java技术干货,都是不得不 ...
- 推荐:7 月份值得一看的 Java 技术干货!
月底了,又到了我们总结这一个月 Java 技术干货的时候了,又到了我们给粉丝免费送书的日子了. 7 月份干货总结 Oracle 发布了一个全栈虚拟机 GraalVM 一文带你深入拆解 Java 虚拟机 ...
- 50篇经典珍藏 | Docker、Mesos、微服务、云原生技术干货
概念篇 全方位探(tian)索(keng)Mesos各种存储处理方式 老肖有话说@Mesos User Group第四次约会 技术实践 | Mesos 全方位“烹饪”指南 回顾 JAVA 发展轨迹,看 ...
- Git-【技术干货】工作中Git的使用实践
Git-[技术干货]工作中Git的使用实践 置顶 2019-09-17 21:02:16 web洋仔 阅读数 11444更多 分类专栏: Git 版权声明:本文为博主原创文章,遵循CC 4.0 B ...
- 【Bugly 技术干货】Android开发必备知识:为什么说Kotlin值得一试
1.Hello, Kotlin Bugly 技术干货系列内容主要涉及移动开发方向,是由 Bugly邀请腾讯内部各位技术大咖,通过日常工作经验的总结以及感悟撰写而成,内容均属原创,转载请标明出处. 1. ...
- 技术干货:实时视频直播首屏耗时400ms内的优化实践
本文由“逆流的鱼yuiop”原创分享于“何俊林”公众号,感谢作者的无私分享. 1.引言 直播行业的竞争越来越激烈,进过2018年这波洗牌后,已经度过了蛮荒暴力期,剩下的都是在不断追求体验.最近正好在做 ...
- H5技术干货
H5技术干货 meta标签相关知识 H5页面窗口自动调整到设备宽度,并禁止用户缩放页面 <meta name="viewport" content="width=d ...
- 走进JavaWeb技术世界7:Tomcat和其他WEB容器的区别
本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点下 ...
- Java 核心编程技术干货,2019 最新整理版!
Java技术栈 www.javastack.cn 优秀的Java技术公众号 以下是Java技术栈微信公众号发布的所有关于 Java 的技术干货,会从以下几个方面汇总,本文会长期更新. Java 基础篇 ...
- 分享iOS开发常用(三方类库,工具,高仿APP,实用网站,技术干货)
一 . JSONModel (三方类库会有更新,建议大家在线下载) http://pan.baidu.com/s/1i5ybP1z 二.AFNetworkiong http://pan.baidu. ...
随机推荐
- 开源!Django-Vue3-Admin的Python后台管理系统
Django-Vue3-Admin 项目简介 Django-Vue3-Admin 是一个基于 Django + Vue3 的前后端分离的后台管理系统,采用了最新的前后端技术栈,内置了丰富的功能模块,可 ...
- Delphi 模糊查询和字段查询
procedure TFrmain.scGPEdit1Change(Sender: TObject); var ASql, AKey: string; //模糊查询和字段查询 const vsql1: ...
- 阅读IDEA生成的equals方法--java进阶day05
1.IDEA生成的equals方法 虽然我们之前写了equals方法,但IDEA中可以快速生成equals方法,因此,我们要能看懂IDEA生成的equals方法 1.if(this==o) 2.if( ...
- 【C#】VS2019新建C#类自动添加作者版权等信息
VisualStudio2019新建C#类自动添加作者版权等信息 问题: 新建C#类的时候经常需要手动添加注释和版权等信息,那么如何新建类的时候就让编译器自动添加好这些信息呢? 解决: 参考网上的资料 ...
- 如果在安装32位Oracle客户端组件的情况下64位模式运行, 将出现此问题.
场景重现 在一台Windows 7 32-bit电脑上 安装了Oracle 11gR2 32-bit的客户端 用 VS2010 写的一个基于数据库驱动的项目 操作Oracle数据库都挺正常的 后来.. ...
- 云备份技术解析:云容灾 CT-CDR 关键技术介绍
本文分享自天翼云开发者社区<云备份技术解析:云容灾 CT-CDR 关键技术介绍>,作者:沈****军 1.CDP+存储快照,实现秒级RPO (1)CDP技术:云容灾CT-CDR(Cloud ...
- Sa-Token v1.42.0 发布 🚀,新增 API Key、TOTP 验证码、RefreshToken 反查等能力
Sa-Token 是一款 免费.开源 的轻量级 Java 权限认证框架,主要解决:登录认证.权限认证.单点登录.OAuth2.0.微服务网关鉴权 等一系列权限相关问题. 目前最新版本 v1.42.0 ...
- spring_声明式事务: @Transactional
例子. <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://ww ...
- centos7防火墙启动关闭
1. 查看防火墙状态 systemctl status firewalld.service 2. 查看对外开放的端口号 firewall-cmd --list-ports 3. 添加端口号 firew ...
- Electron35-DeepSeek桌面端AI系统|vue3.5+electron+arco客户端ai模板
2025跨平台ai实战electron35+vite6+arco仿DeepSeek/豆包ai流式打字聊天助手. electron-deepseek-chat:实战ai大模型对话,基于vue3.5+el ...