0%

使用xterm.js制作web-terminal

xterm.js是什么?

xterm 是大多数 web-terminal 的解决方案,如vscode、atom等等。支持 bash,vim 和 tmux。并且还支持webgl渲染。

安装

1
2
3
4
5
6
7
8
9
// 1、安装 xterm
npm install --save xterm

// 2、安装xterm-addon-fit
// xterm.js的插件,使终端的尺寸适合包含元素。
npm install --save xterm-addon-fit

// 3、支持打开链接
npm install --save xterm-addon-web-links

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit'
import { WebLinksAddon } from 'xterm-addon-web-links';
import 'xterm/css/xterm.css'

const initTerminal = (terminal) => {
const fitAddon = new FitAddon()

terminal.open(document.getElementById('terminal-container'))
terminal.loadAddon(fitAddon) // terminal 的尺寸与父元素匹配
fitAddon.fit()
terminal.focus()

terminal.prompt = () => {
terminal.write('Samonnite $')
}

terminal.writeln('\x1b[1;1;32mwellcom to web terminal!\x1b[0m')
terminal.prompt()

setTerminal(terminal)
}

主要方法:监听、写入

1
2
3
terminal!.onData((data: string) => {
// xxxx
}

纯前端实现也可以使用terminal!.onKey方法

1
terminal!.write('Hello World');

前端只有在完成这两项工作后才能使用,否则只是个黑板。。。

通过onKey监听键盘实现纯前端Terminal

  1. 正常键盘输入后写入
  2. ctrl、command、shift、alt等特殊按键,实现删除、复制粘贴
  3. 上下左右键切换实现光标位置变化及命令回溯
  4. 回车提交换行
1
2
3
4
5
6
7
8
9
const TERMINAL_INPUT_KEY = {
BACK: 8, // 退格删除键
ENTER: 13, // 回车键
UP: 38, // 方向盘上键
DOWN: 40, // 方向盘键
LEFT: 37, // 方向盘左键
RIGHT: 39, // 方向盘右键
CHAR_C: 67 // 字符C
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
terminal.onKey(e => {
const { key } = e
const { keyCode, altKey, altGraphKey, ctrlKey, metaKey } = e.domEvent

const printAble = !(altKey || altGraphKey || ctrlKey || metaKey) // 禁止相关按键
const totalOffsetLength = inputText.length + prefix.length // 总偏移量
let currentOffsetLength = terminal._core.buffer.x // 当前x偏移量
console.log('currentOffsetLength ========= ', currentOffsetLength)

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

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

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
}

case TERMINAL_INPUT_KEY.DOWN: {
if (!inputTextList[currentIndex + 1]) break

// 构造退格(模拟替换效果) \b \b标识退一格; \b\b \b\b表示退两格...
const backLength = getCursorOffsetLength(inputTextList[currentIndex].length, '\b')
const blackLength = getCursorOffsetLength(inputTextList[currentIndex].length, ' ')
inputText = inputTextList[currentIndex + 1]
terminal.write(backLength + blackLength + backLength)
terminal.write(inputTextList[currentIndex + 1])
terminal._core.buffer.x = totalOffsetLength
currentIndex++
break
}

case TERMINAL_INPUT_KEY.LEFT:
if (currentOffsetLength > prefix.length) {
terminal.write(key)
}
break

case TERMINAL_INPUT_KEY.RIGHT:
if (currentOffsetLength < totalOffsetLength) {
terminal.write(key)
}
break

default:
if(keyCode === TERMINAL_INPUT_KEY.CHAR_C && ctrlKey) {
handleCtrlC()
break
}
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)
}
})

实际生产项目中纯前端实现各种命令开发成本非常高,需利用websocket与后端通信,完成输入输出。
此时前端只负责输入、显示,所有的命令交互都交给后端来做。
如node-pty、gotty等等。

踩过的坑

  • 屏幕onResize时需根据父元素宽高匹配行列数,xterm-addon-fit无法满足需求,总是会出现无法自适应屏幕问题,文字被遮挡。
    解决方案:
    获取父元素宽高,文字宽高,并配置rows、cols,即行数及列数。
    vscode解决方案
  • vim模式下粘贴内容丢失
    解决方案:
    对文本进行分片,以此进行输入。

其他