用 xterm.js 实现一个简易的 web-terminal !
前言
大家新年好呀~ 因为工作比较忙,有一段时间没更新了(其实就是懒),一直没想好写啥,直到最近工作中遇到了个需要内嵌 网页终端(web-terminal) 的需求,踩了不少坑,终于整明白了大概,想着写篇文章回馈下社区,于是乎说干就干,走起~
xterm.js 初探
知道需要做 web-terminal ,第一件事先网上调研一下具体需要的技术,最后发现 xterm.js 为大多数 web-terminal 的解决方案,大名鼎鼎的 vscode 也在用,看来可靠性还是有所保证的。
于是乎,我兴高采烈的敲进官网找 demo,瞅了一眼,好家伙看起来挺简单啊,只需要安装一下,初始化实例就行了,如下:
1 | npm install xterm |
1 |
|
因为我们项目的基于 React
的,所以我准备用 create-react-app
写个 demo 试试,一顿操作之后,把官网的例子拷进 demo 里,然后运行一下,看看效果。
随后页面出来了终端的样式,如下:
正准备输写字符看看效果,好家伙。。居然无法输入,我一度怀疑是我 demo 复制错了,仔细比对,发现确实没错啊??
然后我找了找文档,发现输入还需要调用 api 才行,感情官网的例子居然还不能直接运行的,也是第一次见。
调整了下代码
1 | term.open(document.getElementById("terminal")); |
输入终于可以了,但是新的问题又来了,一删除就报错
而且一回车就光标回到最开始了,这。。我不禁陷入了沉思,再次回到文档找寻,发现了文档对 onData
的描述
contains real string data with any valid Unicode codepoint, thus the payload should be treated as UTF-16/UCS-2. For OS interaction this data should be converted to UTF-8 bytes (automatically done by
node-pty
). If you need legacy encoding support, see below.
原来 onData
返回的都是 UTF-16/UCS-2 编码的,要让系统认识得输出成 UTF-8 编码,怪不得我直接输入会有问题,还得自己转一下编码…这可难为我了,难道那么多按键都要做一遍解析?
幸好官方已经提出了解决方案,那就是用 node-pty
进行自动解析。使用方式也很简单,官网有如下代码
1 | pty.onData(recv => terminal.write(recv)); |
大意就是让 onData
返回的 UTF-16/UCS-2 字符串用 node-pty
解析成系统可读的 UTF-8 编码的字符串来完成输入,很明显,我们需要创建他们之间的联系,把 xterm.js
当浏览器的图形渲染界面, node-pty
当服务端监听输入的并转码的工具,通过 websocket 来关联起两边的关系,看上去可行!
使用 node-pty 解析键盘输入信号
既然知道 node-pty
可以解析,我们首先需要安装它,根据官网的描述,不同系统安装 node-pty
需要有不同的准备工作,这也可以理解,因为不同系统会有不同的差异。
Linux/Ubuntu
1 | sudo apt install -y make python build-essential |
macOS
1 | Xcode is needed to compile the sources, this can be installed from the App Store. |
Windows
1 | npm install --global --production windows-build-tools |
安装完之后,创建我们服务端的文件 server.js,果汁用 express
express-ws
来搭建 node 服务 和启用 websocket 服务。
1 | const express = require("express"); |
然后加入 node-pty
的初始代码。
1 | const express = require("express"); |
实际场景中,还会有多个终端共同工作的场景,这样我们在服务器启动就直接初始化显然无法满足,怎么办呢?
经过一番思索.. 有了!果汁的想法是客户端初始化终端实例的时候,就初始化服务端 pty
实例,不同的终端初始化不同的 pty
实例,通过 pid
来区分,这样如果有拓展多终端的场景也可以满足。
客户端方面通过发送一个初始化的请求到服务端,服务端初始化完 pty
实例,返回当前实例的 pid
,然后客户端和服务端每次进行 websocket 交互的时候都带上 pid
,服务端通过 pid
去拿对应的 pty
实例,返回解析后的值给客户端,这样就实现了多终端的场景!
改造我们之前的代码
1 | ... |
客户端连接 websocket
服务端大功告成!接下来我们开始编写客户端代码,客户端需要创建 websocket 连接。
1 | const socketURL = "ws://127.0.0.1:4000/socket/"; |
光这样还不够,我们还需要获取服务端 pty
实例的 pid
,来当做连接的唯一标识符,这就简单了,直接通过接口获取就行。
1 | import axios from "axios"; |
xterm.js
本身提供了拓展包的能力,这里我们用到它的一个扩展包 xterm-addon-attach
,它可以帮我们自动和websocket进行交互,省去我们自己写了。
注意:xterm-addon-attach 需要 xterm.js v4+
1 | import { AttachAddon } from "xterm-addon-attach"; |
这样客户端代码也完成啦~
让我们启动下看看。
居然跨域了。。好吧,那我们再在服务端加入防跨域代码。
1 | // //解决跨域问题 |
重启服务器看效果~
可看到我们已经成功运行起来了,果汁也通过 web-terminal
成功远程登陆了家里的 树莓派,看上去体验还不错哈
总结
从代码量上可以看出,实现一个 web-terminal 并不是特别困难,主要还是思路。想要源码的小伙伴,我已经把代码传到 github 上了,传送门 在这里,路过点个赞👍 就是对我最大的支持啦,那我们下期再见~