Пришел к хитрому паттерну. Делюсь.
Будет полезен тем кому нравиться или приходится работать с 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 - плохая практика.
С чем лично я не согласен.
Пришлось искать решение самостоятельно.
Alexufo
так если этот подход не рекомендуется, детали то в чем?
И если вопрос только в вёрстке, почему нельзя было всю ее засунуть в компонент?
Это же логично, логика одна, представление разное