应用生命周期
了解 Wails 应用从启动到关闭的生命周期
了解应用生命周期
桌面应用具有从启动到关闭的生命周期。Wails v3 提供服务、事件和钩子,用于有效管理此生命周期。
生命周期阶段
1. 创建应用
使用application.New()创建应用:
app := application.New(application.Options{
Name: "My App",
Description: "An application built with Wails",
Services: []application.Service{
application.NewService(&MyService{}),
},
Assets: application.AssetOptions{
Handler: application.BundledAssetFileServer(assets),
},
})执行的操作:
- 解析并验证选项
- 注册服务(但尚未启动)
- 配置资源服务器
- 设置运行时
2. 运行应用
调用app.Run()启动应用:
err := app.Run() // Blocks until quit
if err != nil {
log.Fatal(err)
}执行的操作:
- 按注册顺序启动服务
- 激活事件监听器
- 可以创建窗口
- 开始事件循环
3. 事件循环
应用进入事件循环,并在其中运行大部分时间:
- 处理操作系统事件(鼠标、键盘和窗口事件)
- 处理 Go 到 JS 的消息
- 执行 JS 到 Go 的调用
- 渲染 UI 更新
4. 关闭
应用退出时:
- 检查
ShouldQuit回调(如果已设置) - 执行
OnShutdown回调 - 按相反顺序关闭服务
- 关闭窗口
- 释放资源
服务生命周期
服务是 Wails v3 中管理生命周期的主要方式。服务通过接口提供启动和关闭钩子。有关服务的完整文档,请参阅服务指南。
创建服务
type MyService struct {
db *sql.DB
}
// ServiceStartup is called when the application starts
func (s *MyService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
var err error
s.db, err = sql.Open("sqlite3", "app.db")
if err != nil {
return err // Startup aborts if error returned
}
// Run migrations
if err := s.runMigrations(); err != nil {
return err
}
return nil
}
// ServiceShutdown is called when the application shuts down
func (s *MyService) ServiceShutdown() error {
if s.db != nil {
return s.db.Close()
}
return nil
}注册服务
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&MyService{}),
application.NewService(&AnotherService{}),
},
})要点:
- 服务按注册顺序启动
- 服务按注册顺序的相反顺序关闭
- 如果服务的
ServiceStartup返回错误,应用将中止 - 传递给
ServiceStartup的ctx会在关闭开始时被取消
使用应用上下文
传递给ServiceStartup的上下文在应用程序的整个生命周期内均有效:
func (s *MyService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
// Start a background task that respects shutdown
go func() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.performBackgroundSync()
case <-ctx.Done():
// Application is shutting down
return
}
}
}()
return nil
}也可以从应用程序实例访问该上下文:
app := application.Get()
ctx := app.Context()应用程序级钩子
这些是application.Options中的便捷回调,无需创建完整的服务即可挂接到应用程序生命周期。它们适用于简单的清理任务、退出确认,或需要在关闭流程的特定阶段运行代码的情况。
如果需要通过启动逻辑、依赖注入或有状态资源进行更复杂的生命周期管理,请改用服务。
ShouldQuit
每当收到退出请求时,都会调用ShouldQuit回调——无论是用户关闭最后一个窗口、按下 Cmd+Q (macOS) / Alt+F4 (Windows),还是以编程方式调用app.Quit()。
返回值:
- 返回
true以允许继续退出(应用程序将关闭) - 返回
false以取消退出(应用程序将继续运行)
你可以借此拦截退出请求,并选择阻止退出,例如提示用户存在未保存的更改:
app := application.New(application.Options{
ShouldQuit: func() bool {
if !hasUnsavedChanges() {
return true // No unsaved changes, allow quit
}
// Prompt the user — MessageDialog.Show() blocks and returns nothing.
// The button's OnClick callback fires for whichever button the user picks.
shouldQuit := false
dlg := application.Get().Dialog.Question().
SetTitle("Unsaved Changes").
SetMessage("You have unsaved changes. Quit anyway?")
quit := dlg.AddButton("Quit")
cancel := dlg.AddButton("Cancel")
dlg.SetDefaultButton(cancel)
dlg.SetCancelButton(cancel)
quit.OnClick(func() { shouldQuit = true })
dlg.Show()
return shouldQuit
},
})如果未设置ShouldQuit,应用程序将在收到请求时立即退出。
调用 ShouldQuit 的情况:
- 用户关闭最后一个窗口(除非设置了
DisableQuitOnLastWindowClosed) - 用户在 macOS 上按下 Cmd+Q
- 用户在 Windows 上按下 Alt+F4(焦点位于最后一个窗口时)
- 代码调用
app.Quit()
不调用 ShouldQuit 的情况:
- 进程被终止(SIGKILL、通过任务管理器强制退出)
- 直接调用
os.Exit()
OnShutdown
确认应用程序即将退出时,将调用OnShutdown回调(如果设置了ShouldQuit,则在其返回true后调用)。可使用此回调执行保存状态、关闭数据库连接或释放资源等清理任务。
app := application.New(application.Options{
OnShutdown: func() {
// Save application state
saveState()
// Close connections
cleanup()
},
})在应用程序生命周期内,也可以随时以编程方式注册其他关闭回调:
app.OnShutdown(func() {
log.Println("Application shutting down...")
})多个回调按注册顺序执行。关闭流程会阻塞,直到所有回调执行完毕。
重要提示:请确保关闭回调能快速完成(少于1秒)。操作系统可能会强制终止退出耗时过长的应用程序,这可能会中断清理工作并导致数据丢失。
PostShutdown
所有关闭任务完成后、进程终止前,将调用PostShutdown回调。此时应用程序实例已无法使用——窗口已关闭、服务已停止、资源已释放。
此回调主要用于:
- 在完成所有其他清理工作后必须执行的最终日志记录
- 测试和调试关闭行为
- 在
app.Run()不会返回的平台上确保代码得以运行
app := application.New(application.Options{
PostShutdown: func() {
// Final logging
log.Println("Application terminated cleanly")
// Flush any buffered logs
logger.Sync()
},
})注意:请勿尝试在PostShutdown中使用应用程序功能(窗口、对话框等),因为这些功能已不可用。
基于事件的生命周期
Wails 提供了一个事件系统,可在应用程序中发生各种情况时通知你,例如窗口打开、应用程序启动、主题更改等。你可以监听这些事件,以便在不阻塞或拦截事件的情况下响应生命周期变化。
对于窗口事件,如果需要拦截并取消操作(例如阻止窗口关闭),还可以使用RegisterHook而不是OnWindowEvent。请参阅下文的窗口钩子。
有关事件系统的完整文档,请参阅事件指南。
应用程序事件
监听应用程序生命周期事件:
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) {
app.Logger.Info("Application has started!")
})还可以使用平台特定的事件:
// macOS
app.Event.OnApplicationEvent(events.Mac.ApplicationDidFinishLaunching, func(event *application.ApplicationEvent) {
// Handle macOS launch
})
app.Event.OnApplicationEvent(events.Mac.ApplicationWillTerminate, func(event *application.ApplicationEvent) {
// Handle macOS termination
})
// Windows
app.Event.OnApplicationEvent(events.Windows.ApplicationStarted, func(event *application.ApplicationEvent) {
// Handle Windows start
})窗口事件
监听窗口生命周期事件:
window := app.Window.New()
window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) {
app.Logger.Info("Window gained focus")
})
window.OnWindowEvent(events.Common.WindowClosing, func(e *application.WindowEvent) {
app.Logger.Info("Window is closing")
})窗口钩子(可取消事件)
需要取消事件时,请使用RegisterHook而不是OnWindowEvent:
window := app.Window.New()
var countdown = 3
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
countdown--
if countdown > 0 {
app.Logger.Info("Not closing yet!", "remaining", countdown)
e.Cancel() // Prevent the window from closing
return
}
app.Logger.Info("Window closing now")
})OnWindowEvent 与 RegisterHook 的区别:
OnWindowEvent:在事件发生时通知你(无法取消)RegisterHook:允许你拦截并视情况取消事件
窗口生命周期
窗口有自己的生命周期,从创建到销毁。每个窗口独立加载其前端内容,并且可以随时显示、隐藏或关闭。当用户尝试关闭窗口时,你可以使用RegisterHook拦截此操作,以提示用户确认,或隐藏窗口而不将其销毁。
有关窗口的完整文档,请参阅窗口指南。
创建窗口
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "My Window",
Width: 800,
Height: 600,
})阻止窗口关闭
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
if !hasUnsavedChanges() {
return
}
// MessageDialog.Show() returns nothing; per-button OnClick handlers fire.
dlg := application.Get().Dialog.Question().
SetTitle("Unsaved Changes").
SetMessage("Save before closing?")
save := dlg.AddButton("Save")
discard := dlg.AddButton("Discard")
cancel := dlg.AddButton("Cancel")
dlg.SetDefaultButton(save)
dlg.SetCancelButton(cancel)
save.OnClick(func() { saveChanges() })
cancel.OnClick(func() { e.Cancel() }) // Prevent close
_ = discard // "Discard" falls through and allows close
dlg.Show()
})隐藏而非关闭
系统托盘应用常用以下模式:
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
window.Hide() // Hide instead of destroy
e.Cancel() // Prevent actual close
})多窗口生命周期
使用多个窗口时:
mainWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Main Window",
})
settingsWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Settings",
Width: 400,
Height: 600,
Hidden: true, // Start hidden
})默认行为因平台而异:
| 平台 | 最后一个窗口关闭时的默认行为 |
|---|---|
| macOS | 应用继续运行(菜单栏保留) |
| Windows | 应用退出 |
| Linux | 应用退出 |
macOS 遵循平台原生惯例,即使没有窗口,应用通常也会在菜单栏中保持活动状态。Windows 和 Linux 默认会退出应用。
让所有平台在最后一个窗口关闭时退出应用:
app := application.New(application.Options{
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})让所有平台在最后一个窗口关闭时继续运行应用:
这适用于系统托盘应用或需要在后台继续运行的应用。
app := application.New(application.Options{
Windows: application.WindowsOptions{
DisableQuitOnLastWindowClosed: true,
},
Linux: application.LinuxOptions{
DisableQuitOnLastWindowClosed: true,
},
})常见模式
模式 1:数据库服务
type DatabaseService struct {
db *sql.DB
}
func (s *DatabaseService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
var err error
s.db, err = sql.Open("sqlite3", "app.db")
if err != nil {
return fmt.Errorf("failed to open database: %w", err)
}
if err := s.db.PingContext(ctx); err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
return nil
}
func (s *DatabaseService) ServiceShutdown() error {
if s.db != nil {
return s.db.Close()
}
return nil
}
// Exported methods are available to the frontend
func (s *DatabaseService) GetUsers() ([]User, error) {
// Query implementation
}模式 2:配置服务
type ConfigService struct {
config *Config
path string
}
func (s *ConfigService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
s.path = "config.json"
data, err := os.ReadFile(s.path)
if err != nil {
if os.IsNotExist(err) {
s.config = &Config{} // Default config
return nil
}
return err
}
return json.Unmarshal(data, &s.config)
}
func (s *ConfigService) ServiceShutdown() error {
data, err := json.MarshalIndent(s.config, "", " ")
if err != nil {
return err
}
return os.WriteFile(s.path, data, 0644)
}模式 3:后台工作程序
type WorkerService struct {
cancel context.CancelFunc
}
func (s *WorkerService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
workerCtx, cancel := context.WithCancel(ctx)
s.cancel = cancel
go s.runWorker(workerCtx)
return nil
}
func (s *WorkerService) ServiceShutdown() error {
if s.cancel != nil {
s.cancel()
}
return nil
}
func (s *WorkerService) runWorker(ctx context.Context) {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.doWork()
case <-ctx.Done():
return
}
}
}生命周期参考
| 钩子/接口 | 调用时机 | 可取消? | 用途 |
|---|---|---|---|
ServiceStartup |
在app.Run()期间、事件循环开始前 |
否(返回错误可中止) | 初始化 |
ServiceShutdown |
关闭期间,在OnShutdown之后 |
否 | 清理 |
OnShutdown |
确认退出时 | 否 | 应用清理 |
ShouldQuit |
请求退出时 | 是(返回 false) | 确认退出 |
RegisterHook(WindowClosing) |
请求关闭窗口时 | 是(e.Cancel()) |
阻止窗口关闭 |
OnWindowEvent |
事件发生时 | 否 | 响应事件 |
OnApplicationEvent |
事件发生时 | 否 | 响应事件 |
平台差异
macOS
- 即使没有窗口,应用程序菜单也会保留
- Cmd+Q会触发退出(经过
ShouldQuit) - 除非隐藏,否则程序坞图标会一直保留
- 使用
ApplicationShouldTerminateAfterLastWindowClosed控制退出行为
Windows
- 没有窗口时不会显示应用程序菜单
- Alt+F4会关闭窗口(可使用
RegisterHook阻止) - 系统托盘可使应用保持运行
Linux
- 行为会因桌面环境而异
- 通常与 Windows 类似
调试生命周期问题
问题:应用无法退出
原因:
ShouldQuit返回falseOnShutdown耗时过长- 后台 goroutine 未停止
解决方案:
// 1. Check ShouldQuit logic
ShouldQuit: func() bool {
log.Println("ShouldQuit called")
return true
}
// 2. Keep OnShutdown fast
OnShutdown: func() {
log.Println("OnShutdown started")
// Fast cleanup only
log.Println("OnShutdown finished")
}
// 3. Use context for background tasks
func (s *MyService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
go func() {
<-ctx.Done()
log.Println("Context cancelled, stopping background work")
}()
return nil
}问题:服务启动失败
解决方案:返回描述清晰的错误:
func (s *MyService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
if err := s.init(); err != nil {
return fmt.Errorf("failed to initialise: %w", err)
}
return nil
}系统将记录该错误,并且应用不会启动。
最佳实践
应该做
- 使用服务管理生命周期——服务提供适当的启动和关闭钩子
- 快速完成关闭——争取在1秒内完成所有清理工作
- 使用 context 取消操作——正确停止后台任务
- 处理启动过程中的错误——返回错误以彻底中止启动
- 记录生命周期事件——有助于调试
不应该做
- 不要在服务启动期间阻塞——快速完成初始化(控制在2秒内)
- 不要在关闭期间显示对话框——应用正在退出,UI 可能无法正常工作
- 不要忽略 context——始终在 goroutine 中检查
ctx.Done() - 不要泄漏资源——始终实现
ServiceShutdown
后续步骤
服务——进一步了解服务系统 了解更多 →
事件系统——使用事件进行通信 了解更多 →
窗口管理——创建和管理窗口 了解更多 →