前言
xterm.js是模拟终端产品中最为市场推崇的,较多的开源项目在实现Shell模拟终端都是用了这个产品。 在DockerUI中,dockerUI提供了类似Shell的功能,可以在DockerUI里直接连接到容器里,执行容器的终端命令, 类似于在docker环境下,执行docker container exec -it 这样的命令行功能。 在DockerUI里也使用了xterm.js这个项目来实现了WEB方式的模拟控制台终端。
先看看效果图
注意看清楚哟, 和Xshell长的很像,但是是WEB方式实现的。
今天这篇文章,就来谈谈xterm.js在DockerUI里的具体实现Web控制台的过程。
引入xterm.js
说实在话,虽然这个xterm.js确实在此类产品中的名气确实最大,但是其官方网站上的文档和资料就真的是匹配不上这个江湖地位了, 文档基本没有, 只能看源码进行猜和试错。 我们DockerUI里对xterm.js的集成,基本上全部都是自己试出来的。
ROOT_RES_URL + "/static/plugins/xterm/lib/xterm.js",
ROOT_RES_URL + "/static/plugins/xterm/lib/xterm-addon-fit.js"
ROOT_RES_URL + "/static/plugins/xterm/css/xterm.css"
DockerUI项目没有使用node.js的开发架构, 是使用的CubeUI的前台开发架构,基于EasyUI的前台框架改造而来, 属于类似layui的开发方式,都是原生态的js+html的前后台分离方式。 但是xterm.js的官网里都是基于npm的方式,不适用于这里。
实例化Terminate对象
在DockerUI里把这个Terminate创建过程进行封装,封装一个方法来实现
function createTerminate(target, onKey, rows, cols){
rows = rows || 36;
cols = cols || 80;
let term ;
term = new Terminal({
rendererType: "canvas", //渲染类型
convertEol: true, //启用时,光标将设置为下一行的开头
scrollback: 100, //终端中的回滚量
disableStdin: false, //是否应禁用输入。
cursorStyle: "underline", //光标样式
cursorBlink: true, //光标闪烁
cols: cols,
rows: rows,
theme: {
foreground: "#14e264", //字体
background: "#002833", //背景色
cursor: "help", //设置光标
lineHeight: 16
},
bellStyle:'sound',
rightClickSelectsWord:true,
screenReaderMode:true,
allowProposedApi: true,
LogLevel: 'debug',
tabStopWidth: 4,
});
term.onKey((event) => {
if(onKey){
onKey.call(term, event.key, event.domEvent)
}
});
term.open(target);
//term.open(document.getElementById('container-terminal'));
term.writeln('Welcome to web-console of docker.ui');
term.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
term.writeln('Type some keys and commands to play around. Press the key "ctrl-Z" to exit the console');
term.focus()
return term
}
在这一段代码里, 把传入的target对象进行绑定,根据传入的rows,cols产生一个Terminal对象,并且绑定了Terminal的onkey事件,当Terminal对象里有输入时触发事件。如果和后天Docker容器的通信的监听事件绑定到一起,这样Terminal有输入事件,输入字符后,把对应字符转换成命令行,发送到Docker通信的通道里,Docker容器通过通道收到WebConsole发送过来的命令,进行处理,处理后通过通道返回执行结果给Ternimal, Terminal通过调用write或者writeln方法,把返回结果显示到webconsole里即可。 这个方案完全可行。
开始实现和Docker容器的通信
很明显这里是个双向通信的通道方式, 通过Web实现双向通信最佳的方案当然就是webSocket的方式了, 所以首先实现go后端的http调用支持websocket。 dockerUI项目的Http服务这块都不是用的http的原生服务,有兴趣的可以关注我的其他的文章,很多文章都谈到了这点, 同样DockerUI的http服务也是使用了fasthttp作为底层服务。 在fasthttp里实现支持websocket的handler。
func InitWsRouter(router *routing.Router, routerGroup *routing.RouteGroup) {
routerGroup.Any("/echo", EchoWSHandler)
routerGroup.Any("/exec", ExecWSHandler)
}
func ExecWSHandler(ctx *routing.Context) error {
execId := string(ctx.FormValue("id"))
if util4go.IsEmpty(execId) {
fasthttputil.Result.Fail("没有设置需要执行的ID").Response(ctx.RequestCtx)
return nil
}
ri, err := getRequestInfo4WS("/docker-api-ws", ctx)
if err != nil {
return err
}
err = upgrader.Upgrade(ctx.RequestCtx, func(ws *websocket.Conn) {
defer ws.Close()
ctx.Request.Header.Del("Origin")
hijackExecStartOperation(ws, ri.host, execId, ri.version)
})
if err != nil {
if _, ok := err.(websocket.HandshakeError); ok {
Logger.Info(err)
}
return err
}
ctx.Response.Header.Set("Sec-WebSocket-Protocol", ri.version)
return nil
}
前台通过websocket调用此REST API,然后和Terminal交互
let url = context + "/docker-api-ws/exec?id="+response.ExecID;
ws = $.app.websocket(
url,
function(e){
console.log(e);
term = createTerminate(document.getElementById("container-terminal-"+response.ExecID),
function(key, ev){
//ws.send(key)
if(!sendWs(ws, key)){
alert('控制端已经失去通信,请重新打开');
}
});
if(fn){
fn()
}
wses.push(ws);
}, function (e) {
term.write(e.data)
console.log(e);
}, function (e) {
console.log(e);
closeConsoleDg(dgId, false);
}, function (e) {
console.log(e);
$.app.show("错误消息{0}".format(e.reason))
}, [local_node.node_host, local_node.node_port, local_node.node_version])
看看最终的效果
20220814_213300