Wails开发详尽教程
原理、架构与设计思想深度解析
info 1. Wails简介和概述
Wails是一个现代化的桌面应用开发框架,允许开发者使用Go语言和Web技术(HTML、CSS、JavaScript)来构建跨平台的桌面应用程序。它被设计为Go语言的快速且轻量的Electron替代方案。
核心特点
- 原生性能:Wails不嵌入浏览器,而是利用各平台的原生渲染引擎(Windows上的WebView2,macOS上的WebKit,Linux上的WebKitGTK),从而提供更小的应用体积和更好的性能。
- Go后端:使用Go语言的强大功能处理业务逻辑、系统调用和性能密集型任务。
- Web前端:可以使用任何熟悉的前端技术(React、Vue、Svelte、Angular等)构建用户界面。
- 跨平台支持:支持Windows、macOS和Linux三大主流操作系统。
- 原生UI元素:提供原生菜单、对话框、主题和半透明窗口等原生UI元素。
- 轻量级:相比Electron,Wails应用程序体积更小,内存占用更低。
适用场景
Wails特别适合以下场景:
- 需要构建轻量级桌面应用的Go开发者
- 希望利用现有Web技术栈构建桌面应用的前端开发者
- 对应用体积和性能有较高要求的项目
- 需要与操作系统底层功能深度交互的应用
architecture 2. Wails的架构设计
Wails采用了一种独特的架构设计,将Go后端与Web前端无缝集成,同时保持各自的优势。其架构主要由以下几个核心组件组成:
核心组件详解
前端层 (Frontend)
前端层使用标准的Web技术构建用户界面,支持多种前端框架:
- React:用于构建复杂交互界面的流行库
- Vue:渐进式JavaScript框架,易于学习和使用
- Svelte:编译时框架,生成高效的原生JavaScript代码
- Angular:完整的前端框架,适合大型企业应用
- Vanilla JS:纯JavaScript实现,适合轻量级应用
桥接层 (Bridge Layer)
桥接层是Wails架构的核心,负责前端JavaScript与后端Go代码之间的通信:
- 上下文 (Context):提供应用状态管理和生命周期控制
- 事件系统 (Events):支持Go和JavaScript之间的双向事件通信
- 方法调用 (Methods):允许前端直接调用后端Go方法
- 数据绑定 (Data Binding):自动将Go结构体转换为TypeScript定义
后端层 (Backend)
后端层使用Go语言实现,负责处理业务逻辑和系统交互:
- 应用逻辑 (App Logic):开发者编写的业务代码
- 运行时 (Runtime):提供窗口管理、菜单、对话框等原生功能
- 上下文管理 (Context):管理应用状态和资源
原生渲染引擎 (Native Rendering Engine)
Wails不嵌入浏览器,而是使用各平台的原生渲染引擎:
- Windows:使用Microsoft WebView2(基于Chromium)
- macOS:使用WebKit(Safari的渲染引擎)
- Linux:使用WebKitGTK
这种设计使得Wails应用体积更小,性能更好,同时能够提供与原生应用一致的用户体验。
settings 3. Wails的工作原理
Wails的工作原理可以从应用启动、前后端通信、以及资源处理三个方面来理解。
应用启动流程
- 用户启动Wails应用
- 操作系统加载原生二进制文件
- Go后端初始化,创建应用上下文
- 启动原生WebView组件
- 加载前端资源(HTML、CSS、JS)
- 前端JavaScript初始化,建立与Go后端的连接
- 应用完全启动,用户可以交互
前后端通信机制
Wails采用了一种高效的桥接机制,实现前端JavaScript与后端Go代码之间的通信:
方法调用
Wails自动将导出的Go方法暴露给前端JavaScript,使得前端可以直接调用这些方法:
// Go后端代码 type App struct { runtime *wails.Runtime } func (a *App) Greet(name string) string { return fmt.Sprintf("Hello, %s!", name) }
// 前端JavaScript代码 async function sayHello() { const result = await window.backend.App.Greet("World"); console.log(result); // 输出: Hello, World! }
事件系统
Wails提供了统一的事件系统,支持Go和JavaScript之间的双向事件通信:
// Go后端发送事件 func (a *App) SendNotification() { a.runtime.Events.Emit("notification", "New message received") }
// 前端JavaScript监听事件 window.runtime.EventsOn("notification", (message) => { console.log("Notification:", message); // 显示通知 });
数据绑定
Wails能够自动将Go结构体转换为TypeScript定义,确保前后端数据类型的一致性:
// Go结构体 type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` IsActive bool `json:"isActive"` }
// 自动生成的TypeScript定义 interface User { id: number; name: string; email: string; isActive: boolean; }
资源处理机制
Wails提供了灵活的资源处理机制,支持开发模式和生产模式的不同需求:
开发模式
在开发模式下,Wails会:
- 启动一个开发服务器,实时监控文件变化
- 自动重新编译Go代码并重启应用
- 自动刷新前端资源,无需手动刷新
wails dev
生产模式
在生产模式下,Wails会:
- 将前端资源打包到二进制文件中
- 优化应用性能和体积
- 生成可用于分发的安装包
wails build
compare 4. Wails与Electron的对比
Wails经常被比作Go版本的Electron,但两者在架构、性能和资源消耗等方面有显著差异。
架构对比
特性 | Wails | Electron |
---|---|---|
后端语言 | Go | Node.js |
前端技术 | 任何Web技术 | 任何Web技术 |
渲染引擎 | 系统原生WebView | 嵌入式Chromium |
应用结构 | 单一二进制文件 | 主进程 + 多个渲染进程 |
进程模型 | 单进程 | 多进程 |
性能对比
指标 | Wails | Electron |
---|---|---|
启动速度 | 快 | 较慢 |
内存占用 | 低(通常5-20MB) | 高(通常50-100MB+) |
CPU使用率 | 低 | 中等 |
应用体积 | 小(通常5-15MB) | 大(通常50-100MB+) |
开发体验对比
方面 | Wails | Electron |
---|---|---|
学习曲线 | 需要Go知识 | 需要Node.js知识 |
调试工具 | 标准Go调试工具 | 丰富的前端调试工具 |
生态系统 | Go生态系统 + Web生态系统 | Node.js生态系统 + Web生态系统 |
原生功能 | 直接通过Go调用 | 通过Node.js原生模块或C++插件 |
适用场景对比
Wails更适合:
- 对应用体积和性能有较高要求的项目
- Go开发者构建桌面应用
- 需要与系统底层深度交互的应用
- 资源受限的环境
Electron更适合:
- 需要快速开发和迭代的项目
- 复杂的多窗口应用
- 需要丰富前端调试工具的项目
- 团队主要是前端开发者
build 5. Wails的开发环境搭建
要开始使用Wails开发桌面应用,需要先搭建好开发环境。以下是详细的步骤:
系统要求
- 操作系统:Windows 10/11, macOS 10.15+, Linux
- Go:版本1.21或更高(macOS 15+需要Go 1.23.3+)
- Node.js:版本15或更高(包含NPM)
安装Go
- 访问Go官方下载页面
- 下载适合你操作系统的Go安装包
- 按照官方安装说明进行安装
- 验证安装:
go version
5. 确保Go的bin目录已添加到PATH环境变量:
echo $PATH | grep go/bin
安装Node.js和NPM
- 访问Node.js官方下载页面
- 下载适合你操作系统的Node.js安装包(LTS版本推荐)
- 按照安装向导完成安装
- 验证安装:
node --version npm --version
安装Wails CLI
Wails CLI是Wails的命令行工具,用于创建、构建和管理Wails项目。
- 使用Go安装Wails CLI:
go install github.com/wailsapp/wails/v2/cmd/wails@latest
2. 验证安装:
wails version
平台特定依赖
Windows
Windows系统需要安装WebView2运行时:
- 检查是否已安装WebView2:
wails doctor
2. 如果未安装,可以从Microsoft官网下载并安装
macOS
macOS系统需要安装Xcode命令行工具:
xcode-select --install
Linux
Linux系统需要安装一些依赖包,具体命令因发行版而异。可以使用Wails doctor命令检查所需依赖:
wails doctor
根据输出结果安装相应的依赖包。例如,在Ubuntu/Debian系统上:
sudo apt update sudo apt install build-essential libgtk-3-dev libwebkit2gtk-4.0-dev
验证开发环境
运行以下命令验证开发环境是否正确配置:
wails doctor
如果所有检查都通过,说明开发环境已经搭建完成,可以开始创建Wails项目了。
folder 6. Wails项目结构
了解Wails项目的标准结构对于高效开发至关重要。本节将详细介绍Wails项目的目录结构和各个文件的作用。
标准项目结构
myproject/ ├── app.go # 主应用文件 ├── build/ # 构建配置和资源 │ ├── appicon.png # 应用图标 │ ├── darwin/ # macOS特定构建配置 │ ├── windows/ # Windows特定构建配置 │ └── linux/ # Linux特定构建配置 ├── frontend/ # 前端源代码 │ ├── dist/ # 构建后的前端资源 │ ├── src/ # 前端源代码 │ ├── package.json # 前端依赖配置 │ ├── vite.config.js # Vite构建配置 │ └── wailsjs/ # 自动生成的JS绑定 ├── go.mod # Go模块定义 ├── go.sum # Go依赖校验 └── wails.json # Wails项目配置
核心文件详解
app.go
这是Wails应用的主文件,包含应用的核心逻辑和结构。一个典型的app.go文件如下:
package main import ( "context" "fmt" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" ) // App 结构体 type App struct { ctx context.Context } // NewApp 创建一个新的App实例 func NewApp() *App { return &App{} } // OnStartup 应用启动时调用 func (a *App) OnStartup(ctx context.Context) { a.ctx = ctx } // OnDomReady 前端DOM加载完成时调用 func (a *App) OnDomReady(ctx context.Context) { // 在这里可以安全地调用运行时方法 } // OnShutdown 应用关闭时调用 func (a *App) OnShutdown(ctx context.Context) { // 清理资源 } // Greet 示例方法,可从前端调用 func (a *App) Greet(name string) string { return fmt.Sprintf("Hello, %s!", name) } func main() { // 创建Wails应用实例 app := NewApp() // 配置应用选项 err := wails.Run(&options.App{ Title: "My Project", Width: 1024, Height: 768, AssetServer: &assetserver.Options{ Assets: http.Dir("./frontend/dist"), }, OnStartup: app.OnStartup, OnDomReady: app.OnDomReady, OnShutdown: app.OnShutdown, }) if err != nil { println("Error:", err.Error()) } }
wails.json
这是Wails项目的配置文件,定义了项目的各种设置:
{ "name": "myproject", "outputfilename": "myproject", "frontend:install": "npm install", "frontend:build": "npm run build", "frontend:dev:watcher": "npm run dev", "frontend:dev:serverUrl": "auto", "wailsjsdir": "./frontend", "version": "2", "debug": false, "devServer": { "bindAddress": "localhost", "port": 34115, "assetServer": { "host": "localhost", "port": 34116 } }, "author": { "name": "Your Name", "email": "your.email@example.com" }, "info": { "companyName": "Your Company", "productName": "My Project", "productVersion": "1.0.0", "copyright": "Copyright © 2025, Your Company", "comments": "Built with Wails" }, "nsisType": "multiple", "obfuscated": false, "garbleargs": "", "packaged": true }
前端目录结构
前端目录通常包含以下文件和子目录:
frontend/ ├── dist/ # 构建后的前端资源 ├── src/ # 前端源代码 │ ├── assets/ # 静态资源 │ ├── components/ # 组件 │ ├── main.js # 入口文件 │ └── App.vue # 主组件(如果使用Vue) ├── package.json # 前端依赖配置 ├── vite.config.js # Vite构建配置 └── wailsjs/ # 自动生成的JS绑定 ├── go # Go后端绑定 │ ├── main.ts # 主绑定文件 │ └── models.ts # 数据模型定义 └── runtime # Wails运行时绑定 └── runtime.ts # 运行时方法
构建目录结构
构建目录包含平台特定的构建配置和资源:
build/ ├── appicon.png # 应用图标 ├── darwin/ # macOS特定构建配置 │ ├── Info.plist # macOS应用信息 │ └── icon.icns # macOS应用图标 ├── windows/ # Windows特定构建配置 │ ├── icon.ico # Windows应用图标 │ └── manifest.json # Windows应用清单 └── linux/ # Linux特定构建配置 └── icon.png # Linux应用图标
项目创建和配置
创建新项目
使用Wails CLI创建新项目:
# 创建一个使用Vue和TypeScript的新项目 wails init -n myproject -t vue-ts # 进入项目目录 cd myproject # 安装依赖 wails doctor npm install
配置项目
根据需要修改wails.json文件,调整应用设置:
{ "name": "myproject", "outputfilename": "myproject", "frontend:install": "npm install", "frontend:build": "npm run build", "frontend:dev:watcher": "npm run dev", "frontend:dev:serverUrl": "auto", "wailsjsdir": "./frontend", "version": "2", "debug": false, "devServer": { "bindAddress": "localhost", "port": 34115, "assetServer": { "host": "localhost", "port": 34116 } }, "author": { "name": "Your Name", "email": "your.email@example.com" }, "info": { "companyName": "Your Company", "productName": "My Project", "productVersion": "1.0.0", "copyright": "Copyright © 2025, Your Company", "comments": "Built with Wails" } }
运行项目
在开发模式下运行项目:
wails dev
构建生产版本:
wails build
extension 7. Wails的核心功能
Wails提供了丰富的功能,使开发者能够构建功能强大的桌面应用。本节将详细介绍Wails的核心功能及其使用方法。
原生UI元素
Wails提供了对原生UI元素的支持,使应用能够与操作系统无缝集成。
菜单
Wails允许创建原生菜单,包括应用菜单、上下文菜单和托盘菜单。
package main import ( "context" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/menu" "github.com/wailsapp/wails/v2/pkg/options" ) // App 结构体 type App struct { ctx context.Context } // 创建应用菜单 func (a *App) createApplicationMenu() *menu.Menu { appMenu := menu.NewMenu() // 文件菜单 fileMenu := appMenu.AddSubmenu("File") fileMenu.AddText("&New", nil, a.onNew) fileMenu.AddText("&Open", nil, a.onOpen) fileMenu.AddSeparator() fileMenu.AddText("&Save", nil, a.onSave) fileMenu.AddText("Save &As", nil, a.onSaveAs) fileMenu.AddSeparator() fileMenu.AddText("&Quit", keys.CmdOrCtrl("q"), a.onQuit) // 编辑菜单 editMenu := appMenu.AddSubmenu("Edit") editMenu.AddText("&Undo", keys.CmdOrCtrl("z"), a.onUndo) editMenu.AddText("&Redo", keys.CmdOrCtrl("shift+z"), a.onRedo) editMenu.AddSeparator() editMenu.AddText("Cu&t", keys.CmdOrCtrl("x"), a.onCut) editMenu.AddText("&Copy", keys.CmdOrCtrl("c"), a.onCopy) editMenu.AddText("&Paste", keys.CmdOrCtrl("v"), a.onPaste) return appMenu } // 菜单回调函数 func (a *App) onNew(_ context.Context) { // 处理新建操作 } func (a *App) onOpen(_ context.Context) { // 处理打开操作 } // ... 其他回调函数 func main() { app := NewApp() err := wails.Run(&options.App{ Title: "Menu Demo", Width: 800, Height: 600, Menu: app.createApplicationMenu(), OnStartup: app.OnStartup, OnDomReady: app.OnDomReady, OnShutdown: app.OnShutdown, }) if err != nil { println("Error:", err.Error()) } }
对话框
Wails提供了多种原生对话框,包括消息对话框、文件对话框等。
package main import ( "context" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" ) // App 结构体 type App struct { ctx context.Context } // 显示信息对话框 func (a *App) ShowInfoDialog() { a.runtime.Dialog.Message().Info("Information", "This is an info message") } // 显示错误对话框 func (a *App) ShowErrorDialog() { a.runtime.Dialog.Message().Error("Error", "This is an error message") } // 显示确认对话框 func (a *App) ShowConfirmDialog() bool { response, err := a.runtime.Dialog.Message().Confirm("Confirm", "Are you sure?") if err != nil { return false } return response } // 显示文件打开对话框 func (a *App) ShowOpenFileDialog() string { selection, err := a.runtime.Dialog.OpenFile() if err != nil { return "" } return selection } // 显示文件保存对话框 func (a *App) ShowSaveFileDialog() string { selection, err := a.runtime.Dialog.SaveFile() if err != nil { return "" } return selection } // 显示目录选择对话框 func (a *App) ShowDirectoryDialog() string { selection, err := a.runtime.Dialog.SelectDirectory() if err != nil { return "" } return selection }
窗口控制
Wails提供了丰富的窗口控制功能,包括窗口大小、位置、状态等。
package main import ( "context" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" ) // App 结构体 type App struct { ctx context.Context } // 设置窗口标题 func (a *App) SetWindowTitle(title string) { a.runtime.Window.SetTitle(title) } // 设置窗口大小 func (a *App) SetWindowSize(width, height int) { a.runtime.Window.SetSize(width, height) } // 设置窗口位置 func (a *App) SetWindowPosition(x, y int) { a.runtime.Window.SetPosition(x, y) } // 最大化窗口 func (a *App) MaximizeWindow() { a.runtime.Window.Maximise() } // 最小化窗口 func (a *App) MinimizeWindow() { a.runtime.Window.Minimise() } // 恢复窗口 func (a *App) RestoreWindow() { a.runtime.Window.Unmaximise() } // 全屏窗口 func (a *App) ToggleFullscreen() { a.runtime.Window.ToggleFullscreen() } // 居中窗口 func (a *App) CenterWindow() { a.runtime.Window.Center() }
前后端通信
Wails提供了强大的前后端通信机制,使前端JavaScript能够轻松调用后端Go方法,并支持双向事件通信。
方法调用
Wails自动将导出的Go方法暴露给前端JavaScript,使得前端可以直接调用这些方法。
// Go后端代码 package main import ( "context" "fmt" "time" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" ) // User 结构体 type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` IsActive bool `json:"isActive"` } // App 结构体 type App struct { ctx context.Context } // 获取用户信息 func (a *App) GetUser(id int) (*User, error) { // 模拟从数据库获取用户 if id == 1 { return &User{ ID: 1, Name: "John Doe", Email: "john@example.com", IsActive: true, }, nil } return nil, fmt.Errorf("User not found") } // 获取用户列表 func (a *App) GetUserList() ([]User, error) { // 模拟从数据库获取用户列表 users := []User{ {ID: 1, Name: "John Doe", Email: "john@example.com", IsActive: true}, {ID: 2, Name: "Jane Smith", Email: "jane@example.com", IsActive: true}, {ID: 3, Name: "Bob Johnson", Email: "bob@example.com", IsActive: false}, } return users, nil } // 执行耗时操作 func (a *App) LongRunningOperation(seconds int) (string, error) { // 模拟耗时操作 time.Sleep(time.Duration(seconds) * time.Second) return fmt.Sprintf("Operation completed after %d seconds", seconds), nil }
// 前端JavaScript代码 // 获取单个用户 async function getUser() { try { const user = await window.backend.App.GetUser(1); console.log("User:", user); return user; } catch (error) { console.error("Error getting user:", error); return null; } } // 获取用户列表 async function getUserList() { try { const users = await window.backend.App.GetUserList(); console.log("Users:", users); return users; } catch (error) { console.error("Error getting user list:", error); return []; } } // 执行耗时操作 async function performLongOperation() { try { const result = await window.backend.App.LongRunningOperation(3); console.log("Result:", result); return result; } catch (error) { console.error("Error performing long operation:", error); return null; } }
事件系统
Wails提供了统一的事件系统,支持Go和JavaScript之间的双向事件通信。
// Go后端代码 package main import ( "context" "fmt" "time" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" ) // App 结构体 type App struct { ctx context.Context } // 启动进度更新 func (a *App) StartProgressUpdates() { go func() { for i := 0; i <= 100; i += 10 { // 发送进度更新事件 a.runtime.Events.Emit("progress", i) time.Sleep(500 * time.Millisecond) } // 发送完成事件 a.runtime.Events.Emit("progress_complete", "Operation completed successfully") }() } // 发送通知 func (a *App) SendNotification(message string) { a.runtime.Events.Emit("notification", message) } // 监听前端事件 func (a *App) OnStartup(ctx context.Context) { a.ctx = ctx // 监听前端发送的事件 a.runtime.Events.On("frontend_event", func(data ...interface{}) { if len(data) > 0 { message := data[0].(string) fmt.Printf("Received event from frontend: %s\n", message) // 可以在这里处理前端事件,并发送响应 a.runtime.Events.Emit("backend_response", "Message received: "+message) } }) }
// 前端JavaScript代码 // 监听进度更新事件 function listenToProgressUpdates(callback) { window.runtime.EventsOn("progress", (progress) => { console.log("Progress:", progress); callback(progress); }); // 监听进度完成事件 window.runtime.EventsOn("progress_complete", (message) => { console.log("Progress complete:", message); callback(100, message); }); } // 监听通知事件 function listenToNotifications(callback) { window.runtime.EventsOn("notification", (message) => { console.log("Notification:", message); callback(message); }); } // 监听后端响应事件 function listenToBackendResponses(callback) { window.runtime.EventsOn("backend_response", (message) => { console.log("Backend response:", message); callback(message); }); } // 发送事件到后端 function sendEventToBackend(message) { window.runtime.EventsEmit("frontend_event", message); } // 取消监听事件 function stopListeningToEvents() { window.runtime.EventsOff("progress"); window.runtime.EventsOff("progress_complete"); window.runtime.EventsOff("notification"); window.runtime.EventsOff("backend_response"); }
数据绑定
Wails能够自动将Go结构体转换为TypeScript定义,确保前后端数据类型的一致性。
// Go结构体 type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` IsActive bool `json:"isActive"` }
// 自动生成的TypeScript定义 interface User { id: number; name: string; email: string; isActive: boolean; }
文件系统操作
Wails提供了丰富的文件系统操作功能,使应用能够读写文件、目录等。
package main import ( "context" "os" "path/filepath" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" ) // App 结构体 type App struct { ctx context.Context } // 读取文件内容 func (a *App) ReadFile(path string) (string, error) { content, err := os.ReadFile(path) if err != nil { return "", err } return string(content), nil } // 写入文件内容 func (a *App) WriteFile(path, content string) error { return os.WriteFile(path, []byte(content), 0644) } // 检查文件是否存在 func (a *App) FileExists(path string) bool { _, err := os.Stat(path) return !os.IsNotExist(err) } // 创建目录 func (a *App) CreateDirectory(path string) error { return os.MkdirAll(path, 0755) } // 删除文件 func (a *App) DeleteFile(path string) error { return os.Remove(path) } // 删除目录 func (a *App) DeleteDirectory(path string) error { return os.RemoveAll(path) } // 列出目录内容 func (a *App) ListDirectory(path string) ([]string, error) { entries, err := os.ReadDir(path) if err != nil { return nil, err } var files []string for _, entry := range entries { files = append(files, entry.Name()) } return files, nil } // 获取文件信息 func (a *App) GetFileInfo(path string) (map[string]interface{}, error) { info, err := os.Stat(path) if err != nil { return nil, err } fileInfo := map[string]interface{}{ "name": info.Name(), "size": info.Size(), "mode": info.Mode(), "modTime": info.ModTime(), "isDir": info.IsDir(), } return fileInfo, nil } // 获取应用数据目录 func (a *App) GetAppDataDir() (string, error) { // 获取用户数据目录 userDataDir, err := os.UserConfigDir() if err != nil { return "", err } // 创建应用特定的数据目录 appDataDir := filepath.Join(userDataDir, "myapp") err = os.MkdirAll(appDataDir, 0755) if err != nil { return "", err } return appDataDir, nil }
系统集成
Wails提供了与操作系统深度集成的功能,使应用能够访问系统级功能。
系统托盘
package main import ( "context" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/menu" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/mac" "github.com/wailsapp/wails/v2/pkg/options/windows" ) // App 结构体 type App struct { ctx context.Context } // 创建托盘菜单 func (a *App) createTrayMenu() *menu.Menu { trayMenu := menu.NewMenu() // 显示/隐藏窗口 if a.runtime.Window.IsVisible() { trayMenu.AddText("Hide", nil, func(_ context.Context) { a.runtime.Window.Hide() }) } else { trayMenu.AddText("Show", nil, func(_ context.Context) { a.runtime.Window.Show() }) } trayMenu.AddSeparator() // 退出应用 trayMenu.AddText("Quit", nil, func(_ context.Context) { a.runtime.Quit() }) return trayMenu } func main() { app := NewApp() err := wails.Run(&options.App{ Title: "Tray Demo", Width: 800, Height: 600, Tray: &options.Tray{ Icon: "build/appicon.png", Menu: app.createTrayMenu(), Tooltip: "Tray Demo App", }, OnStartup: app.OnStartup, OnDomReady: app.OnDomReady, OnShutdown: app.OnShutdown, }) if err != nil { println("Error:", err.Error()) } }
系统通知
package main import ( "context" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" ) // App 结构体 type App struct { ctx context.Context } // 显示系统通知 func (a *App) ShowNotification(title, message string) { a.runtime.Notification.Notify(title, message) } // 显示带图标的系统通知 func (a *App) ShowNotificationWithIcon(title, message, iconPath string) { a.runtime.Notification.Notify(title, message) // 注意:图标支持可能因平台而异 }
系统主题
package main import ( "context" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" ) // App 结构体 type App struct { ctx context.Context } // 获取系统主题 func (a *App) GetSystemTheme() string { theme := a.runtime.System.Theme() return theme } // 监听系统主题变化 func (a *App) OnStartup(ctx context.Context) { a.ctx = ctx // 监听系统主题变化 a.runtime.System.OnThemeChange(func(theme string) { // 发送主题变化事件到前端 a.runtime.Events.Emit("theme_changed", theme) }) }
code 8. Wails的实战案例
通过实际案例来学习Wails的开发是最有效的方式。本节将通过几个完整的实战案例,展示如何使用Wails构建功能丰富的桌面应用。
案例1:待办事项管理应用
这是一个简单的待办事项管理应用,展示了Wails的基本功能,包括前后端通信、数据持久化和原生UI元素。
项目结构
todo-app/ ├── app.go # 主应用文件 ├── models/ # 数据模型 │ └── todo.go # 待办事项模型 ├── services/ # 业务逻辑 │ └── todo_service.go # 待办事项服务 ├── build/ # 构建配置和资源 ├── frontend/ # 前端源代码 │ ├── src/ │ │ ├── components/ │ │ │ ├── TodoList.vue # 待办事项列表组件 │ │ │ ├── TodoForm.vue # 待办事项表单组件 │ │ │ └── TodoItem.vue # 待办事项项组件 │ │ ├── stores/ │ │ │ └── todoStore.js # 状态管理 │ │ ├── App.vue │ │ └── main.js │ └── ... ├── go.mod ├── go.sum └── wails.json
后端实现
package models import "time" // Todo 待办事项模型 type Todo struct { ID int `json:"id"` Title string `json:"title"` Completed bool `json:"completed"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` }
package services import ( "fmt" "sync" "time" "todo-app/models" ) // TodoService 待办事项服务 type TodoService struct { todos []*models.Todo nextID int mutex sync.RWMutex } // NewTodoService 创建新的待办事项服务 func NewTodoService() *TodoService { service := &TodoService{ todos: make([]*models.Todo, 0), nextID: 1, } // 添加一些初始数据 service.todos = append(service.todos, &models.Todo{ ID: service.nextID, Title: "Learn Wails", Completed: false, CreatedAt: time.Now(), UpdatedAt: time.Now(), }) service.nextID++ service.todos = append(service.todos, &models.Todo{ ID: service.nextID, Title: "Build a desktop app", Completed: false, CreatedAt: time.Now(), UpdatedAt: time.Now(), }) service.nextID++ return service } // GetAllTodos 获取所有待办事项 func (s *TodoService) GetAllTodos() []*models.Todo { s.mutex.RLock() defer s.mutex.RUnlock() // 返回副本以避免外部修改 todos := make([]*models.Todo, len(s.todos)) copy(todos, s.todos) return todos } // GetTodoByID 根据ID获取待办事项 func (s *TodoService) GetTodoByID(id int) (*models.Todo, error) { s.mutex.RLock() defer s.mutex.RUnlock() for _, todo := range s.todos { if todo.ID == id { return todo, nil } } return nil, fmt.Errorf("Todo not found") } // CreateTodo 创建新的待办事项 func (s *TodoService) CreateTodo(title string) (*models.Todo, error) { if title == "" { return nil, fmt.Errorf("Title cannot be empty") } s.mutex.Lock() defer s.mutex.Unlock() now := time.Now() todo := &models.Todo{ ID: s.nextID, Title: title, Completed: false, CreatedAt: now, UpdatedAt: now, } s.todos = append(s.todos, todo) s.nextID++ return todo, nil } // UpdateTodo 更新待办事项 func (s *TodoService) UpdateTodo(id int, title string, completed bool) (*models.Todo, error) { if title == "" { return nil, fmt.Errorf("Title cannot be empty") } s.mutex.Lock() defer s.mutex.Unlock() for i, todo := range s.todos { if todo.ID == id { s.todos[i].Title = title s.todos[i].Completed = completed s.todos[i].UpdatedAt = time.Now() return s.todos[i], nil } } return nil, fmt.Errorf("Todo not found") } // DeleteTodo 删除待办事项 func (s *TodoService) DeleteTodo(id int) error { s.mutex.Lock() defer s.mutex.Unlock() for i, todo := range s.todos { if todo.ID == id { s.todos = append(s.todos[:i], s.todos[i+1:]...) return nil } } return fmt.Errorf("Todo not found") } // ToggleTodoCompletion 切换待办事项完成状态 func (s *TodoService) ToggleTodoCompletion(id int) (*models.Todo, error) { s.mutex.Lock() defer s.mutex.Unlock() for i, todo := range s.todos { if todo.ID == id { s.todos[i].Completed = !s.todos[i].Completed s.todos[i].UpdatedAt = time.Now() return s.todos[i], nil } } return nil, fmt.Errorf("Todo not found") }
package main import ( "context" "fmt" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/menu" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" "todo-app/models" "todo-app/services" ) // App 结构体 type App struct { ctx context.Context todoService *services.TodoService } // NewApp 创建新的App实例 func NewApp() *App { return &App{ todoService: services.NewTodoService(), } } // OnStartup 应用启动时调用 func (a *App) OnStartup(ctx context.Context) { a.ctx = ctx } // OnDomReady 前端DOM加载完成时调用 func (a *App) OnDomReady(ctx context.Context) { // 在这里可以安全地调用运行时方法 } // OnShutdown 应用关闭时调用 func (a *App) OnShutdown(ctx context.Context) { // 清理资源 } // 创建应用菜单 func (a *App) createApplicationMenu() *menu.Menu { appMenu := menu.NewMenu() // 文件菜单 fileMenu := appMenu.AddSubmenu("File") fileMenu.AddText("&New Todo", keys.CmdOrCtrl("n"), a.onNewTodo) fileMenu.AddSeparator() fileMenu.AddText("&Quit", keys.CmdOrCtrl("q"), a.onQuit) // 编辑菜单 editMenu := appMenu.AddSubmenu("Edit") editMenu.AddText("&Clear Completed", nil, a.onClearCompleted) // 帮助菜单 helpMenu := appMenu.AddSubmenu("Help") helpMenu.AddText("&About", nil, a.onAbout) return appMenu } // 菜单回调函数 func (a *App) onNewTodo(_ context.Context) { // 发送事件到前端,显示新建待办事项对话框 a.runtime.Events.Emit("show_new_todo_dialog") } func (a *App) onQuit(_ context.Context) { a.runtime.Quit() } func (a *App) onClearCompleted(_ context.Context) { // 清除已完成的待办事项 todos := a.todoService.GetAllTodos() for _, todo := range todos { if todo.Completed { a.todoService.DeleteTodo(todo.ID) } } // 发送事件到前端,刷新待办事项列表 a.runtime.Events.Emit("todos_updated") } func (a *App) onAbout(_ context.Context) { a.runtime.Dialog.Message().Info("About", "Todo App v1.0\nBuilt with Wails") } // API方法 - 获取所有待办事项 func (a *App) GetAllTodos() []*models.Todo { return a.todoService.GetAllTodos() } // API方法 - 创建新的待办事项 func (a *App) CreateTodo(title string) (*models.Todo, error) { todo, err := a.todoService.CreateTodo(title) if err != nil { return nil, err } // 发送事件到前端,刷新待办事项列表 a.runtime.Events.Emit("todos_updated") return todo, nil } // API方法 - 更新待办事项 func (a *App) UpdateTodo(id int, title string, completed bool) (*models.Todo, error) { todo, err := a.todoService.UpdateTodo(id, title, completed) if err != nil { return nil, err } // 发送事件到前端,刷新待办事项列表 a.runtime.Events.Emit("todos_updated") return todo, nil } // API方法 - 删除待办事项 func (a *App) DeleteTodo(id int) error { err := a.todoService.DeleteTodo(id) if err != nil { return err } // 发送事件到前端,刷新待办事项列表 a.runtime.Events.Emit("todos_updated") return nil } // API方法 - 切换待办事项完成状态 func (a *App) ToggleTodoCompletion(id int) (*models.Todo, error) { todo, err := a.todoService.ToggleTodoCompletion(id) if err != nil { return nil, err } // 发送事件到前端,刷新待办事项列表 a.runtime.Events.Emit("todos_updated") return todo, nil } func main() { app := NewApp() err := wails.Run(&options.App{ Title: "Todo App", Width: 800, Height: 600, AssetServer: &assetserver.Options{ Assets: http.Dir("./frontend/dist"), }, Menu: app.createApplicationMenu(), OnStartup: app.OnStartup, OnDomReady: app.OnDomReady, OnShutdown: app.OnShutdown, }) if err != nil { println("Error:", err.Error()) } }
前端实现
import { defineStore } from 'pinia'; export const useTodoStore = defineStore('todo', { state: () => ({ todos: [], loading: false, error: null, }), getters: { completedTodos() { return this.todos.filter(todo => todo.completed); }, activeTodos() { return this.todos.filter(todo => !todo.completed); }, totalCount() { return this.todos.length; }, completedCount() { return this.completedTodos.length; }, activeCount() { return this.activeTodos.length; }, }, actions: { async fetchTodos() { this.loading = true; this.error = null; try { this.todos = await window.backend.App.GetAllTodos(); } catch (error) { this.error = error.message; console.error('Error fetching todos:', error); } finally { this.loading = false; } }, async createTodo(title) { this.loading = true; this.error = null; try { const todo = await window.backend.App.CreateTodo(title); this.todos.push(todo); } catch (error) { this.error = error.message; console.error('Error creating todo:', error); } finally { this.loading = false; } }, async updateTodo(id, title, completed) { this.loading = true; this.error = null; try { const todo = await window.backend.App.UpdateTodo(id, title, completed); const index = this.todos.findIndex(t => t.id === id); if (index !== -1) { this.todos[index] = todo; } } catch (error) { this.error = error.message; console.error('Error updating todo:', error); } finally { this.loading = false; } }, async deleteTodo(id) { this.loading = true; this.error = null; try { await window.backend.App.DeleteTodo(id); this.todos = this.todos.filter(todo => todo.id !== id); } catch (error) { this.error = error.message; console.error('Error deleting todo:', error); } finally { this.loading = false; } }, async toggleTodoCompletion(id) { this.loading = true; this.error = null; try { const todo = await window.backend.App.ToggleTodoCompletion(id); const index = this.todos.findIndex(t => t.id === id); if (index !== -1) { this.todos[index] = todo; } } catch (error) { this.error = error.message; console.error('Error toggling todo completion:', error); } finally { this.loading = false; } }, async clearCompleted() { this.loading = true; this.error = null; try { // 调用后端API清除已完成的待办事项 // 这里我们使用前端逻辑,实际项目中应该调用后端API this.todos = this.todos.filter(todo => !todo.completed); } catch (error) { this.error = error.message; console.error('Error clearing completed todos:', error); } finally { this.loading = false; } }, }, });
运行应用
# 在项目根目录下运行开发服务器 wails dev # 构建生产版本 wails build
案例2:文件管理器
这个案例展示了如何使用Wails构建一个简单的文件管理器,包括文件浏览、文件操作和系统集成等功能。
项目结构
file-manager/ ├── app.go # 主应用文件 ├── models/ # 数据模型 │ └── file_info.go # 文件信息模型 ├── services/ # 业务逻辑 │ └── file_service.go # 文件服务 ├── build/ # 构建配置和资源 ├── frontend/ # 前端源代码 │ ├── src/ │ │ ├── components/ │ │ │ ├── FileList.vue # 文件列表组件 │ │ │ ├── FileItem.vue # 文件项组件 │ │ │ ├── Breadcrumb.vue # 面包屑导航组件 │ │ │ └── Toolbar.vue # 工具栏组件 │ │ ├── stores/ │ │ │ └── fileStore.js # 状态管理 │ │ ├── App.vue │ │ └── main.js │ └── ... ├── go.mod ├── go.sum └── wails.json
后端实现
package models import ( "os" "time" ) // FileInfo 文件信息模型 type FileInfo struct { Name string `json:"name"` Path string `json:"path"` IsDir bool `json:"isDir"` Size int64 `json:"size"` LastModified time.Time `json:"lastModified"` Permissions os.FileMode `json:"permissions"` }
package services import ( "fmt" "io/fs" "os" "path/filepath" "sort" "strings" "file-manager/models" ) // FileService 文件服务 type FileService struct { currentPath string } // NewFileService 创建新的文件服务 func NewFileService() *FileService { // 获取当前工作目录作为初始路径 currentPath, _ := os.Getwd() return &FileService{ currentPath: currentPath, } } // GetCurrentPath 获取当前路径 func (s *FileService) GetCurrentPath() string { return s.currentPath } // SetCurrentPath 设置当前路径 func (s *FileService) SetCurrentPath(path string) error { // 检查路径是否存在 info, err := os.Stat(path) if err != nil { return err } // 检查是否是目录 if !info.IsDir() { return fmt.Errorf("Path is not a directory") } s.currentPath = path return nil } // ListFiles 列出当前目录的文件和文件夹 func (s *FileService) ListFiles() ([]*models.FileInfo, error) { entries, err := os.ReadDir(s.currentPath) if err != nil { return nil, err } var files []*models.FileInfo for _, entry := range entries { info, err := entry.Info() if err != nil { continue } file := &models.FileInfo{ Name: entry.Name(), Path: filepath.Join(s.currentPath, entry.Name()), IsDir: entry.IsDir(), Size: info.Size(), LastModified: info.ModTime(), Permissions: info.Mode(), } files = append(files, file) } // 排序:目录在前,文件在后,按名称排序 sort.Slice(files, func(i, j int) bool { if files[i].IsDir && !files[j].IsDir { return true } if !files[i].IsDir && files[j].IsDir { return false } return strings.ToLower(files[i].Name) < strings.ToLower(files[j].Name) }) return files, nil }
package main import ( "context" "fmt" "path/filepath" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/menu" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" "github.com/wailsapp/wails/v2/pkg/options/windows" "file-manager/models" "file-manager/services" ) // App 结构体 type App struct { ctx context.Context fileService *services.FileService } // NewApp 创建新的App实例 func NewApp() *App { return &App{ fileService: services.NewFileService(), } } // OnStartup 应用启动时调用 func (a *App) OnStartup(ctx context.Context) { a.ctx = ctx } // OnDomReady 前端DOM加载完成时调用 func (a *App) OnDomReady(ctx context.Context) { // 在这里可以安全地调用运行时方法 } // OnShutdown 应用关闭时调用 func (a *App) OnShutdown(ctx context.Context) { // 清理资源 } // 创建应用菜单 func (a *App) createApplicationMenu() *menu.Menu { appMenu := menu.NewMenu() // 文件菜单 fileMenu := appMenu.AddSubmenu("File") fileMenu.AddText("&New Folder", keys.CmdOrCtrl("shift+n"), a.onNewFolder) fileMenu.AddText("&New File", keys.CmdOrCtrl("n"), a.onNewFile) fileMenu.AddSeparator() fileMenu.AddText("&Delete", keys.Delete(), a.onDelete) fileMenu.AddText("&Rename", keys.F2(), a.onRename) fileMenu.AddSeparator() fileMenu.AddText("&Refresh", keys.F5(), a.onRefresh) fileMenu.AddSeparator() fileMenu.AddText("&Quit", keys.CmdOrCtrl("q"), a.onQuit) // 编辑菜单 editMenu := appMenu.AddSubmenu("Edit") editMenu.AddText("&Copy", keys.CmdOrCtrl("c"), a.onCopy) editMenu.AddText("Cu&t", keys.CmdOrCtrl("x"), a.onCut) editMenu.AddText("&Paste", keys.CmdOrCtrl("v"), a.onPaste) // 视图菜单 viewMenu := appMenu.AddSubmenu("View") viewMenu.AddText("&Home", keys.CmdOrCtrl("shift+h"), a.onGoHome) viewMenu.AddText("&Up", keys.CmdOrCtrl("u"), a.onGoUp) // 帮助菜单 helpMenu := appMenu.AddSubmenu("Help") helpMenu.AddText("&About", nil, a.onAbout) return appMenu } // 菜单回调函数 func (a *App) onNewFolder(_ context.Context) { // 发送事件到前端,显示新建文件夹对话框 a.runtime.Events.Emit("show_new_folder_dialog") } func (a *App) onNewFile(_ context.Context) { // 发送事件到前端,显示新建文件对话框 a.runtime.Events.Emit("show_new_file_dialog") } func (a *App) onDelete(_ context.Context) { // 发送事件到前端,删除选中的文件或文件夹 a.runtime.Events.Emit("delete_selected") } func (a *App) onRename(_ context.Context) { // 发送事件到前端,重命名选中的文件或文件夹 a.runtime.Events.Emit("rename_selected") } func (a *App) onRefresh(_ context.Context) { // 发送事件到前端,刷新文件列表 a.runtime.Events.Emit("refresh_file_list") } func (a *App) onCopy(_ context.Context) { // 发送事件到前端,复制选中的文件或文件夹 a.runtime.Events.Emit("copy_selected") } func (a *App) onCut(_ context.Context) { // 发送事件到前端,剪切选中的文件或文件夹 a.runtime.Events.Emit("cut_selected") } func (a *App) onPaste(_ context.Context) { // 发送事件到前端,粘贴文件或文件夹 a.runtime.Events.Emit("paste_files") } func (a *App) onGoHome(_ context.Context) { // 导航到用户主目录 home, err := a.fileService.GetHomeDirectory() if err != nil { a.runtime.Dialog.Message().Error("Error", fmt.Sprintf("Failed to get home directory: %v", err)) return } err = a.fileService.NavigateTo(home) if err != nil { a.runtime.Dialog.Message().Error("Error", fmt.Sprintf("Failed to navigate to home directory: %v", err)) return } // 发送事件到前端,刷新文件列表 a.runtime.Events.Emit("current_path_changed", home) } func (a *App) onGoUp(_ context.Context) { // 导航到父目录 err := a.fileService.NavigateUp() if err != nil { a.runtime.Dialog.Message().Error("Error", fmt.Sprintf("Failed to navigate up: %v", err)) return } // 发送事件到前端,刷新文件列表 a.runtime.Events.Emit("current_path_changed", a.fileService.GetCurrentPath()) } func (a *App) onQuit(_ context.Context) { a.runtime.Quit() } func (a *App) onAbout(_ context.Context) { a.runtime.Dialog.Message().Info("About", "File Manager v1.0\nBuilt with Wails") } // API方法 - 获取当前路径 func (a *App) GetCurrentPath() string { return a.fileService.GetCurrentPath() } // API方法 - 列出文件 func (a *App) ListFiles() ([]*models.FileInfo, error) { return a.fileService.ListFiles() } // API方法 - 获取文件信息 func (a *App) GetFileInfo(path string) (*models.FileInfo, error) { return a.fileService.GetFileInfo(path) } // API方法 - 创建目录 func (a *App) CreateDirectory(name string) error { err := a.fileService.CreateDirectory(name) if err != nil { return err } // 发送事件到前端,刷新文件列表 a.runtime.Events.Emit("refresh_file_list") return nil } // API方法 - 创建文件 func (a *App) CreateFile(name string) error { err := a.fileService.CreateFile(name) if err != nil { return err } // 发送事件到前端,刷新文件列表 a.runtime.Events.Emit("refresh_file_list") return nil } // API方法 - 删除文件或目录 func (a *App) Delete(path string) error { // 显示确认对话框 confirmed, err := a.runtime.Dialog.Message().Confirm("Confirm Delete", "Are you sure you want to delete this item?") if err != nil { return err } if !confirmed { return fmt.Errorf("Delete cancelled by user") } err = a.fileService.Delete(path) if err != nil { return err } // 发送事件到前端,刷新文件列表 a.runtime.Events.Emit("refresh_file_list") return nil } // API方法 - 重命名文件或目录 func (a *App) Rename(path, newName string) error { err := a.fileService.Rename(path, newName) if err != nil { return err } // 发送事件到前端,刷新文件列表 a.runtime.Events.Emit("refresh_file_list") return nil } // API方法 - 复制文件或目录 func (a *App) Copy(src, dst string) error { err := a.fileService.Copy(src, dst) if err != nil { return err } // 发送事件到前端,刷新文件列表 a.runtime.Events.Emit("refresh_file_list") return nil } // API方法 - 移动文件或目录 func (a *App) Move(src, dst string) error { err := a.fileService.Move(src, dst) if err != nil { return err } // 发送事件到前端,刷新文件列表 a.runtime.Events.Emit("refresh_file_list") return nil } // API方法 - 导航到指定路径 func (a *App) NavigateTo(path string) error { err := a.fileService.NavigateTo(path) if err != nil { return err } // 发送事件到前端,刷新文件列表 a.runtime.Events.Emit("current_path_changed", path) return nil } // API方法 - 导航到父目录 func (a *App) NavigateUp() error { err := a.fileService.NavigateUp() if err != nil { return err } // 发送事件到前端,刷新文件列表 a.runtime.Events.Emit("current_path_changed", a.fileService.GetCurrentPath()) return nil } // API方法 - 获取驱动器列表 func (a *App) GetDrives() ([]string, error) { return a.fileService.GetDrives() } // API方法 - 打开文件或目录 func (a *App) Open(path string) error { info, err := a.fileService.GetFileInfo(path) if err != nil { return err } if info.IsDir { // 如果是目录,导航到该目录 return a.NavigateTo(path) } // 如果是文件,使用系统默认程序打开 return a.runtime.System.Open(path) } func main() { app := NewApp() err := wails.Run(&options.App{ Title: "File Manager", Width: 1024, Height: 768, MinWidth: 800, MinHeight: 600, AssetServer: &assetserver.Options{Assets: http.Dir("./frontend/dist")}, Menu: app.createApplicationMenu(), OnStartup: app.OnStartup, OnDomReady: app.OnDomReady, OnShutdown: app.OnShutdown, Frameless: !options.IsDarwin(), Windows: &windows.Options{ WebviewUserDataPath: "file-manager-webview-data", }, }) if err != nil { println("Error:", err.Error()) } }
前端实现
import { defineStore } from 'pinia'; export const useFileStore = defineStore('file', { state: () => ({ currentPath: '', files: [], selectedFiles: [], clipboard: { action: null, // 'copy' or 'cut' files: [], }, loading: false, error: null, }), getters: { selectedFile() { return this.selectedFiles.length === 1 ? this.selectedFiles[0] : null; }, hasClipboardContent() { return this.clipboard.action && this.clipboard.files.length > 0; }, }, actions: { async fetchCurrentPath() { try { this.currentPath = await window.backend.App.GetCurrentPath(); } catch (error) { this.error = error.message; console.error('Error fetching current path:', error); } }, async fetchFiles() { this.loading = true; this.error = null; try { this.files = await window.backend.App.ListFiles(); } catch (error) { this.error = error.message; console.error('Error fetching files:', error); } finally { this.loading = false; } }, async navigateTo(path) { this.loading = true; this.error = null; try { await window.backend.App.NavigateTo(path); this.currentPath = path; this.selectedFiles = []; } catch (error) { this.error = error.message; console.error('Error navigating to path:', error); } finally { this.loading = false; } }, async navigateUp() { this.loading = true; this.error = null; try { await window.backend.App.NavigateUp(); await this.fetchCurrentPath(); this.selectedFiles = []; } catch (error) { this.error = error.message; console.error('Error navigating up:', error); } finally { this.loading = false; } }, async createDirectory(name) { this.loading = true; this.error = null; try { await window.backend.App.CreateDirectory(name); await this.fetchFiles(); } catch (error) { this.error = error.message; console.error('Error creating directory:', error); } finally { this.loading = false; } }, async createFile(name) { this.loading = true; this.error = null; try { await window.backend.App.CreateFile(name); await this.fetchFiles(); } catch (error) { this.error = error.message; console.error('Error creating file:', error); } finally { this.loading = false; } }, async delete(path) { this.loading = true; this.error = null; try { await window.backend.App.Delete(path); await this.fetchFiles(); this.selectedFiles = this.selectedFiles.filter(file => file.path !== path); } catch (error) { this.error = error.message; console.error('Error deleting file:', error); } finally { this.loading = false; } }, async rename(path, newName) { this.loading = true; this.error = null; try { await window.backend.App.Rename(path, newName); await this.fetchFiles(); } catch (error) { this.error = error.message; console.error('Error renaming file:', error); } finally { this.loading = false; } }, async copy(src, dst) { this.loading = true; this.error = null; try { await window.backend.App.Copy(src, dst); await this.fetchFiles(); } catch (error) { this.error = error.message; console.error('Error copying file:', error); } finally { this.loading = false; } }, async move(src, dst) { this.loading = true; this.error = null; try { await window.backend.App.Move(src, dst); await this.fetchFiles(); this.selectedFiles = this.selectedFiles.filter(file => file.path !== src); } catch (error) { this.error = error.message; console.error('Error moving file:', error); } finally { this.loading = false; } }, async open(path) { try { await window.backend.App.Open(path); } catch (error) { this.error = error.message; console.error('Error opening file:', error); } }, selectFile(file) { if (!this.selectedFiles.includes(file)) { this.selectedFiles.push(file); } }, deselectFile(file) { this.selectedFiles = this.selectedFiles.filter(f => f !== file); }, toggleFileSelection(file) { if (this.selectedFiles.includes(file)) { this.deselectFile(file); } else { this.selectFile(file); } }, clearSelection() { this.selectedFiles = []; }, selectAll() { this.selectedFiles = [...this.files]; }, setClipboard(action, files) { this.clipboard = { action, files: [...files], }; }, clearClipboard() { this.clipboard = { action: null, files: [], }; }, async paste() { if (!this.hasClipboardContent) { return; } this.loading = true; this.error = null; try { for (const file of this.clipboard.files) { const fileName = file.name; const destPath = `${this.currentPath}/${fileName}`; if (this.clipboard.action === 'copy') { await this.copy(file.path, destPath); } else if (this.clipboard.action === 'cut') { await this.move(file.path, destPath); } } // 如果是剪切操作,清空剪贴板 if (this.clipboard.action === 'cut') { this.clearClipboard(); } } catch (error) { this.error = error.message; console.error('Error pasting files:', error); } finally { this.loading = false; } }, }, });
运行应用
# 在项目根目录下运行开发服务器 wails dev # 构建生产版本 wails build
lightbulb 9. Wails的最佳实践和优化建议
为了充分发挥Wails的潜力,构建高性能、高质量的桌面应用,本节将介绍一些最佳实践和优化建议。
项目结构最佳实践
模块化设计
将应用功能划分为不同的模块,每个模块负责特定的功能。这有助于代码的组织和维护。
myapp/ ├── app.go # 主应用文件 ├── modules/ # 功能模块 │ ├── auth/ # 认证模块 │ │ ├── auth.go # 认证逻辑 │ │ └── models.go # 认证相关模型 │ ├── database/ # 数据库模块 │ │ ├── db.go # 数据库连接 │ │ └── models.go # 数据模型 │ └── services/ # 业务服务 │ ├── user_service.go │ └── product_service.go ├── shared/ # 共享代码 │ ├── utils/ # 工具函数 │ └── constants/ # 常量定义 ├── frontend/ # 前端代码 └── build/ # 构建配置
依赖注入
使用依赖注入模式来管理组件之间的依赖关系,提高代码的可测试性和可维护性。
package main import ( "context" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" "myapp/modules/auth" "myapp/modules/database" "myapp/modules/services" ) // App 应用结构体 type App struct { ctx context.Context authService *auth.Service dbService *database.Service userService *services.UserService } // NewApp 创建新的应用实例 func NewApp() *App { // 初始化数据库服务 dbService := database.NewService() // 初始化认证服务 authService := auth.NewService(dbService) // 初始化用户服务 userService := services.NewUserService(dbService, authService) return &App{ authService: authService, dbService: dbService, userService: userService, } } // OnStartup 应用启动时调用 func (a *App) OnStartup(ctx context.Context) { a.ctx = ctx // 初始化数据库连接 err := a.dbService.Connect() if err != nil { // 处理错误 } } // OnShutdown 应用关闭时调用 func (a *App) OnShutdown(ctx context.Context) { // 关闭数据库连接 a.dbService.Close() } func main() { app := NewApp() err := wails.Run(&options.App{ Title: "My App", Width: 1024, Height: 768, OnStartup: app.OnStartup, OnDomReady: app.OnDomReady, OnShutdown: app.OnShutdown, }) if err != nil { println("Error:", err.Error()) } }
性能优化建议
减少前后端通信频率
前后端通信是Wails应用中的潜在性能瓶颈。以下是一些减少通信频率的建议:
1. 批量操作:将多个小操作合并为一个大操作,减少通信次数。
2. 数据缓存:在前端缓存常用数据,避免重复请求。
3. 事件驱动更新:使用事件机制通知前端数据变化,而不是轮询。
// 不推荐:多次调用 func (a *App) UpdateUserPreferences(userID int, preferences map[string]interface{}) error { for key, value := range preferences { err := a.userService.UpdatePreference(userID, key, value) if err != nil { return err } } return nil } // 推荐:批量更新 func (a *App) UpdateUserPreferences(userID int, preferences map[string]interface{}) error { return a.userService.UpdatePreferences(userID, preferences) }
// 使用缓存存储用户数据 const userCache = new Map(); async function getUser(userID) { // 检查缓存 if (userCache.has(userID)) { return userCache.get(userID); } // 从后端获取数据 const user = await window.backend.App.GetUser(userID); // 存入缓存 userCache.set(userID, user); return user; } // 清除缓存 function clearUserCache(userID) { userCache.delete(userID); }
// 后端:数据变化时发送事件 func (a *App) UpdateUserData(userID int, userData map[string]interface{}) error { err := a.userService.UpdateUser(userID, userData) if err != nil { return err } // 发送用户数据更新事件 a.runtime.Events.Emit("user_data_updated", userID) return nil }
// 前端:监听事件并更新数据 function setupUserUpdateListener() { window.runtime.EventsOn("user_data_updated", (userID) => { // 更新本地数据 refreshUserData(userID); }); }
优化前端资源
// 使用动态导入加载大型组件 const LazyComponent = defineAsyncComponent(() => import('./components/LazyComponent.vue'));
// vite.config.js export default defineConfig({ build: { minify: 'terser', terserOptions: { compress: { drop_console: true, drop_debugger: true, }, }, rollupOptions: { output: { manualChunks: { vendor: ['vue', 'pinia'], }, }, }, }, });
// 使用动态导入加载图片 const loadImage = async (path) => { const image = await import(`@/assets/images/${path}`); return image.default; };
优化Go代码
// 不推荐:阻塞主线程 func (a *App) LongRunningOperation() (string, error) { time.Sleep(5 * time.Second) // 阻塞主线程 return "Operation completed", nil } // 推荐:使用goroutine func (a *App) LongRunningOperation() (string, error) { // 启动goroutine执行耗时操作 go func() { time.Sleep(5 * time.Second) a.runtime.Events.Emit("operation_completed", "Operation completed") }() return "Operation started", nil }
type App struct { ctx context.Context cache map[string]interface{} cacheMu sync.RWMutex } func (a *App) getCachedData(key string, computeFunc func() (interface{}, error)) (interface{}, error) { // 检查缓存 a.cacheMu.RLock() if data, exists := a.cache[key]; exists { a.cacheMu.RUnlock() return data, nil } a.cacheMu.RUnlock() // 计算数据 data, err := computeFunc() if err != nil { return nil, err } // 存入缓存 a.cacheMu.Lock() a.cache[key] = data a.cacheMu.Unlock() return data, nil }
type DatabaseService struct { db *sql.DB config *Config } func NewDatabaseService(config *Config) (*DatabaseService, error) { db, err := sql.Open("mysql", config.DSN) if err != nil { return nil, err } // 设置连接池参数 db.SetMaxOpenConns(25) db.SetMaxIdleConns(25) db.SetConnMaxLifetime(5 * time.Minute) return &DatabaseService{ db: db, config: config, }, nil }
用户体验优化
响应式设计
确保应用界面能够适应不同屏幕尺寸和分辨率。
/* 使用媒体查询适应不同屏幕尺寸 */ .container { width: 100%; max-width: 1200px; margin: 0 auto; padding: 20px; } @media (max-width: 768px) { .container { padding: 10px; } .sidebar { display: none; } } @media (min-width: 769px) and (max-width: 1024px) { .container { padding: 15px; } }
加载状态反馈
为耗时操作提供加载状态反馈,提升用户体验。
<template> <div class="button-container"> <button @click="performAction" :disabled="loading" class="action-button" > <span v-if="!loading">Click Me</span> <span v-else class="loading-text"> <span class="loading-spinner"></span> Processing... </span> </button> </div> </template> <script> export default { data() { return { loading: false, }; }, methods: { async performAction() { this.loading = true; try { await window.backend.App.PerformLongOperation(); } catch (error) { console.error('Error:', error); } finally { this.loading = false; } }, }, }; </script> <style scoped> .action-button { padding: 10px 20px; background-color: #2196f3; color: white; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; } .action-button:hover:not(:disabled) { background-color: #1976d2; } .action-button:disabled { background-color: #ccc; cursor: not-allowed; } .loading-text { display: flex; align-items: center; justify-content: center; } .loading-spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid rgba(255, 255, 255, 0.3); border-radius: 50%; border-top-color: white; animation: spin 1s ease-in-out infinite; margin-right: 8px; } @keyframes spin { to { transform: rotate(360deg); } } </style>
错误处理和用户反馈
提供清晰的错误信息和用户反馈,帮助用户理解问题所在。
// 后端:提供详细的错误信息 func (a *App) ProcessData(data map[string]interface{}) (map[string]interface{}, error) { // 验证输入数据 if data["name"] == nil || data["name"] == "" { return nil, fmt.Errorf("name is required") } // 处理数据 result, err := a.dataService.Process(data) if err != nil { return nil, fmt.Errorf("failed to process data: %v", err) } return result, nil }
// 前端:显示友好的错误信息 async function processData(data) { try { const result = await window.backend.App.ProcessData(data); showSuccessMessage("Data processed successfully"); return result; } catch (error) { // 显示友好的错误信息 if (error.message.includes("name is required")) { showErrorMessage("Please enter a name"); } else if (error.message.includes("failed to process data")) { showErrorMessage("Failed to process data. Please try again."); } else { showErrorMessage("An unexpected error occurred"); } console.error('Error:', error); return null; } }
安全最佳实践
输入验证
始终验证用户输入,防止安全漏洞。
// 后端:验证用户输入 func (a *App) CreateUser(userData map[string]interface{}) (*models.User, error) { // 验证用户名 username, ok := userData["username"].(string) if !ok || username == "" { return nil, fmt.Errorf("username is required and must be a string") } // 验证用户名长度 if len(username) < 3 || len(username) > 20 { return nil, fmt.Errorf("username must be between 3 and 20 characters") } // 验证用户名格式 if !regexp.MustCompile(`^[a-zA-Z0-9_]+$`).MatchString(username) { return nil, fmt.Errorf("username can only contain letters, numbers, and underscores") } // 验证邮箱 email, ok := userData["email"].(string) if !ok || email == "" { return nil, fmt.Errorf("email is required and must be a string") } // 验证邮箱格式 if !regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`).MatchString(email) { return nil, fmt.Errorf("invalid email format") } // 创建用户 user, err := a.userService.CreateUser(username, email) if err != nil { return nil, fmt.Errorf("failed to create user: %v", err) } return user, nil }
敏感数据保护
保护敏感数据,避免在前端暴露不必要的信息。
// 后端:返回敏感数据时进行过滤 func (a *App) GetUserProfile(userID int) (map[string]interface{}, error) { user, err := a.userService.GetUserByID(userID) if err != nil { return nil, err } // 过滤敏感数据 profile := map[string]interface{}{ "id": user.ID, "username": user.Username, "email": user.Email, "fullName": user.FullName, "avatar": user.Avatar, // 不返回密码哈希等敏感信息 } return profile, nil }
权限控制
实现适当的权限控制,确保用户只能访问他们有权访问的资源。
// 后端:实现权限检查 func (a *App) DeleteUser(userID int, currentUserID int) error { // 检查用户是否有权限删除用户 if !a.authService.HasPermission(currentUserID, "users.delete") { return fmt.Errorf("permission denied") } // 检查用户是否尝试删除自己 if userID == currentUserID { return fmt.Errorf("cannot delete your own account") } // 删除用户 err := a.userService.DeleteUser(userID) if err != nil { return fmt.Errorf("failed to delete user: %v", err) } return nil }
跨平台兼容性
处理平台差异
不同操作系统可能有不同的行为和限制,需要妥善处理这些差异。
// 处理不同操作系统的路径分隔符 func (a *App) JoinPath(elements ...string) string { return filepath.Join(elements...) } // 处理不同操作系统的换行符 func (a *App) GetLineBreak() string { if runtime.GOOS == "windows" { return "\r\n" } return "\n" }
条件编译
使用Go的条件编译功能处理平台特定的代码。
// 文件名: platform_specific.go // +build windows package main import ( "github.com/wailsapp/wails/v2/pkg/options/windows" ) func getPlatformOptions() options.App { return options.App{ Windows: &windows.Options{ WebviewUserDataPath: "myapp-webview-data", }, } }
// 文件名: platform_specific.go // +build darwin package main import ( "github.com/wailsapp/wails/v2/pkg/options/mac" ) func getPlatformOptions() options.App { return options.App{ Mac: &mac.Options{ Appearance: mac.NSAppearanceNameDarkAqua, HideTitleBarInFullscreen: true, }, } }
// 文件名: platform_specific.go // +build linux package main func getPlatformOptions() options.App { return options.App{ // Linux特定选项 } }
测试不同平台
确保在所有目标平台上测试应用,验证功能和用户体验。
# 在不同平台上构建和测试应用 # Windows wails build -platform windows # macOS wails build -platform darwin # Linux wails build -platform linux
调试和日志记录
使用日志记录
添加适当的日志记录,帮助调试和监控应用行为。
package main import ( "context" "log" "os" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" ) // App 应用结构体 type App struct { ctx context.Context logger *log.Logger } // NewApp 创建新的应用实例 func NewApp() *App { // 创建日志记录器 logger := log.New(os.Stdout, "APP: ", log.LstdFlags|log.Lshortfile) return &App{ logger: logger, } } // OnStartup 应用启动时调用 func (a *App) OnStartup(ctx context.Context) { a.ctx = ctx a.logger.Println("Application starting up") } // ProcessData 处理数据 func (a *App) ProcessData(data map[string]interface{}) (map[string]interface{}, error) { a.logger.Printf("Processing data: %+v", data) // 处理数据... a.logger.Println("Data processed successfully") return result, nil }
使用调试工具
利用Wails提供的调试工具和功能,提高开发效率。
# 启用调试模式 wails dev -debug # 检查开发环境 wails doctor # 查看详细日志 wails dev -verbose
前端调试
在前端代码中使用适当的调试工具和技术。
// 使用控制台日志调试 function debugLog(message, data) { if (process.env.NODE_ENV === 'development') { console.log(`[DEBUG] ${message}`, data); } } // 使用性能分析工具 function measurePerformance(fn, label) { if (process.env.NODE_ENV === 'development') { console.time(label); const result = fn(); console.timeEnd(label); return result; } return fn(); }
Wails是一个强大的桌面应用开发框架,它结合了Go语言的性能和Web技术的灵活性,为开发者提供了一种构建跨平台桌面应用的高效方式。
通过本教程,我们深入了解了Wails的原理、架构和设计思想,学习了如何搭建开发环境、构建项目结构、实现核心功能,并通过实战案例掌握了Wails的实际应用。同时,我们还探讨了Wails的最佳实践和优化建议,帮助开发者构建高性能、高质量的桌面应用。
Wails的主要优势包括:
- 轻量级:相比Electron,Wails应用体积更小,内存占用更低。
- 高性能:利用Go语言的性能优势和系统原生渲染引擎,提供接近原生应用的性能。
- 跨平台:支持Windows、macOS和Linux三大主流操作系统。
- 开发效率:结合Go和Web技术,开发者可以使用熟悉的技术栈快速构建应用。
- 原生集成:提供原生UI元素和系统集成能力,使应用能够与操作系统无缝集成。
随着桌面应用开发需求的不断增长,Wails作为一个新兴的框架,正在吸引越来越多的开发者和企业的关注。通过掌握Wails的开发技能,开发者可以在桌面应用开发领域获得更多的机会和竞争优势。