Паттерны проектирования Composable в Vue
- понедельник, 28 апреля 2025 г. в 00:00:09
Если вы уже освоили основы написания Composable в Vue, то следующий шаг — собрать коллекцию лучших и самых полезных паттернов, расширив свой инструментарий для решения задач:
Паттерны для улучшения управления состоянием
Организация Composable (не всегда нужен отдельный файл!)
Улучшение опыта разработчика, например поддержка одновременно асинхронного и синхронного поведения
В этой статье мы рассмотрим семь различных паттернов для написания более эффективных Composable.
Проблема: Как управлять глобальным состоянием, избегая дублирования и обеспечивая контролируемый доступ?
Решение: Создать реактивное хранилище в модульной области видимости, экспортируя только необходимые части.
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
.
Единая точка управления логикой.
Проблема: Как отделить бизнес-логику от реактивности для улучшения тестируемости?
Решение: Вынести логику в чистые функции, а в 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 становится «прослойкой» для реактивности.
Проблема: Нам не всегда нужно извлекать логику в отдельный файл.
Решение: Для простых случаев создавайте 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>
Итог:
Упрощение структуры для локальной логики.
Избегаем избыточных файлов.
Проблема: Как сделать API Composable гибким для разных сценариев?
Решение: Сделать опцию для расширенного режима и возвращать либо одно значение, либо значение с методами.
// Пример 1: Простой возврат значения
const timer = useTimer(60);
Однако бывают ситуации, когда нам требуется больше контроля и дополнительные значения или методы для компоновки.
// Пример 2: Расширенный API
const { timer, pause, reset } = useTimer(60, {
controls: true // Опция для расширенного режима
});
Итог:
Адаптация под потребности компонента.
Уменьшение сложности API по умолчанию.
Проблема: Как одновременно работать и с реактивными, и с обычными значениями?
Решение: Использовать 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 };
}
Итог:
Единый интерфейс для любых типов аргументов.
Упрощение интеграции с внешними данными.
Проблема: Как сделать 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(); // Асинхронный доступ
Итог:
Гибкость использования в разных контекстах.
Реактивное обновление данных после разрешения промиса.
Проблема: Как избежать длинных списков параметров?
Решение: Заменить аргументы на объект с именованными свойствами.
// До: Сложно запомнить порядок параметров
useRefHistory(state, true, 10);
// После: Самодокументирующийся код
useRefHistory(state, {
deep: true, // Рекурсивное отслеживание
capacity: 10 // Лимит истории
});
Реализация:
export function useRefHistory(source, options = {}) {
const { deep = false, capacity = 100 } = options;
// ...логика
}
Итог:
Читаемость и расширяемость.
Избегаем «мусорных» параметров.
Эти семь паттернов помогут вам создавать Composable, которые:
Управляют состоянием без хаоса.
Разделяют ответственность между логикой и реактивностью.
Адаптируются под разные сценарии использования.
Дополнительные материалы: