Пришел к хитрому паттерну. Делюсь.
Будет полезен тем кому нравиться или приходится работать с Vue.
В подходящей ситуации он сэкономит кучу времени и поможет избежать дублирования кода.
Контекст
-
Есть несколько компонентов.
-
Компоненты должны выполнять одинаковую сложную логику.
-
Компоненты должны принимать одинаковые свойства и эмитить одинаковые события.
-
Свойства из composable могут быть опциональными со значениями по умолчанию.
-
Каждый компонент выглядит совершенно по разному (разная вёрстка).
-
Каждый компонент, опционально, в дополнение к общему, может:
-
принимать собственные свойства
-
эмитить собственные события
-
выполнять дополнительную логику.
-
Пожелания к реализации
-
Избежать дублирования событий и свойств.
-
Избежать дублирования логики.
Проблематика : Composables vs Mixins
Раньше, во Vue2 без TS и с mixin-ами вместо composables,
можно было легко вынести логику (функции-методы) в mixin,
описать в этом mixin-е общие свойства,
события вообще не нужно было типизировать — их достаточно было просто эмитить.
Потом этот mixin нужно было подключить к компоненту, и вуаля! Всё работает.
В чем-то похоже на mixin-ы из классического ООП.
Теперь, во Vue3 + TS + composables
нужно хитро жонглировать типами,
чтобы добиться поведения, схожего с тем,
которое можно было реализовать на Vue2 mixin-ами без типизации.
Минималистичный пример решения проблемы
// composable useSomething.ts // Объявляем и экспортируем типы общих свойств... export type Props = { propFromSomething?: string } // ... значения по умолчанию для общих свойств ... export const DEFAULT_PROPS = { propFromSomething: 'bar', } // ... и общие события. export type Emit = { // Трюк с передачей эмитов в composable работает с такой сигнатурой. // Таким-же способом нужно описывать события и в компоненте который использует этот composable. (event: 'eventFromSomething', payload: string): void; // Например так не работает: // eventFromSomething: [value: string] } // Вышеописанные типы ни в какой файл не выносим, // храним в том же файле, что и сам composable. // Потому что они будут нужны лишь в самом composable, и в тех компонентах, // в которых применяется данный composable. export function useSomething({ props, emit }: { // Props из компонента при помощи intersection включает в себя Props из composable, // поэтому с типизацией всё в порядке. // Required, потому что опциональные свойства заменятся дефолтовыми при передаче, // и все будут заполнены значениями. props: Required<Props>, // emit из компонента при помощи intersection включает в себя Emit из composable, // поэтому с типизацией всё в порядке. emit: Emit, }) { async function methodFromSomething() { emit('eventFromSomething', props.propFromSomething) } return { methodFromSomething } }
// Component.ts import { useSomething, type Props as PropsFromSomething, DEFAULT_PROPS as DEFAULT_PROPS_FROM_SOMETHING, type Emit as EmitFromSomething, } from "./useSomething"; // Добавляем к свойствам компонента свойства из composable. export type Props = { propFromComponent?: string } & PropsFromSomething // Оттуда-же добавляем дефолтовые значения. const props = withDefaults(defineProps<Props>(), { propFromComponent: 'foo', ...DEFAULT_PROPS_FROM_SOMETHING }) // И добавляем к событиям компонента события из composable. const emit = defineEmits<{ (event: 'eventFromComponent', payload: boolean): void; } & EmitFromSomething>() const { methodFromSomething } = useSomething({ props, emit })
В приведенном минималистичном примере все выглядит просто,
но дойти до этого было сложнее.
Ничего подобного я не видел ни в руководстве по Vue, ни в обучающих материалах.
Комплексный универсальный пример не могли предложить ни StackOverflow, ни нейросети.
В документации по Vue вообще написано что эмитить события из composable — плохая практика.
С чем лично я не согласен.
Пришлось искать решение самостоятельно.
ссылка на оригинал статьи https://habr.com/ru/articles/927214/
Добавить комментарий