Electron + ssh2 + xterm 实现一个简易的ssh客户端
先放一张效果图
在当今的开发环境中,SSH 客户端工具是开发人员日常工作中的必备工具之一。通过 SSH(Secure Shell),我们可以安全地远程登录服务器,执行命令,并实时查看结果。为了提升应用的可定制性和用户体验,结合桌面应用技术,我们可以使用 Electron 、SSH2 和 Xterm.js Vue 来构建一个简易的 SSH 客户端。
本文将带你一步步了解如何实现这一应用,包括三部分核心代码:
-
SSH 连接服务的实现
-
前端终端界面的设计
-
Electron 主进程的创建
通过这些模块的协同工作,我们将实现一个可以与远程服务器交互的桌面应用程序。
1. 实现 SSH 连接服务 (sshService.js
)
在 SSH 客户端工具中,核心功能之一是通过 SSH 协议与远程服务器建立连接,并允许用户发送指令,查看返回结果。为了实现这一功能,我们使用了 ssh2
库,该库提供了简单且强大的 API 来与 SSH 协议交互。 以下是我们的 SSHService
类,它用于处理连接的逻辑:
javascript
const { Client } = require('ssh2');
export class SSHService {
constructor() {
this.client = new Client();
this.stream = null;
this.isConnected = false; // 添加连接状态
}
connect(config, onData, onError, onClose) {
return new Promise((resolve, reject) => {
this.client.on('ready', () => {
this.isConnected = true; // 更新连接状态
this.client.shell((err, stream) => {
if (err) {
onError(err);
return reject(err);
}
this.stream = stream;
stream.on('data', onData);
stream.on('close', () => {
this.isConnected = false; // 更新连接状态
onClose();
this.client.end();
});
stream.stderr.on('data', onData);
resolve();
});
}).on('error', (err) => {
this.isConnected = false; // 更新连接状态
onError(err);
reject(err);
}).connect(config);
});
}
sendCommand(command) {
if (this.stream) {
this.stream.write(command + '\n');
}
}
disconnect() {
if (this.isConnected) {
this.isConnected = false; // 更新连接状态
this.client.end();
}
}
}
在这个类中,我们定义了三大方法:
connect
:负责建立 SSH 连接,并启动与服务器的交互,处理连接中的数据、错误和关闭事件。sendCommand
:通过 SSH 流发送指令到远程服务器。disconnect
:断开与服务器的连接,确保资源的及时释放。
connect
方法返回的是一个 Promise
,因此它的使用更适合现代的异步编程方式,使我们能够更好地处理连接成功或失败的情况。
2. 构建前端终端界面 (Terminal.vue
)
在前端部分,我们使用 Vue 3 和 xterm.js
来创建一个交互式的终端界面。xterm.js
是一个非常流行的开源库,专门用于在网页中模拟终端。它提供了丰富的功能和良好的扩展性,使得终端模拟变得非常简单。
以下是 Terminal.vue
组件的代码,它展示了如何通过一个表单让用户输入 SSH 连接信息,并在终端中实时显示远程服务器的输出:
html
<template>
<div class="term-page">
<el-form :model="ruleForm" @submit.native.prevent="setupSSHConnection()">
<el-form-item label="主机:" prop="host">
<el-input v-model="ruleForm.host" placeholder="请输入主机"></el-input>
</el-form-item>
<el-form-item label="用户名:" prop="username">
<el-input v-model="ruleForm.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item label="密码:" prop="password">
<el-input v-model="ruleForm.password" type="password" placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item label="端口:" prop="port">
<el-input v-model="ruleForm.port" placeholder="请输入端口"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" native-type="submit">创建连接</el-button>
<el-button type="primary" @click="disConnectSSH" :disabled="!connected">断开连接</el-button>
</el-form-item>
</el-form>
<div id="terminal-container">
<div id="terminal"></div>
</div>
</div>
</template>
<script>
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';
import { SSHService } from '../services/sshService';
export default {
data() {
return {
terminal: null,
sshService: null,
connected: false,
ruleForm: {
host: '192.168.0.140',
username: 'root',
password: '',
port: '22'
},
};
},
mounted() {
this.setupTerminal();
},
methods: {
// 初始化 xterm 终端
setupTerminal() {
const fitAddon = new FitAddon();
this.terminal = new Terminal({
cursorBlink: true,
fontSize: 14,
});
this.terminal.loadAddon(fitAddon);
this.terminal.open(document.getElementById('terminal'));
fitAddon.fit();
},
// 设置 SSH 连接
setupSSHConnection() {
const config = {
host: this.ruleForm.host,
port: this.ruleForm.port,
username: this.ruleForm.username,
password: this.ruleForm.password,
};
this.sshService = new SSHService();
// 建立连接并处理数据
this.sshService.connect(
config,
(data) => this.terminal.write(data),
(error) => this.terminal.write(`\r\nError: ${error.message}`),
() => this.terminal.write('\r\nConnection closed.')
).then(() => {
this.connected = this.sshService.isConnected;
});
},
// 断开 SSH 连接
disConnectSSH() {
if (this.sshService) {
this.sshService.disconnect();
this.connected = false;
}
},
}
};
</script>
在前端部分的代码中,我们做了以下几件事情:
- 使用
xterm.js
初始化终端界面,并加载FitAddon
以适应终端窗口大小。 - 通过表单让用户输入 SSH 连接的必要信息(主机、用户名、密码和端口)。
- 使用
SSHService
类中的connect
方法建立 SSH 连接,并通过xterm
将远程服务器的输出实时显示在终端中。 - 提供"断开连接"的按钮以便用户随时结束会话。
3. 创建 Electron 主进程 (main.js
)
Electron 作为一个跨平台的桌面应用框架,允许我们使用 Web 技术(HTML、CSS、JavaScript)来构建桌面应用。在 main.js
文件中,我们定义了应用的主进程逻辑,主要负责创建窗口并加载前端的 HTML 文件。
javascript
const { app, BrowserWindow, systemPreferences, Menu, dialog, globalShortcut } = require('electron')
const path = require('path')
const fs = require('fs')
import setIpc from './ipcMain.js'
import menuconfig from './menu'
try {
// 你的主进程代码
const getBaseUrl = function () {
let configFilePath = ''
if (process.env.NODE_ENV === 'development') {
configFilePath = process.cwd() + '/extraResources/configSetting.json'
} else {
// const appPath = app.isPackaged ? app.getAppPath() : app.getPath('exe')
configFilePath = path.join(process.resourcesPath, 'extraResources/configSetting.json')
}
const configData = JSON.parse(fs.readFileSync(configFilePath, 'utf-8'))
return configData
}
function reloadWindow() {
app.relaunch()
app.exit(0)
}
const LocalPath = getBaseUrl().path
process.env.DIST = path.join(__dirname, '../dist')
process.env.PUBLIC = app.isPackaged ? process.env.DIST : path.join(process.env.DIST, '../public')
let mainWindow
const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
setIpc.setDefaultIpcMain()
function createWindow() {
mainWindow = new BrowserWindow({
width: 1480,
height: 780,
icon: path.join(process.env.PUBLIC, 'vite.svg'),
webPreferences: {
plugins: true,
contextIsolation: false,
nodeIntegration: true,
webSecurity: false,
enableRemoteModule: true,
// 如果是开发模式可以使用devTools
devTools: true,
// preload: path.join(__dirname, 'preload.js'),
},
})
// 载入菜单
const menu = Menu.buildFromTemplate(menuconfig)
Menu.setApplicationMenu(menu)
mainWindow.webContents.on('render-process-gone', (e, details) => {
const options = {
type: 'error',
title: '进程崩溃了',
message: '这个进程已经崩溃.',
buttons: ['重载', '退出'],
}
dialog
.showMessageBox(options)
.then(({ response }) => {
console.log(response)
if (response === 0) reloadWindow()
else app.quit()
})
.catch((e) => {
console.log('err', e)
})
})
}
app.on('window-all-closed', () => {
mainWindow = null
app.quit()
})
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
//允许私有证书
event.preventDefault()
callback(true)
})
app.whenReady().then(() => {
// 注册全局快捷键
globalShortcut.register('CommandOrControl+=', () => {
mainWindow.webContents.send('zoomIn')
})
globalShortcut.register('CommandOrControl+-', () => {
mainWindow.webContents.send('zoomOut')
})
createWindow()
})
} catch (error) {
console.error('Uncaught Exception:', error)
}
在这部分代码中:
createWindow
函数用于创建 Electron 窗口,并加载我们的前端界面。- 根据环境变量判断是加载开发模式下的服务器还是生产模式下的打包文件。
- 当所有窗口被关闭时,应用程序会自动退出。
4. 增加错误处理与用户体验优化
为了进一步提升用户体验,我们可以增加一些改进:
- 连接状态提示:当用户点击"创建连接"后,可以在界面上显示连接进度(例如:正在连接中......),以便用户知道程序正在处理。
- 命令历史:为用户提供输入命令的历史记录,以便快速重复执行命令。
- 自动重连:当连接意外断开时,可以尝试自动重连。
总结
通过本文,我们展示了如何使用 Electron、SSH2 和 Xterm.js 构建一个简易的 SSH 客户端应用。整个流程涵盖了从后端的 SSH 连接处理,到前端的终端界面设计,以及桌面应用的创建。虽然这个项目是一个简易版的 SSH 客户端,但你可以根据实际需求进行进一步扩展,例如添加更多高级功能(如文件传输、会话管理等)。
如果你对项目有任何疑问或需要更多的扩展建议,欢迎与你探讨!