Skip to content

Вызов бекенд-сервисов из фронтенда

Обзор

Frontend может работать в двух режимах в зависимости от типа сборки:

РежимТранспортКогда используется
Tauri (desktop)IPC через @tauri-apps/api/coreProduction desktop-приложение
HTTP (web/headless)REST API через fetch()Web-режим, E2E тесты, интеграционные тесты

Переключение прозрачно для кода приложения — один и тот же invoke() вызов работает в обоих режимах.

plantuml Diagram

Как это работает

1. Specta биндинги (bindings.ts)

Файл frontend/src/shared/api/bindings.ts генерируется автоматически через Specta при сборке backend. Содержит типизированные обёртки для всех команд:

typescript
// Автосгенерировано — НЕ РЕДАКТИРОВАТЬ ВРУЧНУЮ
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:

typescript
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.webVITE_TRANSPORT=http (web-режим)
  • .env.e2eVITE_TRANSPORT=http + VITE_BACKEND_URL=http://localhost:18080 (E2E тесты)

3. HTTP Shim (tauri-core-shim.ts)

При HTTP-режиме invoke() превращается в HTTP-запрос:

typescript
// 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)

typescript
// 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:

typescript
// 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 (рекомендуемый паттерн)

typescript
// 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

typescript
// 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)

typescript
import { unwrapResult } from '@/shared/api/helpers';

// Авто-unwrap Result<T, E> — бросает ошибку при status === 'error'
const identity = unwrapResult(await identityCommands.getIdentity());

Добавление нового API endpoint

Полная инструкция: Создание сервисов.

После создания backend-сервиса добавь маршрут во frontend transport:

typescript
// 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.tsHTTP shim для invoke()
frontend/src/shared/api/transport/command-routes.tsМаппинг команд → HTTP routes
frontend/src/shared/api/transport/tauri-event-shim.tsSSE shim для событий
frontend/src/shared/api/helpers.tsunwrapResult() и утилиты
frontend/vite.config.tsПереключение транспорта через алиасы
frontend/.env.webКонфигурация web-режима
frontend/.env.e2eКонфигурация E2E-режима