Skip to content

Instantly share code, notes, and snippets.

@7iomka
Created February 18, 2025 01:30
Show Gist options
  • Save 7iomka/65669f1058a422b874b1319388b08f82 to your computer and use it in GitHub Desktop.
Save 7iomka/65669f1058a422b874b1319388b08f82 to your computer and use it in GitHub Desktop.
Организация сложной асинхронной логики в FSD-like-проекте на Vue

Организация сложной асинхронной логики в FSD-проекте на Vue

В данной архитектуре сложная бизнес-логика, не связанная напрямую с отображением, выносится в отдельный слой – слой доменной логики (Domain Layer) или слой сценариев/Use Case. При использовании подхода Feature-Driven Development (FSD) можно организовать это следующим образом:

  1. Отдельная фича или доменный модуль

    Для сущности Game можно создать фичу game, внутри которой будут:

    • Use Case / сценарии (например, сценарий добавления игры). В нем реализована цепочка действий: обход файлов, парсинг файла README.md, подготовка данных и вызов API для загрузки.
    • Сервисы/обработчики. Например, сервис для обработки файлов (поиск и парсинг README.md) и сервис для загрузки данных на сервер.
  2. Интерфейсы и контракты

    Внутри фичи определяются контракты (интерфейсы) для каждого сценария. Это позволяет легко подменять реализации (например, для тестирования или изменения бизнес-логики).

  3. Оркестрация через use-case функции

    Сценарий (use-case) инкапсулирует вызовы необходимых сервисов. Он последовательно выполняет все этапы (обход файлов, парсинг, загрузка) и может принимать callback-функцию для обновления прогресса. UI-компонент лишь инициирует сценарий и подписывается на обновления (например, отображение прогресса).

  4. Взаимодействие с UI через абстракции

    UI-компоненты отвечают за сбор входных данных, отображение прогресса и предоставление возможности выбора вариантов развития сценария. Они не содержат бизнес-логику, а лишь вызывают соответствующий сценарий (use-case), который реализует всю сложную асинхронную логику.


Пример организации проекта

Представим, что структура проекта выглядит следующим образом:

  • features/game/components/GameUploader.vue
    UI-компонент для выбора файлов и отображения прогресса загрузки.

  • features/game/use-cases/addGame.ts
    Сценарий добавления игры. Здесь описан flow, включающий:

    • Обход выбранных файлов и поиск файла README.md для парсинга.
    • Парсинг найденного файла для извлечения нужных данных.
    • Загрузку файлов и извлеченных данных на сервер.
  • features/game/services/FileProcessingService.ts
    Сервис, отвечающий за обработку файлов: обход директорий, поиск и парсинг файла README.md.

  • features/game/services/UploadService.ts
    Сервис, реализующий загрузку данных (файлов и результатов парсинга) на сервер.

  • features/game/types/index.ts
    Файл с интерфейсами и типами, описывающими контракт сценариев и сервисов для работы с сущностью Game.

Также могут существовать другие фичи, такие как screenshots или publisher, каждая из которых реализует свои сценарии и сервисы. При наличии пересечений между сущностями (например, связь между Game и Screenshots) можно организовать общий слой для обработки подобных взаимодействий.


Где находится логика?

  • Доменная логика (use-case слой)
    Логика обработки сложных асинхронных сценариев (например, добавление игры с обходом файлов, парсингом и загрузкой) инкапсулируется в use-case функции, которые располагаются внутри соответствующей фичи (features/game/use-cases/addGame.ts).

  • Сервисы
    Детали работы с файловой системой, API и т.п. находятся в сервисах (например, FileProcessingService и UploadService). Эти сервисы не зависят от UI и легко тестируются независимо.

  • UI-компоненты
    UI отвечает за предоставление интерфейса для выбора файлов и отображения прогресса. Они инициируют выполнение use-case функций и реагируют на их результаты (например, обновляя прогресс или показывая сообщение об успехе/ошибке).


Таким образом, при использовании FSD:

  • Доменная логика реализуется в отдельном слое (use-case + сервисы) и не зависит от UI.
  • UI лишь инициирует процесс и отображает его состояние, оставаясь независимым от бизнес-логики.
  • В дальнейшем, при добавлении новых сущностей (например, скриншотов или издателей) или новых сценариев с пересечением между сущностями, можно расширять соответствующие фичи и общий доменный слой, сохраняя чистоту архитектуры.

Пример организации кода

Предположим, структура проекта выглядит так:

src/
 ├─ features/
    ├─ game/
        ├─ components/
            └─ GameUploader.vue      // UI-компонент для выбора файлов, отображения прогресса
        ├─ use-cases/
            └─ addGame.ts             // Сценарий добавления игры
        ├─ services/
            ├─ FileProcessingService.ts
            └─ UploadService.ts
        └─ types/
             └─ index.ts               // Интерфейсы и типы, описывающие контракт сценариев
    ├─ screenshots/ ...                 // Фича для скриншотов
    └─ publisher/ ...                   // Фича для издателей
 ├─ shared/                              // Общие утилиты, например, для работы с файлами, API-клиенты и т.п.
 └─ main.ts

Пример use-case: addGame.ts

// src/features/game/use-cases/addGame.ts
import { FileProcessingService } from '../services/FileProcessingService';
import { UploadService } from '../services/UploadService';

export interface AddGameResult {
  success: boolean;
  parsedData?: any;
  error?: string;
}

/**
 * Функция, реализующая flow добавления игры.
 * Принимает список путей к выбранным файлам/папкам и опционально callback для обновления прогресса.
 */
export async function addGame(
  filePaths: string[],
  onProgress?: (progress: number) => void
): Promise<AddGameResult> {
  try {
    // 1. Проходимся по файлам и ищем README.md
    const parsedData = await FileProcessingService.parseFilesForReadme(filePaths, onProgress);

    // 2. Начинаем загрузку файлов и данных
    const uploadResult = await UploadService.uploadGameData(filePaths, parsedData, onProgress);

    return { success: uploadResult };
  } catch (error: any) {
    return { success: false, error: error.message || 'Ошибка при добавлении игры' };
  }
}

Пример сервиса для обработки файлов

// src/features/game/services/FileProcessingService.ts
export class FileProcessingService {
  static async parseFilesForReadme(
    filePaths: string[],
    onProgress?: (progress: number) => void
  ): Promise<any> {
    // Логика обхода выбранных путей, поиска файла README.md, парсинг его содержимого
    // При необходимости вызываем onProgress(percentage)
    console.log("Начинается парсинг файлов:", filePaths);
    // Имитация асинхронной работы:
    await new Promise((resolve) => setTimeout(resolve, 1000));
    // Возвращаем некие распарсенные данные
    return { title: "Название игры", description: "Описание игры" };
  }
}

Пример сервиса для загрузки

// src/features/game/services/UploadService.ts
export class UploadService {
  static async uploadGameData(
    filePaths: string[],
    parsedData: any,
    onProgress?: (progress: number) => void
  ): Promise<boolean> {
    // Логика загрузки файлов и данных на сервер
    console.log("Начинается загрузка данных игры:", parsedData);
    // Имитация асинхронной загрузки
    await new Promise((resolve) => setTimeout(resolve, 2000));
    return true;
  }
}

Пример UI-компонента

<script lang="ts" setup>
// src/features/game/components/GameUploader.vue
import { ref } from 'vue';
import { addGame } from '../use-cases/addGame';

const selectedFiles = ref<string[]>([]);
const progress = ref(0);
const message = ref('');

async function onUpload() {
  message.value = 'Запуск загрузки...';
  const result = await addGame(selectedFiles.value, (p) => {
    progress.value = p;
  });
  message.value = result.success ? 'Игра успешно добавлена' : `Ошибка: ${result.error}`;
}
</script>

<template>
  <div>
    <!-- UI для выбора файлов, здесь условно через input[type=file] -->
    <input type="file" multiple @change="(e) => { /* заполнить selectedFiles.value */ }" />
    <button @click="onUpload">Добавить игру</button>
    <p>Прогресс: {{ progress }}%</p>
    <p>{{ message }}</p>
  </div>
</template>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment