用 Go + React 构建桌面 SSH 客户端 —— wsShell 开源项目实战教程
项目地址:https://github.com/Julyos-rgb/wsShell
技术栈:Wails v2 + Go + React + TypeScript + xterm.js + SQLite
适合人群:有 Go 或 React 基础,想学习桌面应用开发、SSH 协议实践、前后端桥接的同学
前言
大家好,今天给大家带来一个完整的桌面应用开源项目 —— wsShell。
在日常开发中,我们经常需要通过 SSH 连接服务器。市面上有 Xshell、SecureCRT、Termius 等工具,但你有没有想过:自己动手写一个 SSH 客户端,到底需要哪些技术?难度如何?
wsShell 就是为了回答这个问题而诞生的。它是一个基于 Wails v2 框架(Go 后端 + React 前端)构建的轻量级桌面 SSH 客户端,实现了:
- SSH 终端(多标签、256色、自适应窗口)
- SFTP 文件管理(双栏浏览、断点续传)
- VNC 远程桌面(SSH 隧道代理)
- 服务器管理(凭证加密存储、分组收藏)
通过这个项目,你将学到:
- 如何用 Wails v2 搭建 Go + React 桌面应用
- Go 语言如何实现 SSH 协议通信(golang.org/x/crypto/ssh)
- 前后端如何通过 Wails Bindings 和 Events 双向通信
- 如何用 AES-256-GCM 加密敏感数据
- SQLite 在桌面应用中的持久化方案
- xterm.js 在桌面环境中的集成技巧
- SFTP 文件传输与断点续传的实现
- 通过 SSH 隧道代理 VNC 连接的原理
一、项目架构总览
1.1 为什么选择 Wails v2?
在桌面开发领域,常见的跨平台方案有:
| 方案 | 语言 | 包大小 | 渲染引擎 |
|---|---|---|---|
| Electron | JavaScript | ~100MB+ | Chromium |
| Tauri | Rust + JS | ~5MB | 系统 WebView |
| Wails | Go + JS | ~8MB | 系统 WebView |
| Qt | C++ | ~20MB | 自绘 |
选择 Wails 的理由:
- Go 后端:天然适合网络编程,SSH 库成熟
- 系统 WebView:不打包 Chromium,体积小
- 前后端分离:React 前端 + Go 后端,各司其职
- 类型安全:Wails 自动生成 TypeScript 绑定
1.2 整体架构
┌──────────────────────────────────────────────────────┐
│ Desktop Application │
│ (Wails Runtime) │
├────────────────────┬─────────────────────────────────┤
│ Go Backend │ React Frontend (embedded) │
│ │ │
│ ┌──────────────┐ │ ┌────────────────────────────┐ │
│ │ SSHService │ │ │ Terminal (xterm.js) │ │
│ │ │◄─┼──│ │ │
│ └──────────────┘ │ └────────────────────────────┘ │
│ ┌──────────────┐ │ ┌────────────────────────────┐ │
│ │ SFTPManager │ │ │ FileManager (双栏浏览器) │ │
│ │ │◄─┼──│ │ │
│ └──────────────┘ │ └────────────────────────────┘ │
│ ┌──────────────┐ │ ┌────────────────────────────┐ │
│ │ VNC Proxy │ │ │ VncViewer (noVNC) │ │
│ │ │◄─┼──│ │ │
│ └──────────────┘ │ └────────────────────────────┘ │
│ ┌──────────────┐ │ ┌────────────────────────────┐ │
│ │ ConfigManager│ │ │ Sidebar (服务器列表) │ │
│ │ │◄─┼──│ │ │
│ └──────────────┘ │ └────────────────────────────┘ │
│ ┌──────────────┐ │ ┌────────────────────────────┐ │
│ │ Encryptor │ │ │ Zustand State │ │
│ │ (AES-256) │ │ │ (状态管理) │ │
│ └──────────────┘ │ └────────────────────────────┘ │
│ ┌──────────────┐ │ │
│ │ SQLite Store │ │ │
│ └──────────────┘ │ │
├────────────────────┴─────────────────────────────────┤
│ Wails Bindings (Request-Response) │
│ Wails Events (Server → Client Stream) │
└──────────────────────────────────────────────────────┘
1.3 通信模式
Wails 提供了两种前后端通信方式:
模式一:Bindings(请求-响应)
类似 HTTP 调用,前端直接调用 Go 方法,获取返回值。
// 前端调用
const result = await Connect({
host: "192.168.1.100",
port: 22,
username: "root",
password: "xxx",
authType: "password"
})
// result = { success: true, sessionId: "root@192.168.1.100:22" }
// Go 后端方法(自动暴露给前端)
func (s *SSHService) Connect(req ConnectRequest) (ConnectResponse, error) {
// ... SSH 连接逻辑
}
模式二:Events(服务端推送流)
Go 后端主动向前端推送数据,适合持续的数据流。
// Go 后端推送终端输出
runtime.EventsEmit(s.Ctx, "ssh:"+sessionID+":stdout", data)
// 前端订阅事件
EventsOn("ssh:root@192.168.1.100:22:stdout", (data) => {
xterm.write(data) // 写入终端
})
1.4 项目结构
wsShell/
├── main.go # 应用入口,Wails 配置
├── app.go # App 结构体,服务编排
├── wails.json # Wails 项目配置
│
├── internal/ # Go 后端(按功能模块划分)
│ ├── ssh/
│ │ └── ssh_service.go # SSH 连接与会话管理
│ ├── sftp/
│ │ └── sftp_manager.go # SFTP 文件传输
│ ├── config/
│ │ └── config_manager.go # 服务器配置 CRUD
│ ├── crypto/
│ │ └── encryptor.go # AES-256-GCM 加解密
│ ├── store/
│ │ ├── sqlite.go # 数据库初始化
│ │ ├── migrations.go # 建表迁移
│ │ └── server_repository.go # Repository 模式
│ └── vnc/
│ └── proxy.go # VNC WebSocket 代理
│
├── frontend/ # React 前端
│ ├── src/
│ │ ├── components/
│ │ │ ├── Terminal.tsx # xterm.js 终端组件
│ │ │ ├── FileManager.tsx # SFTP 双栏文件管理
│ │ │ ├── VncViewer.tsx # noVNC 远程桌面
│ │ │ ├── Sidebar.tsx # 服务器列表侧栏
│ │ │ ├── AddServerDialog.tsx # 添加/编辑服务器
│ │ │ └── StatusBar.tsx # 底部状态栏
│ │ ├── stores/
│ │ │ └── ui.ts # Zustand 状态管理
│ │ ├── types/
│ │ │ └── index.ts # TypeScript 类型定义
│ │ ├── App.tsx # 根组件
│ │ └── main.tsx # 入口
│ └── wailsjs/ # Wails 自动生成的绑定(勿手动编辑)
│
└── build/ # 构建资源(图标、manifest)
二、入口与启动流程
2.1 main.go —— 一切从这里开始
package main
import (
"embed"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
//go:embed all:frontend/dist
var assets embed.FS // 将前端构建产物嵌入到二进制文件中
func main() {
app := NewApp()
err := wails.Run(&options.App{
Title: "wsShell",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{
Assets: assets, // 前端资源
},
BackgroundColour: &options.RGBA{R: 26, G: 26, B: 46, A: 1},
OnStartup: app.startup,
Bind: []interface{}{ // 暴露给前端的 Go 对象
app,
app.sshService,
app.sftpManager,
app.configManager,
app.vncProxy,
},
})
if err != nil {
println("Error:", err.Error())
}
}
关键点解读:
//go:embed all:frontend/dist:Go 1.16 的嵌入指令,把 React 构建产物打包进二进制文件,运行时不需要额外的文件Bind: []interface{}{...}:注册要暴露给前端的 Go 对象。Wails 会为每个对象的公开方法自动生成 TypeScript 绑定OnStartup: app.startup:应用启动时的回调,在这里注入 Wails Context
2.2 app.go —— 服务编排中心
type App struct {
ctx context.Context
sshService *ssh.SSHService
sftpManager *sftp.SFTPManager
configManager *config.ConfigManager
vncProxy *vnc.Proxy
}
func NewApp() *App {
repo, err := store.NewServerRepository() // 初始化 SQLite
if err != nil {
log.Fatalf("Failed to initialize server repository: %v", err)
}
return &App{
sshService: ssh.NewSSHService(),
sftpManager: sftp.NewSFTPManager(),
configManager: config.NewConfigManager(repo),
vncProxy: vnc.NewProxy(),
}
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
a.sshService.Ctx = ctx // 注入 Context,用于 Events 事件推送
a.sftpManager.Ctx = ctx
a.vncProxy.SetSSHProvider(a.sshService) // VNC 复用 SSH 连接
}
设计思路:
App是所有服务的容器,负责依赖注入和生命周期管理context.Context是 Wails 事件系统的基础,必须在 startup 时注入- VNC Proxy 通过
SetSSHProvider获取 SSH 客户端引用,实现 SSH 隧道复用
三、SSH 终端 —— 核心模块详解
3.1 架构设计
SSH 终端是整个项目最核心的模块。核心挑战是:如何将 Go 的 SSH 会话流式输出桥接到前端的 xterm.js?
用户键盘输入 → xterm.js → Wails Binding → Go stdin pipe → 远程服务器
↓
远程服务器 → Go stdout goroutine → Wails Event → xterm.js 渲染
3.2 连接建立(Connect)
type SSHService struct {
mu sync.RWMutex // 读写锁,保护并发访问
sessions map[string]*sshSession // 会话映射:sessionID → sshSession
clients map[string]*sshcrypto.Client // 客户端映射:sessionID → Client
Ctx context.Context // Wails Context
}
type sshSession struct {
session *sshcrypto.Session
stdin chan string // 带缓冲的输入通道(容量256)
done chan struct{} // 退出信号
}
Connect 方法的核心流程:
func (s *SSHService) Connect(req ConnectRequest) (ConnectResponse, error) {
// 1. 配置 SSH 客户端
config := &sshcrypto.ClientConfig{
User: req.Username,
HostKeyCallback: sshcrypto.InsecureIgnoreHostKey(), // 注意:生产环境应验证 HostKey
Timeout: 10 * time.Second,
}
// 2. 选择认证方式
if req.AuthType == "key" && req.PrivateKey != "" {
signer, _ := sshcrypto.ParsePrivateKey([]byte(req.PrivateKey))
config.Auth = []sshcrypto.AuthMethod{sshcrypto.PublicKeys(signer)}
} else {
config.Auth = []sshcrypto.AuthMethod{sshcrypto.Password(req.Password)}
}
// 3. 建立 TCP 连接 → SSH 握手
client, err := sshcrypto.Dial("tcp", addr, config)
// 4. 创建 Session,请求 PTY(伪终端)
session, _ := client.NewSession()
session.RequestPty("xterm-256color", 40, 120, modes)
session.Shell() // 启动 shell
// 5. 获取 stdin/stdout/stderr 管道
stdinPipe, _ := session.StdinPipe()
stdoutPipe, _ := session.StdoutPipe()
stderrPipe, _ := session.StderrPipe()
// 6. 启动 goroutine 读取输出,通过 Events 推送到前端
go func() {
buf := make([]byte, 4096)
for {
n, err := stdoutPipe.Read(buf)
if n > 0 {
runtime.EventsEmit(s.Ctx, "ssh:"+sessionID+":stdout", string(buf[:n]))
}
if err != nil { close(ss.done); return }
}
}()
}
要点解析:
| 技术点 | 说明 |
|---|---|
sync.RWMutex |
读写锁。多个终端可同时读取,但增删会话需要写锁 |
session.RequestPty |
请求伪终端。xterm-256color 支持 256 色,40行×120列是初始大小 |
chan string (缓冲256) |
stdin 通道。缓冲防止前端快速输入时阻塞 |
runtime.EventsEmit |
Wails 事件推送。每个会话用不同的 event name 隔离数据 |
| Session ID 格式 | {username}@{host}:{port},如 root@192.168.1.100:22 |
3.3 前端终端组件
const TerminalInstance: React.FC<TerminalInstanceProps> = ({ tab, isActive }) => {
const terminalRef = useRef<HTMLDivElement>(null)
const xtermRef = useRef<XTermTerminal | null>(null)
useEffect(() => {
// 创建 xterm 实例
const term = new XTermTerminal({
theme: terminalTheme, // 自定义配色
fontFamily: 'Consolas, Monaco, "Courier New", monospace',
fontSize: 14,
lineHeight: 1.2,
cursorBlink: true,
scrollback: 10000, // 回滚缓冲区 10000 行
})
term.open(terminalRef.current)
// 监听用户输入 → 发送到 Go 后端
term.onData(async (data) => {
await WriteToSession({ sessionId: tab.sessionId, data })
})
xtermRef.current = term
}, [tab.id])
// 订阅后端推送的终端输出
useEffect(() => {
const stdoutHandler = (data: string) => { term.write(data) }
EventsOn(`ssh:${tab.sessionId}:stdout`, stdoutHandler)
EventsOn(`ssh:${tab.sessionId}:stderr`, stderrHandler)
return () => {
EventsOff(`ssh:${tab.sessionId}:stdout`)
EventsOff(`ssh:${tab.sessionId}:stderr`)
}
}, [tab.sessionId])
// 窗口大小变化 → 通知后端调整 PTY 大小
useEffect(() => {
const resizeObserver = new ResizeObserver(() => {
fitAddon.fit()
ResizeTerminal({ sessionId: tab.sessionId, rows: term.rows, cols: term.cols })
})
resizeObserver.observe(terminalRef.current)
}, [isActive])
}
数据流图:
┌───────────────────────────────────────────────────┐
│ React Frontend │
│ │
│ 用户输入 ──→ term.onData() ──→ WriteToSession() │
│ │ │
│ Wails Binding │
│ ↓ │
│ term.write() ←── EventsOn() ←── Go 后端 │
│ │ ↑ │
│ ↓ stdin.Write(data) │
│ xterm 渲染 │ │
│ SSH Channel → 远程服务器 │
└───────────────────────────────────────────────────┘
3.4 多标签管理
wsShell 支持同一服务器打开多个终端标签。核心思路是复用 SSH 连接,创建新 Session:
func (s *SSHService) CreateShell(req CreateShellRequest) (CreateShellResponse, error) {
// 复用已有的 SSH Client,创建新的 Session
client := s.resolveClient(req.BaseSessionID)
session, _ := client.NewSession() // 一个 Client 可以创建多个 Session
session.RequestPty("xterm-256color", 40, 120, modes)
session.Shell()
// 新标签的 ID 格式:root@host:22#2
sessionID := fmt.Sprintf("%s#%d", req.BaseSessionID, len(s.sessions)+1)
// 同样启动 goroutine 读取输出...
}
前端使用 Zustand 管理标签状态:
interface TerminalTabState {
terminalTabs: TerminalTab[]
activeTerminalTabId: string | null
addTerminalTab: (tab: TerminalTab) => void
removeTerminalTab: (tabId: string) => void
}
四、SFTP 文件管理
4.1 设计思路
SFTP 模块的核心是复用 SSH 连接来打开 SFTP 子系统:
func (m *SFTPManager) Connect(req ConnectRequest) (ConnectResponse, error) {
// 1. 建立 SSH 连接
sshClient, _ := sshcrypto.Dial("tcp", addr, config)
// 2. 在 SSH 连接上打开 SFTP 子系统
sftpClient, _ := sftp.NewClient(sshClient)
// 3. 存储映射关系
m.clients[sessionID] = sftpClient
m.sshClients[sessionID] = sshClient
}
4.2 文件列表(ListFiles)
func (m *SFTPManager) ListFiles(req ListFilesRequest) (ListFilesResponse, error) {
client := m.clients[req.SessionID]
entries, _ := client.ReadDir(path) // 读取远程目录
files := []FileInfo{}
for _, entry := range entries {
files = append(files, FileInfo{
Name: entry.Name(),
Size: entry.Size(),
Type: fileType, // "file" 或 "directory"
Path: fullPath,
ModTime: entry.ModTime().Format("2006-01-02 15:04:05"),
})
}
return ListFilesResponse{Success: true, Files: files}, nil
}
Go 的时间格式化
"2006-01-02 15:04:05"是 Go 的独特设计——用参考时间来定义格式。
4.3 文件上传与进度推送
func (m *SFTPManager) UploadFile(req UploadRequest) (UploadResponse, error) {
localFile, _ := os.Open(req.LocalPath)
remoteFile, _ := client.Create(req.RemotePath)
buf := make([]byte, 32768) // 32KB 缓冲区
var written int64
total := stat.Size()
for {
nr, readErr := localFile.Read(buf)
if nr > 0 {
nw, _ := remoteFile.Write(buf[:nr])
written += int64(nw)
// 推送上传进度到前端
if m.Ctx != nil && total > 0 {
progress := float64(written) / float64(total) * 100
runtime.EventsEmit(m.Ctx, "sftp:upload:progress", map[string]interface{}{
"progress": progress,
"written": written,
"total": total,
})
}
}
if readErr == io.EOF { break }
}
}
4.4 断点续传
断点续传是 wsShell 的亮点功能之一。核心原理:
已传输 50MB / 总 100MB → 连接断开 → 检测远程文件已有 50MB → 从 50MB 处继续
func (m *SFTPManager) ResumeUpload(req ResumeUploadRequest) (ResumeUploadResponse, error) {
// 获取远程已存在的文件大小
remoteStat, _ := client.Stat(req.RemotePath)
offset := remoteStat.Size()
// 本地文件 Seek 到 offset 位置
localFile.Seek(offset, io.SeekStart)
// 远程文件以追加模式打开
remoteFile, _ := client.OpenFile(req.RemotePath, os.O_WRONLY|os.O_APPEND)
// 从 offset 开始传输...
}
五、凭证加密存储
5.1 安全架构
用户密码/私钥
↓ AES-256-GCM 加密
密文存储到 SQLite
↑ AES-256-GCM 解密
读取时还原明文
↑
Master Key(32字节)
↓ 存储于
Windows: %LOCALAPPDATA%/wsShell/master.key
macOS/Linux: ~/.wsShell/keys/master.key
5.2 AES-256-GCM 实现
func Encrypt(plaintext string) (string, error) {
block, _ := aes.NewCipher(masterKey) // AES-256,需要 32 字节密钥
aesGCM, _ := cipher.NewGCM(block) // GCM 模式提供认证加密
nonce := make([]byte, aesGCM.NonceSize()) // 12 字节随机 nonce
io.ReadFull(rand.Reader, nonce)
// Seal 将 nonce 前置到密文中:nonce + ciphertext + tag
ciphertext := aesGCM.Seal(nonce, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
func Decrypt(encoded string) (string, error) {
ciphertext, _ := base64.StdEncoding.DecodeString(encoded)
block, _ := aes.NewCipher(masterKey)
aesGCM, _ := cipher.NewGCM(block)
nonceSize := aesGCM.NonceSize()
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, _ := aesGCM.Open(nil, nonce, ciphertext, nil)
return string(plaintext), nil
}
为什么选择 AES-GCM 而不是 AES-CBC?
| 特性 | AES-CBC | AES-GCM |
|---|---|---|
| 认证 | 需要额外 HMAC | 内置认证标签 |
| 填充 | 需要 PKCS#7 填充 | 无需填充 |
| 安全性 | 容易被 padding oracle 攻击 | 不存在此问题 |
| Go 标准库 | cipher.NewCBCEncrypter |
cipher.NewGCM |
六、SQLite 持久化
6.1 数据库初始化
wsShell 使用 modernc.org/sqlite(纯 Go 实现的 SQLite),不需要 CGO:
func GetDB() (*sql.DB, error) {
once.Do(func() { // 单例模式,只初始化一次
homeDir, _ := os.UserHomeDir()
configDir := filepath.Join(homeDir, ".wsShell")
os.MkdirAll(configDir, 0755)
dbPath := filepath.Join(configDir, "wsShell.db")
db, dbErr = sql.Open("sqlite", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
db.SetMaxOpenConns(1) // SQLite 单写特性
dbErr = runMigrations(db)
})
return db, nil
}
关键配置:
_journal_mode=WAL:Write-Ahead Logging,允许读写并发_busy_timeout=5000:写冲突时等待 5 秒而非直接报错SetMaxOpenConns(1):SQLite 不支持多连接并发写
6.2 Repository 模式
type ServerRepository interface {
GetAll() ([]ServerRow, error)
GetByID(id string) (*ServerRow, error)
Save(s ServerRow) error
Update(s ServerRow) error
Delete(id string) error
ToggleFavorite(id string) error
}
使用接口而非直接实现,好处是:
- 业务层(ConfigManager)不依赖具体数据库
- 方便单元测试(可以 mock Repository)
- 未来如果切换数据库,只需实现新 Repository
6.3 数据库迁移
func runMigrations(db *sql.DB) error {
migrations := []string{
`CREATE TABLE IF NOT EXISTS servers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
host TEXT NOT NULL,
port INTEGER DEFAULT 22,
password TEXT DEFAULT '', -- 加密存储
private_key TEXT DEFAULT '', -- 加密存储
favorite INTEGER DEFAULT 0,
tags TEXT DEFAULT '[]', -- JSON 数组
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE TABLE IF NOT EXISTS groups (...)`,
`CREATE TABLE IF NOT EXISTS host_keys (...)`,
}
for _, m := range migrations {
db.Exec(m)
}
}
七、VNC 远程桌面代理
7.1 设计思路
VNC 连接的安全性是个问题——VNC 协议本身不加密。wsShell 的解决方案是 SSH 隧道代理:
noVNC (前端) → WebSocket → Go Proxy → SSH 隧道 → 远程 VNC 服务器
7.2 WebSocket 代理实现
func (p *Proxy) StartProxy(req StartProxyRequest) (StartProxyResponse, error) {
// 1. 在本地随机端口启动 HTTP 服务器
listener, _ := net.Listen("tcp", "127.0.0.1:0")
localPort := listener.Addr().(*net.TCPAddr).Port
// 2. 注册 WebSocket 路由
mux := http.NewServeMux()
mux.HandleFunc("/websockify", func(w http.ResponseWriter, r *http.Request) {
ps.handleWebSocket(w, r)
})
// 3. 启动服务
go srv.Serve(listener)
// 4. 返回 WebSocket URL 给前端
wsURL := fmt.Sprintf("ws://127.0.0.1:%d/websockify", localPort)
}
7.3 WebSocket ↔ TCP 双向转发
func (ps *proxyServer) handleWebSocket(w http.ResponseWriter, r *http.Request) {
wsConn, _ := upgrader.Upgrade(w, r, nil) // 升级为 WebSocket
// 通过 SSH 隧道连接远程 VNC
conn, _ := ps.sshClient.Dial("tcp", ps.target)
// 双向数据转发
go func() {
// TCP → WebSocket
buf := make([]byte, 32*1024)
for {
n, _ := conn.Read(buf)
wsConn.WriteMessage(websocket.BinaryMessage, buf[:n])
}
}()
go func() {
// WebSocket → TCP
for {
_, reader, _ := wsConn.NextReader()
io.Copy(conn, reader)
}
}()
}
为什么要用 Go 做代理,而不是直接让前端连 VNC?
- 安全性:VNC 流量通过 SSH 加密
- 跨域限制:浏览器 WebSocket 不能直连远程 TCP
- 灵活性:Go 可以控制连接生命周期、添加认证
八、前端状态管理
8.1 Zustand Store 设计
wsShell 使用 Zustand 管理前端状态,分为四个独立的 Store:
// UI 状态(当前标签、主题、对话框)
const useUIStore = create<UIState>((set) => ({
activeTab: 'terminal',
activeServerId: null,
showAddServerDialog: false,
}))
// 连接状态(服务器列表、SSH/SFTP 会话映射)
const useConnectionStore = create<ConnectionState>((set, get) => ({
servers: [],
connections: new Map<string, ConnectionInfo>(),
sftpSessions: new Map<string, string>(),
}))
// 文件传输状态(上传/下载任务列表、进度)
const useTransferStore = create<TransferState>((set) => ({
transfers: [],
}))
// 终端标签状态(多标签管理)
const useTerminalTabStore = create<TerminalTabState>((set, get) => ({
terminalTabs: [],
activeTerminalTabId: null,
}))
为什么选择 Zustand 而不是 Redux?
| 特性 | Redux | Zustand |
|---|---|---|
| 模板代码 | 多(action、reducer、dispatch) | 少(直接 set) |
| 包大小 | ~7KB | ~1KB |
| 异步处理 | 需要 redux-thunk/saga | 原生支持 |
| 学习曲线 | 陡峭 | 平缓 |
九、开发与构建
9.1 环境搭建
# 1. 安装 Go(1.18+)
go version
# 2. 安装 Node.js(16+)
node --version
# 3. 安装 Wails CLI
go install github.com/wailsapp/wails/v2/cmd/wails@latest
wails doctor # 检查环境
9.2 开发模式
# 克隆项目
git clone https://github.com/Julyos-rgb/wsShell.git
cd wsShell
# 安装前端依赖
cd frontend && npm install && cd ..
# 启动开发模式(支持热重载)
wails dev
wails dev 会同时启动:
- Go 后端(热重载)
- Vite 前端开发服务器(HMR)
- Wails DevTools(调试窗口)
9.3 构建发布
wails build
构建产物在 build/bin/ 目录,是一个独立的可执行文件,无需安装运行时。
十、项目亮点与设计模式总结
10.1 设计模式一览
| 模式 | 应用场景 | 代码位置 |
|---|---|---|
| 单例模式 | SQLite 数据库连接(sync.Once) |
store/sqlite.go |
| Repository 模式 | 数据访问层抽象 | store/server_repository.go |
| 依赖注入 | VNC Proxy 注入 SSH Provider | app.go |
| 观察者模式 | Wails Events 事件订阅 | 全项目 |
| 并发安全 | sync.RWMutex 保护共享状态 |
SSH、SFTP、VNC |
| Goroutine + Channel | SSH stdin 非阻塞写入 | ssh/ssh_service.go |
10.2 值得学习的技术决策
-
modernc.org/sqlite而非mattn/go-sqlite3- 纯 Go 实现,不需要 CGO,交叉编译更简单
- 缺点是性能略低,但对于桌面应用完全够用
-
Session ID 设计:
user@host:port- 直观可读
- 天然唯一(同一用户不会重复连接同一服务器)
- 多标签通过
#2后缀区分
-
32KB 缓冲区
- SFTP 传输使用 32KB 缓冲区
- 平衡了内存使用和传输效率
- 同时避免大缓冲区导致的进度更新不细腻
-
前端不存储明文密码
- 密码仅在需要连接时从加密存储中解密
- Zustand Store 中只保存连接信息,不保存凭证
十一、扩展方向
如果你想基于 wsShell 继续开发,这里有一些建议:
入门级
- 添加主题切换(深色/浅色)
- 支持服务器搜索/过滤
- 添加终端快捷键(复制粘贴、清屏)
- 支持导出/导入服务器配置
进阶级
- 实现 HostKey 指纹验证(替代
InsecureIgnoreHostKey) - 添加 SFTP 文件预览(图片、文本)
- 支持批量文件操作
- 添加 SSH 跳板机(ProxyJump)支持
高级
- 多窗口支持(独立终端窗口)
- 录制/回放终端操作
- 实现 Docker 容器管理面板
- 添加插件系统
结语
wsShell 虽然是一个轻量级项目,但涵盖了很多实战技术点:
- Go 网络编程:SSH、SFTP、WebSocket、TCP
- Go 并发编程:goroutine、channel、sync.RWMutex、sync.Once
- 桌面应用开发:Wails 框架、前后端桥接
- 前端工程:React、TypeScript、Zustand、xterm.js
- 安全实践:AES-256-GCM 加密、凭证保护
- 数据持久化:SQLite、Repository 模式、数据库迁移
希望这个项目和学习笔记对你有帮助!如果觉得不错,欢迎 Star ⭐ 支持一下。
项目地址:https://github.com/Julyos-rgb/wsShell
有任何问题欢迎在 GitHub Issues 中讨论交流!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)