vue+xterm.js结合websocket通信+zmodem协议lrzsz上传下载

一、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,其他都可以直接用,里面的逻辑都是完整的
由于权限限制,不能拷贝,代码都是一行行码的,所以里面可能会有一些小问题

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_45717984/article/details/130004395