Вызов бекенд-сервисов из фронтенда
Обзор
Frontend может работать в двух режимах в зависимости от типа сборки:
| Режим | Транспорт | Когда используется |
|---|---|---|
| Tauri (desktop) | IPC через @tauri-apps/api/core | Production desktop-приложение |
| HTTP (web/headless) | REST API через fetch() | Web-режим, E2E тесты, интеграционные тесты |
Переключение прозрачно для кода приложения — один и тот же invoke() вызов работает в обоих режимах.
Как это работает
1. Specta биндинги (bindings.ts)
Файл frontend/src/shared/api/bindings.ts генерируется автоматически через Specta при сборке backend. Содержит типизированные обёртки для всех команд:
// Автосгенерировано — НЕ РЕДАКТИРОВАТЬ ВРУЧНУЮ
export const devicesCommands = {
async listDevices(): Promise<Result<Device[], string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('list_devices') };
} catch (e) {
if (e instanceof Error) throw e;
else return { status: 'error', error: String(e) };
}
},
};TAURI_INVOKE — это invoke из @tauri-apps/api/core, но в HTTP-режиме подменяется на shim.
2. Переключение транспорта (Vite)
frontend/vite.config.ts подменяет импорты при VITE_TRANSPORT=http:
if (isHttpTransport) {
aliases['@tauri-apps/api/core'] = './tauri-core-shim.ts';
aliases['@tauri-apps/api/event'] = './tauri-event-shim.ts';
aliases['@tauri-apps/api/webviewWindow'] = './tauri-webview-shim.ts';
}Env-файлы:
.env.web—VITE_TRANSPORT=http(web-режим).env.e2e—VITE_TRANSPORT=http+VITE_BACKEND_URL=http://localhost:18080(E2E тесты)
3. HTTP Shim (tauri-core-shim.ts)
При HTTP-режиме invoke() превращается в HTTP-запрос:
// frontend/src/shared/api/transport/tauri-core-shim.ts
export async function invoke<T>(cmd: string, args?: Record<string, unknown>): Promise<T> {
const route = COMMAND_ROUTES[cmd];
if (!route) throw `Command "${cmd}" is not available in HTTP transport mode`;
const url = `${BACKEND_URL}${route.path}`;
const response =
route.method === 'GET'
? await fetch(url)
: await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(args ?? {}),
});
if (!response.ok) {
const err = await response.json().catch(() => ({ error: response.statusText }));
throw err.error ?? response.statusText;
}
return response.json();
}4. Маппинг команд → маршруты (command-routes.ts)
// frontend/src/shared/api/transport/command-routes.ts
export const COMMAND_ROUTES: Record<string, { method: 'GET' | 'POST'; path: string }> = {
get_identity: { method: 'GET', path: '/api/identity' },
create_identity: { method: 'POST', path: '/api/identity' },
list_devices: { method: 'GET', path: '/api/devices' },
setup_pin: { method: 'POST', path: '/api/vault/setup-pin' },
start_pairing: { method: 'POST', path: '/api/pairing/start' }, // → StartPairingResult { qr_image, token }
// ... 100+ команд
};Маршруты должны совпадать с #[api(METHOD, "/path")] в backend.
5. Событийная система
Полный реестр событий и архитектура EventBus: Internals: Events.
Tauri mode: нативные события через @tauri-apps/api/event.
HTTP mode: Server-Sent Events (SSE) через shim:
// frontend/src/shared/api/transport/tauri-event-shim.ts
export function listen<T>(event: string, handler: EventCallback<T>): Promise<UnlistenFn> {
const eventSource = new EventSource(`${BACKEND_URL}/api/events`);
eventSource.addEventListener(event, (e) => {
handler({ payload: JSON.parse(e.data) });
});
return Promise.resolve(() => eventSource.close());
}Backend SSE endpoint (/api/events) отправляет события из EventBus через Axum SSE.
Использование в коде
Entity API (рекомендуемый паттерн)
// entities/device/api.ts
import { devicesCommands } from '@/shared/api/bindings';
export async function fetchDevices(): Promise<Device[]> {
const result = await devicesCommands.listDevices();
if (result.status === 'error') throw new Error(result.error);
return result.data;
}Pinia Store
// entities/device/model/store.ts
export const useDevicesStore = defineStore('devices', () => {
const devices = ref<Device[]>([]);
async function loadDevices() {
devices.value = await fetchDevices();
}
return { devices, loadDevices };
});Helpers (unwrapResult)
import { unwrapResult } from '@/shared/api/helpers';
// Авто-unwrap Result<T, E> — бросает ошибку при status === 'error'
const identity = unwrapResult(await identityCommands.getIdentity());Добавление нового API endpoint
Полная инструкция: Создание сервисов.
После создания backend-сервиса добавь маршрут во frontend transport:
// frontend/src/shared/api/transport/command-routes.ts
my_new_command: { method: 'POST', path: '/api/my-endpoint' },Затем перегенерируй биндинги: nx run backend:gen:bindings.
Ключевые файлы
| Файл | Назначение |
|---|---|
frontend/src/shared/api/bindings.ts | Автосгенерированные типы и команды (Specta) |
frontend/src/shared/api/transport/tauri-core-shim.ts | HTTP shim для invoke() |
frontend/src/shared/api/transport/command-routes.ts | Маппинг команд → HTTP routes |
frontend/src/shared/api/transport/tauri-event-shim.ts | SSE shim для событий |
frontend/src/shared/api/helpers.ts | unwrapResult() и утилиты |
frontend/vite.config.ts | Переключение транспорта через алиасы |
frontend/.env.web | Конфигурация web-режима |
frontend/.env.e2e | Конфигурация E2E-режима |