Files
knock-gui/article/embed-gui-guide.md
2025-09-10 16:34:23 +06:00

187 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Встраиваем веб-GUI в консольную утилиту: практический гайд
```metadata
id: 10
readTime: 15-20 минут
date: 2025-09-10 18:00
author: Direct-Dev (aka Антон Кузнецов)
level: Средний
tags: #go #angular #spa #embed #static #cli #webui #devops
version: 1.0.0
```
## Содержание
- [Встраиваем веб-GUI в консольную утилиту: практический гайд](#встраиваем-веб-gui-в-консольную-утилиту-практический-гайд)
- [Содержание](#содержание)
- [Введение](#введение)
- [Идея и архитектура](#идея-и-архитектура)
- [Минимальный GUI](#минимальный-gui)
- [Сборка фронтенда](#сборка-фронтенда)
- [Встраивание в Go-сервис](#встраивание-в-go-сервис)
- [API: контракт и примеры](#api-контракт-и-примеры)
- [Запуск и проверка](#запуск-и-проверка)
- [FAQ и типичные ошибки](#faq-и-типичные-ошибки)
## Введение
Задача: добавить простой веб-интерфейс к консольной утилите, собрать его один раз и отдавать статические файлы прямо из бинарника или из каталога рядом. В качестве примера используем минимальный UI со следующими полями:
- Targets (строка вида `tcp:host:port;udp:host:port...`)
- Delay (например `1s`)
- Флаг Wait connection
- Кнопка Execute
## Идея и архитектура
- **Фронтенд (Angular SPA)** собирается в статический набор файлов (`index.html`, `main-*.js`, `styles-*.css`, и т.д.).
- **Бэкенд (Go, Gin/Chi/Stdlib)** отдаёт эти файлы по `/` и реализует API по `/api/...`.
- Раздача статики может быть:
- из файловой системы (проще для разработки);
- через `embed.FS` (удобно для единого бинарника).
## Минимальный GUI
Мы упростили компонент до минимума в ветке `for-article`. Основной экран — одна форма и кнопка запуска.
Ключевой шаблон компонента:
```12:60:/home/su/projects/articles/embed-gui-article/ui/src/app/knock/knock-page.component.html
<div class="container">
<p-card header="Port Knocker (Минимальный UI)">
<form [formGroup]="form" (ngSubmit)="execute()" class="p-fluid">
<div class="grid">
<div class="col-12">
<label>Targets</label>
<input pInputText type="text" formControlName="targets" placeholder="tcp:host:port;udp:host:port" class="w-full" />
</div>
<div class="col-12 md:col-6">
<label>Delay</label>
<input pInputText type="text" formControlName="delay" placeholder="1s" class="w-full" />
</div>
<div class="col-12 md:col-6 flex align-items-center gap-2">
<p-checkbox formControlName="waitConnection" [binary]="true"></p-checkbox>
<label class="checkbox-label">Wait connection</label>
</div>
<div class="col-12">
<button pButton type="submit" label="Execute" class="w-full" [loading]="executing" [disabled]="executing || form.invalid"></button>
</div>
</div>
</form>
</p-card>
</div>
```
Логика отправки запроса:
```1:40:/home/su/projects/articles/embed-gui-article/ui/src/app/knock/knock-page.component.ts
this.http.post('/api/v1/knock-actions/execute', {
targets: v.targets,
delay: v.delay,
waitConnection: v.waitConnection
}).subscribe(...)
```
## Сборка фронтенда
Собираем Angular-приложение и копируем артефакты в каталог, из которого Go-сервис будет обслуживать статику.
Скрипт сборки:
```1:20:/home/su/projects/articles/embed-gui-article/ui/build-for-embeding.sh
#!/bin/bash
# Использование: ./build-for-embeding.sh /abs/path/to/back/cmd/public
npx ng build --configuration production
mkdir -p "$DESTINATION_DIR"
cp -r /home/su/projects/angular/project-front/dist/project-front/browser/* "$DESTINATION_DIR"
```
- Артефакты после `ng build` лежат в `ui/dist/project-front/browser/`.
- Рекомендуемая папка для копирования: `back/cmd/public/` (в проекте уже есть пример статики).
## Встраивание в Go-сервис
Вариант А: раздача файлов из папки (`back/cmd/public`).
- Положите файлы фронта в `back/cmd/public`.
- Убедитесь, что роутер отдаёт `/` и `/*` из этой папки.
Вариант Б: `embed.FS` в бинарник.
Пример (концептуально):
```go
package main
import (
"embed"
"net/http"
)
//go:embed public/**
var webFS embed.FS
func main() {
// http.FileServerFS доступен в Go 1.22+, иначе используйте fs.Sub
http.Handle("/", http.FileServer(http.FS(webFS)))
http.HandleFunc("/api/v1/knock-actions/execute", executeHandler)
http.ListenAndServe(":8080", nil)
}
```
В репозитории уже есть бэкенд с маршрутами и готовой статикой в `back/cmd/public/`. Для статьи достаточно положить собранный фронт туда.
## API: контракт и примеры
Endpoint: `POST /api/v1/knock-actions/execute`
Request JSON:
```json
{
"targets": "tcp:127.0.0.1:22;udp:1.2.3.4:53",
"delay": "1s",
"waitConnection": false
}
```
Response 200:
```json
{ "status": "ok" }
```
Пример curl:
```bash
curl -X POST http://localhost:8080/api/v1/knock-actions/execute \
-H 'Content-Type: application/json' \
-d '{"targets":"tcp:127.0.0.1:22","delay":"1s","waitConnection":false}'
```
## Запуск и проверка
1 Собрать фронт и скопировать файлы:
```bash
cd /home/su/projects/articles/embed-gui-article/ui
./build-for-embeding.sh /home/su/projects/articles/embed-gui-article/back/cmd/public
```
2 Запустить бэкенд (варианты):
- Через make/скрипт, если есть.
- Локально `go run ./back` (или соответствующая команда в вашем проекте).
3 Открыть в браузере `/` и проверить, что форма грузится, а `Execute` бьёт в API.
## FAQ и типичные ошибки
- Статика не находится: проверьте путь копирования и что сервер обслуживает каталог.
- SPA роуты дают 404 при F5: настройте отдачу `index.html` по умолчанию.
- CORS при раздельных портах: либо проксируйте, либо включите CORS в сервере.
---
Этот гайд показывает минимальный путь: собрать веб-UI, скопировать в папку статики бэкенда и обеспечить один APIэндпоинт. Для production можно добавить `embed.FS`, версионирование артефактов и CI/CD.