Приложение на Wails 3 и ReactJS
опубликовано: ⏰ 11Очередная вариация на тему комбинации бэкенда с фронтендом в одном приложении. Уже были примеры на Spring Boot с ReactJS, Go с HTMX и Electron с ReactJS. В этот раз альтернатива Electron – Wails v3, который объединяет Go и ReactJS для создания нативных десктопных приложений.
Введение
Wails – это фреймворк, который позволяет использовать Go для бэкенда и современные веб-технологии для фронтенда. В отличие от Electron, который упаковывает целый Chromium, Wails использует нативный webview операционной системы: WebView2 на Windows, WKWebView на macOS и WebKit на Linux. Это означает, что итоговый бинарник весит в 10 раз меньше и потребляет значительно меньше памяти.
Под капотом Wails v3 работает так: Go-код компилируется в нативный бинарник, а фронтенд собирается через Vite и встраивается в бинарник через go:embed. Go-сервисы автоматически экспонируются в JavaScript через сгенерированные TypeScript-биндинги. Никакого ручного IPC – просто вызываешь Go-метод из React, как будто это обычный API.
Одно из важных преимуществ Wails v3 – встроенный MCP-сервер. Он позволяет LLM-агентам (Claude Code, Copilot, opencode) подключаться к запущенному приложению, управлять окнами, делать DOM-снимки, кликать по элементам и вызывать Go-методы. Это может сильно облегчить процессы тестирования и отладки.
Почему Go вместо Node.js
Если сравнивать Wails и Electron, то основные отличия кроются в выборе бэкенда: Go vs Node.js.
| Метрика | Wails 3 (Go) | Electron (Node.js) |
|---|---|---|
| Размер бинарника | ~15 МБ | 100–150 МБ |
| Потребление памяти | ~10 МБ | 100+ МБ |
| Время старта | < 0.5 с | 2–3 с |
| Рендеринг | Нативный webview OS | Встроенный Chromium |
| Язык бэкенда | Компилируемый (Go) | Интерпретируемый (JS/TS) |
| Конкурентность | Goroutines | Event loop (single thread) |
| Типобезопасность | Статическая (Go) | Динамическая (JS) / статическая (TS) |
Размер бинарника – это, пожалуй, самое заметное отличие. Electron упаковывает Chromium, что добавляет ~100 МБ даже к пустому приложению. Wails использует уже установленный в системе webview, поэтому бинарник содержит только Go-код и собранный фронтенд.
Потребление памяти тоже существенно отличается. Каждое окно Electron – это отдельный процесс Chromium, который потребляет от 50 до 200 МБ. Нативный webview в Wails использует системные ресурсы и потребляет значительно меньше памяти.
Go как компилируемый язык даёт предсказуемую производительность. Goroutines позволяют легко обрабатывать конкурентные операции: чтение из базы данных, файловая система, сетевые запросы – всё это работает параллельно без блокировки UI. TypeScript в Electron (Node.js) ограничен single-threaded event loop, и для конкурентности приходится использовать worker-thread’ы, что усложняет архитектуру.
Типобезопасность Go добавляет ещё одно преимущество: компилятор ловит ошибки на этапе сборки, а не в рантайме. Wails дополнительно генерирует TypeScript-биндинги из Go-структов, что обеспечивает типобезопасность и на стороне фронтенда.
Архитектура Wails
В Electron архитектура строится вокруг трёх процессов: main, preload и renderer, которые общаются через IPC. В Wails всё проще: один Go-процесс + webview, и вызов Go-методов из JavaScript происходит напрямую через сгенерированные биндинги. Полное поисание со схемами можно найти на сайте разработчиков How Wails Works.
Архитектура приложения из примера состоит из трёх слоёв:
Frontend (React 19 + TypeScript) – пользовательский интерфейс, собранный через Vite. React-хуки (useTodos, useNotes, useTags) инкапсулируют вызовы Go-сервисов через автоматически сгенерированные TypeScript-биндинги. UI-компоненты построены на shadcn/ui с Radix UI.
Service Layer (Go) – бизнес-логика приложения. Сервисы (TodoService, NoteService, TagService, AppService) содержат валидацию данных, бизнес-правила и обращаются к слою с репозиториями. Методы сервисов автоматически прокидываются в JavaScript через Wails bindings.
Repository Layer (Go + SQLite) – доступ к данным. Используется драйвер modernc.org/sqlite – чистый Go, без CGO, что упрощает кроссплатформенную сборку.
Ключевое отличие от Electron: нет необходимости описывать IPC-каналы, писать preload-скрипты или объявлять типы для window.api. Wails автоматически генерирует TypeScript-обёртки из Go-кода, и вызов TodoService.List() из React выглядит как обычный асинхронный вызов.
Создание приложения
Для начала работы с Wails v3 нужно установить CLI:
go install github.com/wailsapp/wails/v3/cmd/wails3@latestСоздание нового проекта:
# Создание нового проекта с React + TypeScript
wails3 create -n sample-wails-react-golang -t react-ts
# Переход в директорию и установка зависимостей
cd sample-wails-react-golang
go mod tidy
cd frontend && npm install && cd ..
# Запуск в режиме разработки с hot-reload
wails3 devКоманда wails3 dev запускает Go-приложение с hot-reload и Vite-сервер для фронтенда. При изменении Go-файлов приложение перезапускается, а изменения фронтенда применяются мгновенно через HMR.
Опять же полный код примера приводить не стану – статья будет ссылаться на репозиторий и описывать только важные моменты. Проект представляет собой приложение для управления задачами (todos) и заметками (notes) с тегами, поиском и фильтрацией.
Go-бэкенд
Точка входа
Файл main.go инициализирует базу данных, создаёт репозитории и сервисы, регистрирует сервисы в приложении и настраивает окно:
func main() {
// Путь к базе данных в XDG data directory
dbPath := filepath.Join(xdg.DataHome, "sample-wails-react-golang", "app.db")
// Инициализация базы данных
db, err := repository.NewDB(dbPath)
if err != nil {
log.Fatalf("Failed to initialize database: %v", err)
}
defer db.Close()
// Создание repository
todoRepo := repository.NewTodoRepo(db)
noteRepo := repository.NewNoteRepo(db)
tagRepo := repository.NewTagRepo(db)
// Регистрация сервисов
app := application.New(application.Options{
Name: "ToDo Notes",
Description: "A to-do and notes application",
Services: []application.Service{
application.NewService(service.NewTodoService(todoRepo)),
application.NewService(service.NewNoteService(noteRepo)),
application.NewService(service.NewTagService(tagRepo)),
application.NewService(service.NewAppService()),
},
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
})
// Настройка окна
app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "ToDo Notes",
Width: 1200,
Height: 800,
})
if err := app.Run(); err != nil {
log.Fatal(err)
}
}Фронтенд встраивается в бинарник через go:embed:
//go:embed all:frontend/dist
var assets embed.FSСервисы
Сервисы содержат бизнес-логку и валидацию. Например, TodoService предоставляет методы для работы с задачами:
func (s *TodoService) Create(input model.TodoCreateInput) (*model.Todo, error) {
if input.Title == "" {
return nil, fmt.Errorf("title is required")
}
return s.repo.Create(input)
}
func (s *TodoService) Search(query string) ([]model.Todo, error) {
if query == "" {
return []model.Todo{}, nil
}
return s.repo.Search(query)
}Аналогично устроены NoteService (CRUD + поиск заметок), TagService (список тегов) и AppService (информация о версии приложения).
Репозитории данных
Приложение использует SQLite через драйвер modernc.org/sqlite – чистый Go, без CGO. Это означает, что кроссплатформенная сборка не требует установки системных зависимостей SQLite. Файл базы данных хранится в директории XDG: $XDG_DATA_HOME/sample-wails-react-golang/app.db.
Репозитории работают с SQLite в рамках транзакции. Например, TodoRepo создаёт задачи и автоматически добавляет новые теги:
func (r *TodoRepo) Create(input model.TodoCreateInput) (*model.Todo, error) {
tx, err := r.db.Begin()
if err != nil {
return nil, fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback()
priority := input.Priority
if priority == "" {
priority = "medium"
}
res, err := tx.Exec(
`INSERT INTO todos (title, description, status, priority, due_date) VALUES (?, ?, 'pending', ?, ?)`,
input.Title, input.Description, priority, input.DueDate)
// ... создание тегов и commit
}Все запросы параметризованы – SQL-инъекции исключены на уровне кода.
Миграции выполняются при первой инициализации базы данных в db.go.
Автоматические биндинги
Одна из самых удобных фич Wails – автоматическая генерация TypeScript-биндингов из Go-кода. Когда Wails находит публичные методы в зарегистрированных сервисах, он генерирует TypeScript-модули в frontend/bindings/.
Например, для TodoService генерируется todoservice.ts:
import {Call as $Call} from "@wailsio/runtime";
import * as model from "../../pkg/model/models.js";
export function List(filter: model.TodoFilter | null): $CancellablePromise<model.Todo[] | null> {
return $Call.ByID(3506783982, filter);
}
export function Create(input: model.TodoCreateInput): $CancellablePromise<model.Todo | null> {
return $Call.ByID(3583200614, input);
}Каждый Go-метод получает уникальный ID, и вызов через $Call.ByID() маршрутизирует запрос на соответствующий метод сервиса. TypeScript-типы генерируются из Go-структов, поэтому компилятор проверяет корректность вызовов.
В Electron пришлось бы вручную описывать IPC-каналы: объявлять типы для window.api, писать preload-скрипт, регистрировать обработчики в ipcMain. В Wails всё это делается автоматически.
Фронтенд
Фронтенд построен на React 19 + TypeScript + Vite 8. UI-компоненты используют shadcn/ui (Radix UI) и Tailwind CSS v4. Навигация реализована через простой useState + switch без роутера, так как приложение небольшое.
Хуки для работы с Go-сервисами
Каждый домен (todos, notes, tags) имеет свой кастомный хук. useTodos инкапсулирует все операции с задачами:
import * as TodoService from '@bindings/.../service/todoservice';
export function useTodos() {
const [todos, setTodos] = useState<Todo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchTodos = useCallback(async (filter?: TodoFilter) => {
try {
setLoading(true);
const result = await TodoService.List(filter ?? null);
setTodos(result ? result.map(normalizeTodo) : []);
setError(null);
} catch (e: unknown) {
setError((e as Error)?.message || 'Failed to load todos');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchTodos();
}, [fetchTodos]);
const createTodo = async (input: TodoCreateInput): Promise<Todo | null> => {
const result = await TodoService.Create(input);
if (result) {
setTodos(prev => [...prev, normalizeTodo(result)]);
return normalizeTodo(result);
}
return null;
};
// ... updateTodo, deleteTodo, search
return {todos, loading, error, fetchTodos, createTodo, updateTodo, deleteTodo, search};
}Хук вызывает Go-сервис напрямую через сгенерированные биндинги, обновляет локальное состояние React и обрабатывает ошибки. Никакого IPC, preload или ipcRenderer.invoke.
Компоненты и страницы
Страница TodosPage использует хук useTodos, отображает список задач с поиском и фильтрацией, и предоставляет форму для создания/редактирования:
export default function TodosPage() {
const {todos, loading, createTodo, updateTodo, deleteTodo, search} = useTodos();
const {tags: availableTags} = useTags();
const [filter, setFilter] = useState<TodoFilter>({});
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold">Todos</h2>
<Button onClick={() => setFormOpen(true)} size="sm">
<Plus className="h-4 w-4 mr-1"/> New Todo
</Button>
</div>
<SearchBar onSearch={handleSearch} placeholder="Search todos..."/>
<TodoList todos={todos} filter={filter} onFilterChange={setFilter} ... />
<TodoForm open={formOpen} onClose={...} onCreate={...} ... />
</div>
);
}UI-компоненты (Button, Card, Badge, Select) построены на shadcn/ui с поддержкой CVA (class-variance-authority) для вариантов стилей. Иконки – из lucide-react.


MCP-сервер (LLM Control)
Wails v3 включает экспериментальный MCP-сервер – встроенный сервер Model Context Protocol, который позволяет LLM-агентам подключаться к запущенному приложению, управлять им и тестировать UI.
Подключившийся агент может:
- Управлять окнами – размер, позиция, фокус, fullscreen, devtools, reload.
- Инспектировать DOM – CSS-запросы, HTML-содержимое, структурный снимок страницы.
- Выполнять произвольный код внутри окна приложения.
- Симулировать ввод – перемещение мыши, клики, drag-and-drop, скролл.
- Вводить текст и нажимать клавиши.
- Вызывать сервисы (Go), например
main.TodoService.List. - Работать с событиями(emit и wait для Wails-событий).
ℹ️ Предупреждение
MCP-сервер даёт полный программный контроль над приложением. Любой, кто имеет доступ к порту, может читать DOM, выполнять JavaScript, кликать по элементам и вызывать Go-методы.
По соображениям безопасности применяются следующие ограничения:
- Сервер привязывается к
127.0.0.1по умолчанию и отклоняет не-локальные запросы (защита от DNS-rebinding). - Production-сборки не должны включать tag
mcp. Командаwails3 buildбезWAILS_MCP=1не включает сервер. - Для тестирования в LAN можно задать
WAILS_MCP_HOST=0.0.0.0, но только в доверенных сетях.
Активация
MCP-сервер компилируется в приложение только при наличии тега mcp. Без этого тега код сервера полностью отсутствует в бинарнике – нет накладных расходов, нет открытых портов, нет дополнительной поверхности атаки.
Для активации достаточно установить переменную окружения WAILS_MCP=1:
# Режим разработки
WAILS_MCP=1 wails3 dev
# Production build с MCP
WAILS_MCP=1 wails3 buildИли передать тег напрямую:
go run -tags mcp .
go build -tags mcp -o myapp .При запуске приложение выводит адрес MCP-сервера:
INFO MCP server started. Connect MCP clients using the streamable HTTP transport.
url=http://127.0.0.1:9099/mcpПодключение клиентов
Claude Code:
claude mcp add --transport http my-app http://127.0.0.1:9099/mcpЗатем можно просить Claude взаимодействовать с приложением:
Take a DOM snapshot of the main window.
Click the "New Todo" button, type "Buy milk" in the input, then press Enter and verify the item appears.
Call the bound method main.TodoService.List and return the result.VS Code (GitHub Copilot):
{
"github.copilot.chat.mcp.enabled": true,
"mcp": {
"servers": {
"my-wails-app": {
"type": "http",
"url": "http://127.0.0.1:9099/mcp"
}
}
}
}opencode:
opencode поддерживает подключение к MCP-серверам через MCP. Для подключения к Wails-приложению нужно добавить конфигурацию MCP-сервера в opencode.json или opencode.jsonc:
{
"mcp": {
"servers": {
"wails-app": {
"type": "http",
"url": "http://127.0.0.1:9099/mcp"
}
}
}
}Сборка приложения
Когда приложение готово, его собирают командой:
# Production build для текущей ОС
wails3 buildWails собирает фронтенд через Vite, генерирует TypeScript-биндинги, компилирует Go-код и упаковывает всё в единый бинарник. Результат появляется в директории build/bin/.
Кроссплатформенная сборка:
GOOS=linux wails3 build
GOOS=darwin wails3 build
GOOS=windows wails3 buildДля Linux-дистрибутивов Wails использует nfpm для создания пакетов (.deb, .rpm). Конфигурация nfpm находится в build/linux/nfpm/nfpm.yaml.
Заключение
Wails 3 + ReactJS – это более производительная и в части вещей удобная альтернатива Electron для создания десктопных приложений. Основные плюсы:
- Размер: ~15 МБ против 100+ МБ у Electron.
- Память: нативный webview потребляет в разы меньше.
- Простота: нет IPC, preload-скриптов и ручных биндингов – Go-методы доступны из React напрямую.
- Производительность: компилируемый Go с goroutines вместо Node.js event loop.
- MCP-сервер: встроенная возможность подключения LLM-агентов для тестирования и отладки.
Есть и ограничения: нативный webview может отличаться между платформами (WebView2 vs WKWebView), что требует тестирования на каждой ОС. Экосистема Wails v3 пока меньше, чем у Electron, но активно развивается.
ℹ️ Примечание
Пример проекта доступен в открытом доступе.