文章目录
一、xterm实例化,生成terminal终端页面
1.安装xterm所需要的相关依赖包
"xterm": "^4.9.0",
"xterm-addon-attach": "^0.6.0", // 用来支持xterm链接websocket
"xterm-addon-fit": "^0.4.0", // 用来让xterm终端自适应页面大小变化
"xterm-addon-unicode11": "^0.2.0", // (非必要库)为xterm.js 提供 Unicode 版本 11 规则的插件
这是terminal实例化所需要的依赖包,版本可自行调整,目前我没遇到版本带来的问题
2.安装好依赖之后进行引入
import {
Terminal } from "xterm";
import {
FitAddon } from "xterm-addon-fit";
import {
Unicode11Addon } from "xterm-addon-unicode11";
import _lodash from "lodash";
3.写一个dom,用来挂载terminal,并引入依赖包
<template>
<div id="term" style="background-color: #1e1e1e; height: 100%"></div>
</template>
<script>
import {
Terminal } from "xterm";
import {
FitAddon } from "xterm-addon-fit";
import {
Unicode11Addon } from "xterm-addon-unicode11";
import "xterm/css/xterm.css"; // 这个css样式必须要引入,不然生成的terminal终端会有问题
</script>
4.初始化终端
export default {
data() {
return{
// 定义一些初始化变量
term: null,
unicode11Addon: new Unicode11Addon(),
fitAddon: new FitAddon(),
}
},
mounted() {
this.initTerm()
}
methods: {
// 初始化term终端
initTerm() {
// 实例化term以及依赖包
this.term = new Terminal({
cursorBlink: true, // 光标闪烁
cursorStyle: "underline", // 光标闪烁样式
rendererType: "canvas", // 渲染类型
theme: {
background: "#1d242b", selection: "rgba(245, 108, 108, 0.5)" }, // 主题样式
fontFamily: 'Consolas, Menlo, Monaco, "Courier New", monospace',
scrollback: 10000,
})
this.term.loadAddon(this.fitAddon);
this.term.loadAddon(this.unicode11Addon);
this.term.unicode.activeVersion = "11";
this.term.onResize((size) => {
console.log(size.rows, size.cols) });
// 将term挂载到dom元素上
const terminalContainer = document.getElementById("term");
this.term.open(terminalContainer);
this.fitAddon.fit()
this.term.focus()
this.term.write("hello, welcome to terminal!")
}
}
}
这样一个简单的终端terminal页面就渲染完成了,如果还需要和websocket交互,可以继续往下看
二、xterm和websocket通信并进行数据交互
1.websocket连接需要一个 ws协议的url作为参数,这个需要根据自己的业务做准备
2.与socket通信还需要 zmodem.js库的支持
看不懂的地方结合注释看,该有注释的都有注释
<script>
import {
Terminal } from "xterm";
import {
FitAddon } from "xterm-addon-fit";
import {
Unicode11Addon } from "xterm-addon-unicode11";
import "xterm/css/xterm.css";
// 安装这个库:npm install zmodem.js,因为这个库已经三年没更新了,所以也没有版本问题,直接安latest版本
// 因为这个库支持lrzsz命令,推荐使用
import Zmodem from "zmodem.js";
</script>
export default {
data() {
return{
// 定义一些初始化变量
term: null,
unicode11Addon: new Unicode11Addon(),
fitAddon: new FitAddon(),
// 定义socket连接相关
zsentry: Zmodem.Sentry,
zsession: null,
terminalSocket: null,
termLoading: false, // 我用来在连接时loading用,根据自己需求使用
}
},
methods: {
// 初始化term终端
initTerm() {
// 实例化term以及依赖包
this.term = new Terminal({
cursorBlink: true, // 光标闪烁
cursorStyle: "underline", // 光标闪烁样式
rendererType: "canvas", // 渲染类型
theme: {
background: "#1d242b", selection: "rgba(245, 108, 108, 0.5)" }, // 主题样式
fontFamily: 'Consolas, Menlo, Monaco, "Courier New", monospace',
scrollback: 10000,
})
this.term.loadAddon(this.fitAddon);
this.term.loadAddon(this.unicode11Addon);
this.term.unicode.activeVersion = "11";
this.term.onResize((size) => {
console.log(size.rows, size.cols) });
// 将term挂载到dom元素上
const terminalContainer = document.getElementById("term");
this.term.open(terminalContainer);
this.fitAddon.fit()
this.term.focus()
this.term.write("hello, welcome to terminal!")
<------------------websocket通信---------------------------->
this.zsentry = new Zmodem.Sentry({
// 这几个参数都为必要参数,是zmodem.js库源码示例中所必须的,每个参数都对应一个function
// 为了代码可读,把相关方法单独拎了出来
// 这几个内置函数在源码里都有,里面的逻辑有些小改动,可以对比查看
to_terminal: this._to_terminal, //发送的处理程序 到终端对象的流量。接收可迭代对象(例如,数组)包含八位字节数。
sender: this._sender, // 将流量发送到的处理程序对等方。例如,如果您的应用程序使用 WebSocket 进行通信到对等方,使用它将数据发送到 WebSocket 实例。
on_detect: this._on_detect, // 处理程序检测事件。接收新的检测对象。
on_retract: this._on_retract, // 于收回的处理程序事件。不接收任何输入。
})
this.initWsSocket();
this.term.onData((data) => {
this.terminalSocket.send(data);
});
},
// 初始化连接socket
initWsSocket() {
if(!this.terminalSocket) {
this.term.focus();
// 实例化socket
this.terminalSocket = new WebSocket("ws://10.22.33.444:5555/service/channel/123456");
// socket断开事件监听
this.terminalSocket.onclose = (e) => {
this.term.write("连接已断开!");
}
this.terminalSocket.binaryType = "arraybuffer";
// socket消息监听
this.terminalSocket.onmessage = (e) => {
try {
this.zsentry.consume(e.data);
} catch (error) {
this.zsession._on_session_end();
this.terminalSocket.send("\n");
}
}
}
},
// 以下这些都是内置方法,和源码对比有一些小改动
// 这个方法地址:https://github.com/FGasper/zmodemjs/blob/master/src/zmodem_browser.js
send_files(session, files, options) {
if (!options) options = {
};
//Populate the batch in reverse order to simplify sending
//the remaining files/bytes components.
var batch = [];
var total_size = 0;
for (var f=files.length - 1; f>=0; f--) {
var fobj = files[f];
total_size += fobj.size;
batch[f] = {
obj: fobj,
name: fobj.name,
size: fobj.size,
mtime: new Date(fobj.lastModified),
files_remaining: files.length - f,
bytes_remaining: total_size,
};
}
var file_idx = 0;
function promise_callback() {
var cur_b = batch[file_idx];
if (!cur_b) {
return Promise.resolve(); //batch done!
}
file_idx++;
return session.send_offer(cur_b).then( function after_send_offer(xfer) {
if (options.on_offer_response) {
options.on_offer_response(cur_b.obj, xfer);
}
if (xfer === undefined) {
return promise_callback(); //skipped
}
return new Promise( function(res) {
var reader = new FileReader();
//This really shouldn’t happen … so let’s
//blow up if it does.
reader.onerror = function reader_onerror(e) {
console.error("file read error", e);
throw("File read error: " + e);
};
var piece;
reader.onprogress = function reader_onprogress(e) {
//Some browsers (e.g., Chrome) give partial returns,
//while others (e.g., Firefox) don’t.
if (e.target.result) {
piece = new Uint8Array(e.target.result, xfer.get_offset())
// _check_aborted(session);
if(session.aborted()) {
throw new Zmodem.Error("aborted")
}
xfer.send(piece);
if (options.on_progress) {
options.on_progress(cur_b.obj, xfer, piece);
}
}
};
reader.onload = function reader_onload(e) {
piece = new Uint8Array(e.target.result, xfer, piece)
// _check_aborted(session);
if(session.aborted()) {
throw new Zmodem.Error("aborted")
}
xfer.end(piece).then( function() {
if (options.on_progress && piece.length) {
options.on_progress(cur_b.obj, xfer, piece);
}
if (options.on_file_complete) {
options.on_file_complete(cur_b.obj, xfer);
}
//Resolve the current file-send promise with
//another promise. That promise resolves immediately
//if we’re done, or with another file-send promise
//if there’s more to send.
res( promise_callback() );
} );
};
reader.readAsArrayBuffer(cur_b.obj);
} );
} );
}
return promise_callback();
},
on_detect(detection) {
//Do this if we determine that what looked like a ZMODEM session
//is actually not meant to be ZMODEM.
// if (no_good) {
// detection.deny();
// return;
//}
this.zsession = detection.confirm();
if (zsession.type === "send") {
//Send a group of files, e.g., from an <input>’s “.files”.
//There are events you can listen for here as well,
//e.g., to update a progress meter.
// Zmodem.Browser.send_files( zsession, files_obj );
}
else {
zsession.on("offer", (xfer) => {
//Do this if you don’t want the offered file.
//if (no_good) {
// xfer.skip();
// return;
//}
xfer.accept().then( () => {
//Now you need some mechanism to save the file.
//An example of how you can do this in a browser:
this._save_to_disk(
xfer.get_payloads(),
xfer.get_details().name
);
} );
});
zsession.start();
}
},
_save_to_disk(packets, name) {
var blob = new Blob(packets);
var url = URL.createObjectURL(blob);
var el = document.createElement("a");
el.style.display = "none";
el.href = url;
el.download = name;
document.body.appendChild(el);
//It seems like a security problem that this actually works;
//I’d think there would need to be some confirmation before
//a browser could save arbitrarily many bytes onto the disk.
//But, hey.
el.click();
document.body.removeChild(el);
},
_to_terminal(octets) {
// i.e. send to the ZMODEM peer
if(this.terminalSocket) {
this.terminalSocket.send(new Uint8Array(octets).buffer);
}
},
_on_retract() {
//for when Sentry retracts a Detection
console.log("retract")
}
}
}
到这里基本xterm和websocket通信就完成了,如果你还 需要基于zmodem协议的lrzsz命令功能,接着往下看
三、zmodem协议lrzsz命令上传下载功能
1.这里用的element-ui的组件
<template>
<---rz命令上传文件--->
<el-dialog
title="请选择要上传的文件"
:visible.sync="uploadDialogVisible"
width="400px"
:before-close="handleCloseUpload"
>
<el-upload
ref="upload"
action="http://localhost/posts/"
multiple
:auto-upload="false"
v-loading="uploadLoading"
>
<el-button slot="trigger" type="primary">选取文件</el-button>
<el-button type="primary" style="margin-left: 10px" @click="upload">上传</el-button>
</el-upload>
</el-dialog>
<---sz命令下载文件--->
<el-dialog
title="正在下载请稍后"
:visible.sync="downloadDialogVisible"
width="400px"
:before-close="handleCloseDownload"
>
<el-progress
:percentage="percentage"
:color="customColorMethod"
></el-progress>
</el-dialog>
</template>
2.相关变量和方法(注释都有标明)
看不懂的地方结合注释看,该有注释的都有注释
export default {
data() {
return{
// 定义一些初始化变量
term: null,
unicode11Addon: new Unicode11Addon(),
fitAddon: new FitAddon(),
// 定义socket连接相关
zsentry: Zmodem.Sentry,
zsession: null,
terminalSocket: null,
termLoading: false, // 我用来在连接时loading用,根据自己需求使用
// 上传下载需要的变量
uploadLoading: false,
uploadDialogVisible: false,
downloadDialogVisible: false,
percentage: 0,
}
},
methods: {
// rz命令上传文件
upload() {
let fileElem = document.getElementsByName("file")[0];
if(fileElem.files.length > 0) {
this.uploadLoading = true;
const _t = this;
// 这里就需要用到_send_files函数,函数在下面,里面的逻辑不用动
// 这个内置函数需要传三个参数,具体参数介绍在git里面有,不做赘述
// 第三个参数是一个object,包含三个回调函数,可以自己拎出来
_t._send_files(this.zsession, fileElem.files, {
// 上传响应
on_offer_response(obj, xfer) {
// 如果回调参数xfer为undefined,说明上传有问题
if(xfer) {
console.log(xfer)
} else {
_t.$notify.error({
title: "Error",
message: `${
obj.name} was upload skipped`,
});
}
},
// 上传进度回调
on_progress(obj, xfer) {
let detail = xfer.get_details();
let name = detail.name;
let total = detail.size;
let percent;
if(total === 0) {
percent = 100;
} else {
percent = Math.round((xfer.file_offset/total) * 100);
}
console.log(`${
percent}%`)
},
// 上传成功回调
on_file_complete(obj) {
_t.$notify.error({
title: "成功",
message: `${
obj.name} 上传成功`,
type: "success"
});
}
}).then(() => {
fileElem.value = "";
this.zsession.close();
this.uploadDialogVisible = false;
this.uploadLoading = false;
this.terminalSocket.send("\n");
this.term.focus();
});
}else {
this.$message({
type: "error",
message: "请选择文件",
});
this.uploadLoading = false;
}
let upload = this.$refs.upload;
upload.clearFiles();
},
// 上传文件弹框关闭
handleCloseUpload() {
if(this.uploadLoading) {
this.$message({
type: "error",
message: "上传中无法关闭",
});
}else {
this.zsession.close().then(() => {
let upload = this.$refs.upload;
upload.clearFiles();
})
}
},
// rzsz上传下载需要在这个内置函数中做一些改动
// 用来控制上传下载dialog弹框的显隐和upload方法的触发
on_detect(detection) {
//Do this if we determine that what looked like a ZMODEM session
//is actually not meant to be ZMODEM.
this.zsession = detection.confirm();
// 这里是监听上传事件
if (zsession.type === "send") {
//Send a group of files, e.g., from an <input>’s “.files”.
//There are events you can listen for here as well,
//e.g., to update a progress meter.
// Zmodem.Browser.send_files( zsession, files_obj );
// 打开上传dialog弹框
this.uploadDialogVisible = true;
}
// 这里监听下载事件
else {
zsession.on("offer", (xfer) => {
//Do this if you don’t want the offered file.
// 这里是做了一个进度的计算,可有可无
let total = xfer.get_details().bytes_remaining;
let length = 0;
this.downloadDialogVisible = true;
xfer.on("input", (octets) => {
length += octets.length;
this.percentage = Math.ceil((length * 100) / total);
});
// 这里往下是功能区
xfer.accept().then( () => {
//Now you need some mechanism to save the file.
//An example of how you can do this in a browser:'
// 这个下载函数也是内置源码有的,在下面有,可以直接用
this._save_to_disk(
xfer._spool,
xfer.get_details().name
);
});
});
// 监听到下载完毕,关闭下载弹框
this.zsession.on("session_end", () => {
this.percentage = 0;
this.downloadDialogVisible = false;
});
this.zsession.start();
}
},
/***--------------------分割线--------------------***/
// 初始化term终端
initTerm() {
// 实例化term以及依赖包
this.term = new Terminal({
cursorBlink: true, // 光标闪烁
cursorStyle: "underline", // 光标闪烁样式
rendererType: "canvas", // 渲染类型
theme: {
background: "#1d242b", selection: "rgba(245, 108, 108, 0.5)" }, // 主题样式
fontFamily: 'Consolas, Menlo, Monaco, "Courier New", monospace',
scrollback: 10000,
})
this.term.loadAddon(this.fitAddon);
this.term.loadAddon(this.unicode11Addon);
this.term.unicode.activeVersion = "11";
this.term.onResize((size) => {
console.log(size.rows, size.cols) });
// 将term挂载到dom元素上
const terminalContainer = document.getElementById("term");
this.term.open(terminalContainer);
this.fitAddon.fit()
this.term.focus()
this.term.write("hello, welcome to terminal!")
<------------------websocket通信---------------------------->
this.zsentry = new Zmodem.Sentry({
// 这几个参数都为必要参数,是zmodem.js库源码示例中所必须的,每个参数都对应一个function
// 为了代码可读,把相关方法单独拎了出来
// 这几个内置函数在源码里都有,里面的逻辑有些小改动,可以对比查看
to_terminal: this._to_terminal,
sender: this._sender,
on_detect: this._on_detect,
on_retract: this._on_retract,
})
this.initWsSocket();
this.term.onData((data) => {
this.terminalSocket.send(data);
});
},
// 初始化连接socket
initWsSocket() {
if(!this.terminalSocket) {
this.term.focus();
this.terminalSocket = new WebSocket("ws:/?10.22.33.444:5555/service/channel/123456");
this.terminalSocket.onclose = (e) => {
this.term.write("连接已断开!");
}
this.terminalSocket.binaryType = "arraybuffer";
this.terminalSocket.onmessage = (e) => {
try {
this.zsentry.consume(e.data);
} catch (error) {
this.zsession._on_session_end();
this.terminalSocket.send("\n");
}
}
}
},
// 以下这些都是内置方法,和源码对比有一些小改动
// 这个方法地址:https://github.com/FGasper/zmodemjs/blob/master/src/zmodem_browser.js
send_files(session, files, options) {
if (!options) options = {
};
//Populate the batch in reverse order to simplify sending
//the remaining files/bytes components.
var batch = [];
var total_size = 0;
for (var f=files.length - 1; f>=0; f--) {
var fobj = files[f];
total_size += fobj.size;
batch[f] = {
obj: fobj,
name: fobj.name,
size: fobj.size,
mtime: new Date(fobj.lastModified),
files_remaining: files.length - f,
bytes_remaining: total_size,
};
}
var file_idx = 0;
function promise_callback() {
var cur_b = batch[file_idx];
if (!cur_b) {
return Promise.resolve(); //batch done!
}
file_idx++;
return session.send_offer(cur_b).then( function after_send_offer(xfer) {
if (options.on_offer_response) {
options.on_offer_response(cur_b.obj, xfer);
}
if (xfer === undefined) {
return promise_callback(); //skipped
}
return new Promise( function(res) {
var reader = new FileReader();
//This really shouldn’t happen … so let’s
//blow up if it does.
reader.onerror = function reader_onerror(e) {
console.error("file read error", e);
throw("File read error: " + e);
};
var piece;
reader.onprogress = function reader_onprogress(e) {
//Some browsers (e.g., Chrome) give partial returns,
//while others (e.g., Firefox) don’t.
if (e.target.result) {
piece = new Uint8Array(e.target.result, xfer.get_offset())
// _check_aborted(session);
if(session.aborted()) {
throw new Zmodem.Error("aborted")
}
xfer.send(piece);
if (options.on_progress) {
options.on_progress(cur_b.obj, xfer, piece);
}
}
};
reader.onload = function reader_onload(e) {
piece = new Uint8Array(e.target.result, xfer, piece)
// _check_aborted(session);
if(session.aborted()) {
throw new Zmodem.Error("aborted")
}
xfer.end(piece).then( function() {
if (options.on_progress && piece.length) {
options.on_progress(cur_b.obj, xfer, piece);
}
if (options.on_file_complete) {
options.on_file_complete(cur_b.obj, xfer);
}
//Resolve the current file-send promise with
//another promise. That promise resolves immediately
//if we’re done, or with another file-send promise
//if there’s more to send.
res( promise_callback() );
} );
};
reader.readAsArrayBuffer(cur_b.obj);
} );
} );
}
return promise_callback();
},
// 这个函数在sz命令下载文件的时候用的到,也是源码写好的,可以直接用
_save_to_disk(packets, name) {
var blob = new Blob(packets);
var url = URL.createObjectURL(blob);
var el = document.createElement("a");
el.style.display = "none";
el.href = url;
el.download = name;
document.body.appendChild(el);
//It seems like a security problem that this actually works;
//I’d think there would need to be some confirmation before
//a browser could save arbitrarily many bytes onto the disk.
//But, hey.
el.click();
document.body.removeChild(el);
},
_to_terminal(octets) {
// i.e. send to the ZMODEM peer
if(this.terminalSocket) {
this.terminalSocket.send(new Uint8Array(octets).buffer);
}
},
_on_retract() {
//for when Sentry retracts a Detection
console.log("retract")
}
}
}
到这里,所有的流程就都结束了,代码除了实例化socket的url,其他都可以直接用,里面的逻辑都是完整的
由于权限限制,不能拷贝,代码都是一行行码的,所以里面可能会有一些小问题