Миграция с v2 на v3
Полное руководство по миграции приложения Wails с v2 на v3
Wails v3 — это полностью переписанная версия со значительными улучшениями архитектуры, производительности и удобства разработки. Это руководство поможет перенести приложение с v2 на v3.
Основные изменения:
- Новая структура приложения
- Улучшенная система привязок
- Улучшенное управление окнами
- Улучшенная система событий
- Упрощённая конфигурация
Время миграции: 1-4 ч для типичных приложений
Изменения, нарушающие обратную совместимость
Инициализация приложения
В v2 настройка приложения, конфигурация окна и запуск были объединены в один вызов wails.Run(). При таком монолитном подходе было сложно создавать несколько окон, обрабатывать ошибки на разных этапах и тестировать отдельные компоненты приложения.
В v3 эти задачи разделены на отдельные этапы: создание приложения, создание окна и запуск. Такое разделение обеспечивает явный контроль над каждым этапом жизненного цикла приложения и делает код более модульным и удобным для тестирования.
v2:
err := wails.Run(&options.App{
Title: "My App",
Width: 1024,
Height: 768,
Bind: []interface{}{
&GreetService{},
},
})v3:
app := application.New(application.Options{
Name: "My App",
Services: []application.Service{
application.NewService(&GreetService{}),
},
})
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "My App",
Width: 1024,
Height: 768,
})
app.Run()Преимущества этого подхода:
- Поддержка нескольких окон: окна можно создавать динамически в любой момент, а не только при запуске
- Улучшенная обработка ошибок: каждый этап можно проверять отдельно с надлежащей обработкой ошибок
- Более понятный код: благодаря разделению ясно, что происходит на каждом этапе
- Удобство тестирования: настройку приложения можно тестировать без запуска цикла событий
- Повышенная гибкость: окна можно создавать, уничтожать и создавать заново на протяжении всего жизненного цикла приложения
Привязки
В v2 каждой привязанной структуре требовались поле контекста и метод startup(ctx) для получения контекста среды выполнения. Это создавало тесную связь между бизнес-логикой и средой выполнения Wails, усложняя тестирование и понимание кода.
В v3 используется шаблон сервисов: структуры полностью автономны, и им не нужно хранить контекст среды выполнения. Если сервису необходим доступ к экземпляру приложения, он получает его явно посредством внедрения зависимостей, а не через неявную передачу контекста по цепочке вызовов.
v2:
type App struct {
ctx context.Context
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
}
func (a *App) Greet(name string) string {
return "Hello " + name
}v3:
type GreetService struct{}
func (g *GreetService) Greet(name string) string {
return "Hello " + name
}
// Register as service
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&GreetService{}),
},
})Преимущества этого подхода:
- Отсутствие неявных зависимостей: сервисы представляют собой обычные структуры Go без скрытых зависимостей от среды выполнения
- Упрощённое тестирование: методы сервисов можно тестировать без создания имитации контекста Wails
- Более понятный код: зависимости задаются явно — передаются как аргументы конструктора, а не скрываются в поле контекста
- Улучшенная организация: сервисы можно группировать по предметным областям, а не размещать все в одной структуре
App - Корректная инициализация: когда требуется инициализация, используйте метод
ServiceStartup(), чтобы выполнять её явно
Среда выполнения
В v2 для всех операций среды выполнения требовалось передавать контекст глобальным функциям из пакета runtime. Это создавало тесную связь с объектом контекста во всей кодовой базе, а API выглядел процедурным, а не объектно-ориентированным.
В v3 среда выполнения на основе контекста заменена прямыми вызовами методов объектов приложения и окон. Операции вызываются непосредственно у объектов, на которые они воздействуют, благодаря чему код становится более интуитивным и объектно-ориентированным.
v2:
import "github.com/wailsapp/wails/v2/pkg/runtime"
runtime.WindowSetTitle(a.ctx, "New Title")
runtime.EventsEmit(a.ctx, "event-name", data)v3:
// Store app reference
type MyService struct {
app *application.App
}
func (s *MyService) UpdateTitle() {
window := s.app.Window.Current()
window.SetTitle("New Title")
}
func (s *MyService) EmitEvent() {
s.app.Event.Emit("event-name", data)
}Преимущества этого подхода:
- Объектно-ориентированная архитектура: методы вызываются у объектов, на которые они воздействуют (окна, приложения, меню и т. д.)
- Более ясное назначение:
window.SetTitle()понятнее, чемruntime.WindowSetTitle(ctx, ...) - Улучшенная поддержка IDE: автодополнение работает корректно, когда методы принадлежат объектам
- Ясность при работе с несколькими окнами: если окон несколько, можно явно выбрать окно, над которым будет выполняться операция
- Отсутствие сквозной передачи контекста: контекст не нужно передавать через каждую функцию
Привязки фронтенда
В v2 привязки группировались по имени пакета Go и имени структуры, что обычно приводило к путям вида wailsjs/go/main/App. Такая структура не отражала логическую группировку и затрудняла поиск связанных функций.
В v3 привязки группируются по имени сервиса и модулю приложения, что формирует более понятную логическую структуру. Они создаются в каталоге bindings и упорядочиваются по имени приложения и именам сервисов, поэтому доступные функции легче понять.
v2:
import { Greet } from '../wailsjs/go/main/App'
const result = await Greet("World")v3:
import { Greet } from './bindings/changeme/greetservice'
const result = await Greet("World")Преимущества этого подхода:
- Логичная организация: привязки группируются по именам сервисов, а не по структуре пакетов Go
- Более понятный импорт: путь отражает логику предметной области (greetservice), а не структуру файлов (main/App)
- Упрощённый поиск: привязки можно просматривать по функциям, а не по технической структуре
- Единообразные имена: организация по сервисам соответствует архитектуре серверной части
- Более простые пути: префикс
../wailsjs/goбольше не нужен — используется только./bindings
События
В v2 для событий использовались вариативные параметры interface{}, а в каждую функцию событий требовалось передавать контекст. Обработчики событий получали нетипизированные данные, для которых приходилось вручную выполнять утверждения типов, поэтому система событий была подвержена ошибкам и затрудняла отладку.
В v3 появились типизированные объекты событий, а необходимость передавать контекст устранена. Обработчики получают полноценный объект события с типизированными данными, благодаря чему система событий стала надёжнее и проще в использовании.
v2:
runtime.EventsOn(ctx, "event-name", func(data ...interface{}) {
// Handle event
})
runtime.EventsEmit(ctx, "event-name", data)v3:
app.Event.On("event-name", func(e *application.CustomEvent) {
data := e.Data
// Handle event
})
app.Event.Emit("event-name", data)Почему этот подход лучше:
- Безопасность типов: вместо
...interface{}для событий используются полноценные объекты событий - Упрощённая отладка: объекты событий содержат метаданные, например имя события, что упрощает отладку
- Более понятный API:
app.Event.On()иapp.Event.Emit()интуитивно понятнее функций среды выполнения - Контекст не требуется: события работают непосредственно с объектом приложения без сквозной передачи контекста
- Более простые обработчики: у обработчиков событий чёткая сигнатура вместо вариативных параметров
Окна
В v2 поддерживалось только одно окно на приложение. Окно создавалось при запуске, а все операции с ним выполнялись через функции среды выполнения, которые неявно обращались к этому единственному окну.
В v3 встроенная поддержка нескольких окон стала одной из основных возможностей. Каждое окно представляет собой полноценный объект со своими методами и жизненным циклом. В течение всего времени работы приложения можно динамически создавать несколько окон, управлять ими и уничтожать их.
v2:
// Single window only
runtime.WindowSetSize(ctx, 800, 600)v3:
// Multiple windows supported
window1 := app.Window.New()
window1.SetSize(800, 600)
window2 := app.Window.New()
window2.SetSize(1024, 768)Почему этот подход лучше:
- Многооконные приложения: создавайте приложения с несколькими независимыми окнами (панелями мониторинга, настройками, инструментами и т. д.)
- Явные ссылки на окна: каждое окно является объектом, который можно хранить и которым можно управлять напрямую
- Динамическое создание окон: создавайте и уничтожайте окна в любой момент во время работы приложения
- Независимое состояние окон: у каждого окна есть собственные события, свойства и жизненный цикл
- Улучшенная архитектура: управление окнами основано на объектах, а не на контексте
Этапы миграции
Шаг 1: обновите зависимости
go.mod:
module myapp
go 1.25.0
require (
github.com/wailsapp/wails/v3 v3.0.0-beta.0
)Обновите:
go get github.com/wailsapp/wails/v3@latest
go mod tidy
Шаг 2: обновите main.go
v2:
package main
import (
"embed"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/windows"
)
//go:embed all:frontend/dist
var assets embed.FS
func main() {
app := NewApp()
err := wails.Run(&options.App{
Title: "My App",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{
Assets: assets,
},
Bind: []interface{}{
app,
},
Windows: &windows.Options{
WebviewIsTransparent: false,
},
})
if err != nil {
println("Error:", err.Error())
}
}v3:
package main
import (
"embed"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed frontend/dist
var assets embed.FS
func main() {
app := application.New(application.Options{
Name: "My App",
Services: []application.Service{
application.NewService(&MyService{}),
},
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
})
app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "My App",
Width: 1024,
Height: 768,
})
err := app.Run()
if err != nil {
panic(err)
}
}Шаг 3: преобразуйте структуру App в сервис
v2:
type App struct {
ctx context.Context
}
func NewApp() *App {
return &App{}
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
// Initialisation
}
func (a *App) Greet(name string) string {
return "Hello " + name
}v3:
type MyService struct {
app *application.App
}
func NewMyService(app *application.App) *MyService {
return &MyService{app: app}
}
func (s *MyService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
// Initialisation
return nil
}
func (s *MyService) Greet(name string) string {
return "Hello " + name
}
// Register after app creation
app := application.New(application.Options{})
app.RegisterService(application.NewService(NewMyService(app)))Шаг 4: обновите вызовы среды выполнения
v2:
func (a *App) DoSomething() {
runtime.WindowSetTitle(a.ctx, "New Title")
runtime.EventsEmit(a.ctx, "update", data)
runtime.LogInfo(a.ctx, "Message")
}v3:
func (s *MyService) DoSomething() {
window := s.app.Window.Current()
window.SetTitle("New Title")
s.app.Event.Emit("update", data)
s.app.Logger.Info("Message")
}Шаг 5: обновите клиентскую часть
Сгенерируйте новые привязки:
wails3 generate bindings
Обновите импорты:
// v2
import { Greet } from '../wailsjs/go/main/App'
// v3
import { Greet } from './bindings/changeme/myservice'Обновите обработку событий:
// v2
import { EventsOn, EventsEmit } from '../wailsjs/runtime/runtime'
EventsOn("update", (data) => {
console.log(data)
})
EventsEmit("action", data)
// v3
import { Events } from '@wailsio/runtime'
Events.On("update", (data) => {
console.log(data)
})
Events.Emit("action", data)Шаг 6: обновите конфигурацию
v2 (wails.json):
{
"name": "myapp",
"outputfilename": "myapp",
"frontend:install": "npm install",
"frontend:build": "npm run build",
"frontend:dev:watcher": "npm run dev",
"frontend:dev:serverUrl": "auto"
}v3 (wails.json):
{
"name": "myapp",
"frontend": {
"dir": "./frontend",
"install": "npm install",
"build": "npm run build",
"dev": "npm run dev",
"devServerUrl": "http://localhost:5173"
}
}Сопоставление возможностей
Диалоговые окна
v2:
selection, err := runtime.OpenFileDialog(ctx, runtime.OpenDialogOptions{
Title: "Select File",
})v3:
selection, err := app.Dialog.OpenFileWithOptions(&application.OpenFileDialogOptions{
Title: "Select File",
}).PromptForSingleSelection()Меню
v2:
menu := menu.NewMenu()
menu.Append(menu.Text("File", nil, []*menu.MenuItem{
menu.Text("Quit", nil, func(_ *menu.CallbackData) {
runtime.Quit(ctx)
}),
}))v3:
menu := app.NewMenu()
fileMenu := menu.AddSubmenu("File")
fileMenu.Add("Quit").OnClick(func(ctx *application.Context) {
app.Quit()
})Область уведомлений
v2:
// Not available in v2v3:
systray := app.SystemTray.New()
systray.SetIcon(iconBytes)
systray.SetLabel("My App")
menu := app.NewMenu()
menu.Add("Show").OnClick(showWindow)
menu.Add("Quit").OnClick(app.Quit)
systray.SetMenu(menu)Распространённые проблемы
Проблема: привязки не найдены
Проблема: ошибки импорта после миграции
Решение:
# Regenerate bindings
wails3 generate bindings
# Check output directory
ls frontend/bindings
Проблема: ошибки контекста
Проблема: ctx недоступен
Решение:
Вместо этого сохраните ссылку на приложение:
type MyService struct {
app *application.App
}
func NewMyService(app *application.App) *MyService {
return &MyService{app: app}
}Проблема: методы окна не работают
Проблема: runtime.WindowSetTitle() не существует
Решение:
Вызывайте методы окна напрямую:
window := s.app.Window.Current()
window.SetTitle("New Title")Проблема: события не срабатывают
Проблема: события зарегистрированы, но не принимаются
Решение:
Убедитесь, что имена событий совпадают в точности:
// Go
app.Event.Emit("my-event", data)
// JavaScript
OnEvent("my-event", handler) // Must match exactlyТестирование после миграции
Контрольный список
- Приложение запускается без ошибок
- Все привязки работают
- События отправляются и принимаются
- Окна открываются и закрываются корректно
- Меню работают (если применимо)
- Диалоговые окна работают (если применимо)
- Значок в области уведомлений работает (если применимо)
- Процесс сборки работает
- Сборка для выпуска работает
Команды тестирования
# Development
wails3 dev
# Build
wails3 build
# Generate bindings
wails3 generate bindings
Преимущества v3
Производительность
- Более быстрый запуск — оптимизированная инициализация
- Меньше потребление памяти — эффективное использование ресурсов
- Улучшенный мост — накладные расходы на вызов: <1 мс
Возможности
- Поддержка нескольких окон — встроенная поддержка
- Область уведомлений — встроенная поддержка
- Улучшенные события — типизированный и более простой API
- Сервисы — улучшенная организация кода
Удобство разработки
- Безопасность типов — полная поддержка TypeScript
- Улучшенная обработка ошибок — понятные сообщения об ошибках
- Горячая перезагрузка — более быстрая разработка
- Улучшенная документация — исчерпывающие руководства
Получение помощи
Ресурсы
Частые вопросы
В: Можно ли использовать v2 и v3 одновременно? О: Да, они используют разные пути импорта.
В: Готова ли v3 к использованию в рабочей среде? О: v3 — бета-версия со стабильным API для настольных приложений. На ней уже работают приложения в рабочей среде, но перед развёртыванием необходимо провести тщательное тестирование. v2 остаётся текущей стабильной версией.
В: Будет ли поддерживаться v2? О: Да, для v2 будут выпускаться критически важные обновления.
В: Сколько времени занимает миграция? О: Для типичных приложений — 1-4 ч.
Дальнейшие действия
Начните работу с Wails v3.
Изучите архитектуру v3.
Изучите новую систему привязок.
Ознакомьтесь с полными примерами для v3.
Есть вопросы? Задайте их в Discord или создайте задачу.