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

от автора

Если вы уже освоили основы написания 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. Адаптируются под разные сценарии использования.

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


ссылка на оригинал статьи https://habr.com/ru/articles/904818/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *