Лекция 3. Web-обёртка как UI: pywebview, CEF Python, Wails¶
PyQt — мощный фреймворк, но человека, умеющего рисовать формы в Qt Designer, ещё нужно найти. А web-разработчиков — и фронтенд-, и фуллстек — на рынке гораздо больше. К тому же современный web-стек (HTML/CSS/JS, React, Vue, Svelte) даёт несравнимо больше свободы в визуальной части, чем десктопные виджеты.
Отсюда популярный подход: взять встроенный браузер и сделать его «окном» приложения. Дизайн интерфейса делается на HTML/CSS/JS, часть логики уходит в JavaScript, а Python (или Go) обслуживает «бэкенд» — работу с файлами, БД, сетью.
Применений масса:
- информационный киоск (контент можно даже подгружать из интернета);
- внутренний инструмент с богатым UI;
- админка / монитор / дашборд;
- кросс-платформенное приложение без Qt.
Какие решения существуют¶
| Решение | Язык | Движок | Особенности |
|---|---|---|---|
| pywebview | Python | Системный WebView (WebKit/Edge/GTK) | Самый простой способ для Python. Маленький, без Chromium. |
| CEF Python | Python | Chromium Embedded Framework | Полный Chromium, тяжелее, но больше возможностей. Зрелый, но менее активно развивается. |
| Eel | Python | Chrome/Edge | Поднимает локальный сервер + браузер. Хорош для прототипов. |
| Flet | Python | Flutter | Не «браузер в окне», но идея похожа: UI на декларативных компонентах, общих с Web/Mobile. |
| Wails | Go | Системный WebView (с Go-бэкендом) | «Electron на Go», но без Node.js. |
| Electron | Node.js | Chromium | Стандарт индустрии (VS Code, Slack, Discord). С Python работает плохо. |
| Tauri | Rust + любой фронт | Системный WebView | Лёгкая альтернатива Electron. Бэкенд — Rust. |
В Python-мире на 2025 год pywebview — самый удобный стартовый выбор; CEF Python — когда нужен именно Chromium с полной поддержкой современных API. В Go-мире — Wails.
Рассмотрим обе экосистемы.
Python: pywebview¶
pywebview — лёгкая обёртка над системным движком:
- macOS — WebKit;
- Windows — WebView2 (Chromium);
- Linux — WebKit2GTK или Qt WebEngine.
Установка:
Минимальный пример:
Можно открыть локальный HTML-файл или удалённый URL:
Вызов Python из JavaScript¶
import webview
class API:
def greet(self, name: str) -> str:
return f"Привет, {name}!"
html = """
<!doctype html>
<html>
<body>
<input id="name" value="мир">
<button id="say">Сказать</button>
<p id="out"></p>
<script>
document.getElementById("say").addEventListener("click", async () => {
const name = document.getElementById("name").value;
const text = await window.pywebview.api.greet(name);
document.getElementById("out").innerText = text;
});
</script>
</body>
</html>
"""
webview.create_window("Hello", html=html, js_api=API())
webview.start()
В JS у вас появляется глобальный объект window.pywebview.api, все методы Python-класса доступны асинхронно (Promises).
Вызов JavaScript из Python¶
window = webview.create_window("App", html=html)
def on_loaded() -> None:
window.evaluate_js("document.title = 'Изменили из Python'")
window.events.loaded += on_loaded
webview.start()
Python: CEF Python (Chromium Embedded Framework)¶
CEF Python встраивает в приложение настоящий Chromium со всеми его возможностями: современный JS, HTML5, CSS3, WebGL, видео/аудио. Цена — приложение «тяжёлое» (десятки мегабайт), и проект развивается медленнее, чем хотелось бы.
Минимальный пример:
import sys
from cefpython3 import cefpython as cef
HTML = """
<!doctype html>
<html>
<head><meta charset="utf-8"></head>
<body>
<h1>CEF Tutorial</h1>
<div id="console"></div>
</body>
</html>
"""
def main() -> None:
sys.excepthook = cef.ExceptHook
cef.Initialize()
cef.CreateBrowserSync(url=html_to_data_uri(HTML), window_title="CEF Tutorial")
cef.MessageLoop()
cef.Shutdown()
def html_to_data_uri(html: str) -> str:
import base64
b64 = base64.b64encode(html.encode("utf-8")).decode("ascii")
return f"data:text/html;base64,{b64}"
if __name__ == "__main__":
main()
Структура CEF-приложения¶
Основные шаги:
- Установить глобальный обработчик исключений (
sys.excepthook = cef.ExceptHook) — CEF использует свой контракт для исключений в дочерних процессах. cef.Initialize(settings=...)— инициализация (до создания окон).cef.CreateBrowserSync(url=..., window_title=...)— создание окна-браузера. URL может быть и обычнымhttps://..., и data-URI с HTML-кодом.- Зарегистрировать обработчики (
browser.SetClientHandler(...)) и привязки JS (browser.SetJavascriptBindings(...)). cef.MessageLoop()— главный цикл сообщений (аналогmainloopв Tkinter).cef.Shutdown()— финализация (имеет смысл обернутьtry/finally).
Привязка Python ↔ JavaScript¶
class External:
def __init__(self, browser) -> None:
self.browser = browser
def test_callbacks(self, js_callback) -> None:
# JavaScript передал нам колбэк — можем вызвать его обратно
js_callback.Call("Строка из Python")
def set_bindings(browser) -> None:
bindings = cef.JavascriptBindings(bindToFrames=False, bindToPopups=False)
bindings.SetProperty("python_property", "это свойство задано в Python")
bindings.SetFunction("html_to_data_uri", html_to_data_uri)
bindings.SetObject("external", External(browser))
browser.SetJavascriptBindings(bindings)
В браузере:
window.onload = function () {
console.log(python_property); // обычное свойство
external.test_callbacks(function (s) { // вызов метода + колбэк
console.log("из Python пришло:", s);
});
};
Со стороны Python можно дёрнуть JS:
browser.ExecuteJavascript("alert('hi')")— выполнить произвольный JS-код;browser.ExecuteFunction("name", arg1, arg2)— вызвать функцию по имени.
Межпроцессный обмен с рендерером асинхронный, поэтому возврат значений из JS обычно делают через колбэки.
Когда выбрать CEF, а не pywebview¶
- Нужен именно Chromium (например, ради WebGL, Service Workers, WebRTC).
- Нужны кастомные настройки браузера (DevTools, перехват ресурсов, кастомные схемы URL).
- Нужно одинаковое поведение на всех ОС независимо от системного движка.
В остальных случаях pywebview проще, легче и менее проблемен в установке.
Go: Wails¶
Wails — фреймворк для десктопных приложений на Go с фронтендом на HTML/JS. Архитектурно близок к Electron, но не использует Chromium: вместо него системный WebView (WebView2 на Windows, WebKit на macOS, WebKitGTK на Linux). Поэтому Wails-приложения весят несколько мегабайт, а не сотни.
# Установка CLI
go install github.com/wailsapp/wails/v2/cmd/wails@latest
# Создание проекта
wails init -n my-app -t vue # шаблоны: vanilla, vue, react, svelte, preact, lit
cd my-app
wails dev # горячая перезагрузка
wails build # сборка артефакта
Архитектура Wails¶
Структура типичного проекта:
my-app/
├── frontend/ # JS/TS-фронтенд (Vite + Vue/React/…)
├── app.go # Go-структура с экспортируемыми методами
├── main.go # точка входа
└── wails.json
app.go — обычная Go-структура, которая «биндится» во фронтенд:
package main
import "context"
type App struct {
ctx context.Context
}
func NewApp() *App {
return &App{}
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
}
func (a *App) Greet(name string) string {
return "Привет, " + name + "!"
}
В main.go структура регистрируется:
package main
import (
"embed"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
//go:embed all:frontend/dist
var assets embed.FS
func main() {
app := NewApp()
_ = wails.Run(&options.App{
Title: "my-app",
Width: 800,
Height: 600,
AssetServer: &assetserver.Options{
Assets: assets,
},
OnStartup: app.startup,
Bind: []any{app},
})
}
В JS-фронтенде сгенерированный wailsjs/go/main/App.js даёт прямой доступ:
import { Greet } from "../wailsjs/go/main/App";
const text = await Greet("мир");
console.log(text); // "Привет, мир!"
Когда выбрать Wails¶
- Хочется писать backend на Go с настоящими горутинами/каналами.
- Нужно действительно лёгкое распространение (один бинарник несколько МБ).
- Команда комфортно работает с TypeScript/Vue/React.
- Нужны системные API: файлы, БД, сеть, GPU — всё через Go.
Когда не лучший выбор:
- Команда — чистые Python-разработчики (выбирайте
pywebviewилиflet). - Нужна максимальная совместимость рендера между ОС (нужен Chromium → берите Electron или CEF).
Сравнение Python ↔ Go¶
| Аспект | pywebview / CEF Python | Wails |
|---|---|---|
| Backend язык | Python | Go |
| Движок (по умолчанию) | Системный (pywebview) / Chromium (CEF) | Системный |
| Биндинги Python/Go ↔ JS | js_api, pywebview.api, JavascriptBindings |
Авто-генерируемый wailsjs/go/... |
| Многопоточность бэкенда | threading, asyncio |
Горутины и каналы (нативно) |
| Распространение | Питон + dependencies (PyInstaller / Nuitka) | Один бинарник + ассеты |
| Размер артефакта | Десятки МБ (CEF — больше) | Несколько МБ |
| Hot reload фронта | Зависит от настройки | wails dev из коробки |
Дилемма выбора подхода к UI¶
Подведём итог всех трёх лекций темы 8. Какой подход к UI выбрать?
| Что важно | Tkinter | PyQt6 / PySide6 | pywebview / Wails |
|---|---|---|---|
| Начать прямо сейчас, без зависимостей | ✅ | ❌ | ❌ |
| Современный внешний вид | ⚠️ (через ttk) |
✅ | ✅ |
| Сложные виджеты (таблицы, графики) | ⚠️ | ✅ | ✅ (через JS-библиотеки) |
| Визуальный редактор | ❌ | ✅ (Qt Designer) | ⚠️ (через CSS-фреймворки) |
| Web-разработчики в команде | ❌ | ❌ | ✅ |
| Лёгкое распространение | ✅ | ⚠️ | ⚠️/✅ (Wails) |
| Сообщество и live-проект | ⚠️ | ✅ | ✅ |
Хорошее правило для учебных и небольших задач: Tkinter → PyQt → web-UI — двигайтесь вверх по сложности по мере того, как требований к интерфейсу становится больше.
Контрольные вопросы¶
- В чём разница между «встроенным WebView» и «полным Chromium»?
- Почему
pywebviewвесит мало, а CEF Python — много? - Как Python-объект становится доступен JavaScript-у в
pywebview? В CEF? - Что такое межпроцессный обмен в CEF и почему результат возвращается через колбэки?
- Чем Wails концептуально отличается от Electron?
- В каких случаях стоит выбрать PyQt, а в каких — web-UI?
- Где живёт «бизнес-логика» в Wails-приложении и как она «биндится» во фронтенд?
- Можно ли в pywebview / CEF / Wails показать удалённый URL, и какие у этого риски?