javascript

Паттерны проектирования Composable в Vue

  • понедельник, 28 апреля 2025 г. в 00:00:09
https://habr.com/ru/articles/904818/

Если вы уже освоили основы написания Composable в Vue, то следующий шаг — собрать коллекцию лучших и самых полезных паттернов, расширив свой инструментарий для решения задач:

  • Паттерны для улучшения управления состоянием

  • Организация Composable (не всегда нужен отдельный файл!)

  • Улучшение опыта разработчика, например поддержка одновременно асинхронного и синхронного поведения

В этой статье мы рассмотрим семь различных паттернов для написания более эффективных Composable.

1. Паттерн Data Store (Хранилище данных)

Проблема: Как управлять глобальным состоянием, избегая дублирования и обеспечивая контролируемый доступ?

Решение: Создать реактивное хранилище в модульной области видимости, экспортируя только необходимые части.

import { reactive, toRefs, readonly } from 'vue';
import { themes } from './utils';

// 1. Глобальное состояние, существующее в рамках модуля
// (будет общим для всех вызовов composable)
const state = reactive({
  darkMode: false,
  sidebarCollapsed: false,
  // 2. Приватное поле, не экспортируется наружу
  theme: 'nord'
});

export default () => {
  // 2. Экспортируем только часть состояния
  const { darkMode, sidebarCollapsed } = toRefs(state);

  // 3. Метод для изменения приватного поля
  const changeTheme = (newTheme) => {
    if (themes.includes(newTheme)) {
	  // Обновляем если тема валидна
      state.theme = newTheme;
    }
  };

  return {
	// 2. Возращаем только часть состояния
    darkMode,
    sidebarCollapsed,
    // 2. Возращаем версию для чтения
    theme: readonly(state.theme),
    // 3. Возращаем метода для изменения базового состояния
    changeTheme
  };
};

Итог:

  • Изоляция глобального состояния.

  • Защита приватных данных через readonly.

  • Единая точка управления логикой.

2. Thin Composables («Тонкие» Composable)

Проблема: Как отделить бизнес-логику от реактивности для улучшения тестируемости?

Решение: Вынести логику в чистые функции, а в Composable оставить только реактивность.

import { ref, watch } from 'vue';
import { convertToFahrenheit } from './temperatureConversion';

export function useTemperatureConverter(celsiusRef) {
  const fahrenheit = ref(0);

  watch(celsiusRef, (newCelsius) => {
    // Логика конвертации вынесена в отдельный модуль
    fahrenheit.value = convertToFahrenheit(newCelsius);
  });

  return { fahrenheit };
}

Итог:

  • Бизнес-логика не зависит от фреймворка.

  • Composable становится «прослойкой» для реактивности.

3. Inline Composables (Встроенные Composable)

Проблема: Нам не всегда нужно извлекать логику в отдельный файл.

Решение: Для простых случаев создавайте Composable прямо в компоненте.

const useCount = (i) => {
  const count = ref(0);

  const increment = () => count.value += 1;
  const decrement = () => count.value -= 1;

  return {
    id: i,
    count,
    increment,
    decrement,
  };
};

const listOfCounters = [];
for (const i = 0; i < 10; i++) {
  listOfCounters.push(useCount(i));
}

В шаблоне мы можем использовать счетчики по отдельности:

<div v-for="counter in listOfCounters" :key="counter.id">
  <button @click="counter.decrement()">-</button>
  {{ counter.count }}
  <button @click="counter.increment()">+</button>
</div>

Итог:

  • Упрощение структуры для локальной логики.

  • Избегаем избыточных файлов.

4. Dynamic Return (Динамический возврат значений)

Проблема: Как сделать API Composable гибким для разных сценариев?

Решение: Сделать опцию для расширенного режима и возвращать либо одно значение, либо значение с методами.

// Пример 1: Простой возврат значения
const timer = useTimer(60);

Однако бывают ситуации, когда нам требуется больше контроля и дополнительные значения или методы для компоновки.

// Пример 2: Расширенный API
const { timer, pause, reset } = useTimer(60, { 
  controls: true // Опция для расширенного режима
});

Итог:

  • Адаптация под потребности компонента.

  • Уменьшение сложности API по умолчанию.

5. Flexible Arguments (Гибкие аргументы)

Проблема: Как одновременно работать и с реактивными, и с обычными значениями?

Решение: Использовать ref() и toValue() для автоматической конвертации.

import { ref, toValue } from 'vue';

export function useSearch(url, search) {
  // 1. Всегда получаем ref, даже если передано не значение а тоже ref
  const searchQuery = ref(search);

  // 2. toValue() получим значение даже если передан не ref а обычное значение
  const results = computed(() => {
    return fetchResults(toValue(url), toValue(searchQuery));
  });

  return { searchQuery, results };
}

Итог:

  • Единый интерфейс для любых типов аргументов.

  • Упрощение интеграции с внешними данными.

6. Async + Sync (Поддержка обоих режимов)

Проблема: Как сделать Composable полезным как для асинхронных, так и для синхронных сценариев?

Решение: Возвращать объект с реактивными данными и промисом.

import { ref } from 'vue';
import { ofetch } from 'ofetch';

function useAsyncOrSync() {
  // Синхронное значение будет немедленно возвращено
  const data = ref(null);

  // Асинхронная операция
  const asyncOperationPromise = ofetch(
    'https://api.example.com/data'
  )
    .then(response => {
      // Реактивное обновление данных по ref
      data.value = response;
      return { data };
    });

  // Объединяем промис и реактивные данные
  const enhancedPromise = Object.assign(asyncOperationPromise, {
    data,
  });

  return enhancedPromise;
}


// Использование:

// Если мы используем его синхронно, то сразу же получаем значение, 
// которое мы инициализировали с помощью data . 
// Затем, когда Promise наконец выполняется, значение обновится.
const { data } = useAsyncOrSync(); // Синхронный доступ


//Или просто await, и вам вообще не придётся иметь дело со null
const { data } = await useAsyncOrSync(); // Асинхронный доступ

Итог:

  • Гибкость использования в разных контекстах.

  • Реактивное обновление данных после разрешения промиса.

7. Options Object (Объект настроек)

Проблема: Как избежать длинных списков параметров?

Решение: Заменить аргументы на объект с именованными свойствами.

// До: Сложно запомнить порядок параметров
useRefHistory(state, true, 10);

// После: Самодокументирующийся код
useRefHistory(state, { 
  deep: true,    // Рекурсивное отслеживание
  capacity: 10   // Лимит истории
});

Реализация:

export function useRefHistory(source, options = {}) {
  const { deep = false, capacity = 100 } = options;
  // ...логика
}

Итог:

  • Читаемость и расширяемость.

  • Избегаем «мусорных» параметров.

Заключение

Эти семь паттернов помогут вам создавать Composable, которые:

  1. Управляют состоянием без хаоса.

  2. Разделяют ответственность между логикой и реактивностью.

  3. Адаптируются под разные сценарии использования.

Дополнительные материалы: