Кога ќе креирате компоненти за вашиот проект, сè започнува забавно и лесно.MyButton.vue
Додадете малку стил и еве го тоа.
<template>
<button class="my-fancy-style">
<slot></slot>
</button>
</template>
Тогаш веднаш сфаќате дека ви треба десетина приклучоци, бидејќи вашиот дизајнерски тим сака да биде во различни бои и големини, со икони на лево и десно, со бројачи...
const props = withDefaults(defineProps<{
theme?: ComponentTheme;
small?: boolean;
icon?: IconSvg; // I’ve described how I cook icons in my previous article
rightIcon?: IconSvg;
counter?: number;
}>(), {
theme: ComponentTheme.BLUE,
icon: undefined,
rightIcon: undefined,
counter: undefined
});
На крајот на краиштата, не можете да ги имате копчињата „Откажи“ и „Ок“ од иста боја, и ви се потребни за да реагирате на интеракциите со корисниците.
const props = withDefaults(defineProps<{
theme?: ComponentTheme;
small?: boolean;
icon?: IconSvg;
rightIcon?: IconSvg;
counter?: number;
disabled?: boolean;
loading?: boolean;
}>(), {
theme: ComponentTheme.BLUE,
icon: undefined,
rightIcon: undefined,
counter: undefined
});
Па, добивате идеја: ќе има нешто диво, како поминувањеwidth: 100%
или додавање на автофокус – сите знаеме како изгледа едноставно во Фигма додека вистинскиот живот не удари тешко.
Сега замислете копче за линк: изгледа исто, но кога ќе го притиснете, треба да одите на надворешната или внатрешната врска.<RouterLink>
или<a>
Тагови секој пат, но ве молиме не. Можете исто така да додадетеto
иhref
Придобивки за вашата почетна компонента, но ќе се чувствувате задушени наскоро:
<component
:is="to ? RouterLink : href ? 'a' : 'button'"
<!-- ugh! -->
Се разбира, ќе ви треба компонента на "второ ниво" која го обвива копчето (исто така, ќе се справи со претпоставките за хиперлинкови и некои други интересни работи, но јас ќе ги пропуштам заради едноставноста):
<template>
<component
:is="props.to ? RouterLink : 'a'"
:to="props.to"
:href="props.href"
class="my-link-button"
>
<MyButton v-bind="$attrs">
<slot></slot>
</MyButton>
</component>
</template>
<script lang="ts" setup>
import MyButton from './MyButton.vue';
import { RouteLocationRaw, RouterLink } from 'vue-router';
const props = defineProps<{
to?: RouteLocationRaw;
href?: string;
}>();
</script>
И тука започнува нашата приказна.
Square One
Плоштад 1Па, во основа тоа ќе работи, нема да лажам.<MyLinkButton :counter=“2">
Но нема да има автокомплет за деривативните приклучоци, што не е кул:
Можеме тивко да ги шириме прописите, но ИДЕ не знае ништо за нив, и тоа е срам.
Едноставно и очигледно решение е да ги шириме експлицитно:
<template>
<component
:is="props.to ? RouterLink : 'a'"
:to="props.to"
:href="props.href"
class="my-link-button"
>
<MyButton
:theme="props.theme"
:small="props.small"
:icon="props.icon"
:right-icon="props.rightIcon"
:counter="props.counter"
:disabled="props.disabled"
:loading="props.loading"
>
<slot></slot>
</MyButton>
</component>
</template>
<script lang="ts" setup>
// imports...
const props = withDefaults(
defineProps<{
theme?: ComponentTheme;
small?: boolean;
icon?: IconSvg;
rightIcon?: IconSvg;
counter?: number;
disabled?: boolean;
loading?: boolean;
to?: RouteLocationRaw;
href?: string;
}>(),
{
theme: ComponentTheme.BLUE,
icon: undefined,
rightIcon: undefined,
counter: undefined,
}
);
</script>
Ќе функционира. ИДЕ ќе има соодветна автокомплета. Ќе имаме многу болка и жалење за поддршката.
Очигледно, принципот „Не се повторувај себеси“ не беше применет тука, што значи дека ќе треба да го синхронизираме секое ажурирање. Еден ден, ќе треба да додадете уште еден проп, и ќе мора да најдете секоја компонента која го обвива основниот. Да, копчето и LinkButton веројатно се доволни, но замислете TextInput и десетина компоненти кои зависат од него: PasswordInput, EmailInput, NumberInput, DateInput, HellKnowsWhatElseInput.
На крајот на краиштата, тоа е грдо. И колку повеќе приклучоци имаме, толку полошо станува.
Clean It Up
Чистење нагореТешко е повторно да се користи анонимен тип, па да му дадеме име.
// MyButton.props.ts
export interface MyButtonProps {
theme?: ComponentTheme;
small?: boolean;
icon?: IconSvg;
rightIcon?: IconSvg;
counter?: number;
disabled?: boolean;
loading?: boolean;
}
Ние не можеме да извезуваме интерфејс од.vue
датотеки поради некои внатрешниscript setup
магија, па ние треба да се создаде посебен.ts
На светлата страна, погледнете што имаме тука:
const props = withDefaults(defineProps<MyButtonProps>(), {
theme: ComponentTheme.BLUE,
icon: undefined,
rightIcon: undefined,
counter: undefined,
});
Многу чист, нели? и еве го наследениот:
interface MyLinkButtonProps {
to?: RouteLocationRaw;
href?: string;
}
const props = defineProps<MyButtonProps & MyLinkButtonProps>();
Сепак, тука е еден проблем: сега, кога основните приклучоци се третираат какоMyLinkButton
„Пропагандата“ не се шири соv-bind=”$attrs”
Повеќе, па ние мораме да го направиме тоа сами.
<!-- MyLinkButton.vue -->
<component
:is="props.to ? RouterLink : 'a'"
:to="props.to"
:href="props.href"
class="my-link-button"
>
<MyButton v-bind="props"> <!-- there we go -->
<slot></slot>
</MyButton>
</component>
Сето тоа е добро, но ние поминуваме малку повеќе отколку што сакаме:
Како што можете да видите, сега нашиот основен копче, исто така, имаhref
Тоа не е трагедија, само малку неред и екстра бајтови, иако не е кул.
<template>
<component
:is="props.to ? RouterLink : 'a'"
:to="props.to"
:href="props.href"
class="my-link-button"
>
<MyButton v-bind="propsToPass">
<slot></slot>
</MyButton>
</component>
</template>
<script lang="ts" setup>
// imports and definitions…
const props = defineProps<MyButtonProps & MyLinkButtonProps>();
const propsToPass = computed(() =>
Object.fromEntries(
Object.entries(props).filter(([key, _]) => !["to", "href"].includes(key))
)
);
</script>
Сега, ние само го поминуваме она што мора да се помине, но сите тие буквали на струни не изгледаат неверојатно, нели?
Interfaces vs Abstract Interfaces
Интерфејси против апстрактни интерфејсиАко некогаш сте работеле со соодветни објектно-ориентирани јазици, веројатно знаете за такви работи какоРефлексија, што ни овозможува да ги добиеме метаподатоците за нашите структури. За жал, во TypeScript, интерфејсите се ефемерни; тие не постојат во текот на времето, и не можеме лесно да дознаеме кои полиња припаѓаат наMyButtonProps
.
Тоа значи дека имаме две опции.Прво, можеме да ги задржиме работите онакви какви што се: секогаш кога ќе додадемеMyLinkButton
Исто така, треба да се исклучи одpropsToPass
(И дури и ако го заборавиме тоа, тоа не е голема работа).
Вториот начин е да се користат објекти наместо интерфејси. Тоа може да звучи бесмислено, но дозволете ми да кодирам нешто: тоа нема да биде ужасно; ветувам.ТоаСтрашно е
// MyButton.props.ts
export const defaultMyButtonProps: MyButtonProps = {
theme: ComponentTheme.BLUE,
small: false,
icon: undefined,
rightIcon: undefined,
counter: undefined,
disabled: false,
loading: false,
};
Нема смисла да се создаде објект само за да се создаде објект, но можеме да го користиме за стандардни прописи.MyButton.vue
станува уште почист:
const props = withDefaults(defineProps<MyButtonProps>(), defaultMyButtonProps);
Сега само треба да ажурирамеpropsToPass
воMyLinkButton.vue
:
const propsToPass = computed(() =>
Object.fromEntries(
Object.entries(props).filter(([key, _]) =>
Object.hasOwn(defaultMyButtonProps, key)
)
)
);
За да се направи оваа работа, ние треба експлицитно да се дефинира ситеundefined
иnull
Полето воdefaultMyButtonProps
Инаку, објектот не е „вклучен“.
На овој начин, секогаш кога ќе додадете приклучок на основната компонента, исто така ќе треба да го додадете на објектот со претпоставлени вредности. Значи, да, тоа е повторно две места, и можеби тоа не е подобро од решението од претходното поглавје.
I’m Done
Јас сум направенТоа не е ремек-дело, но тоа е веројатно најдоброто што можеме да го направиме во рамките на ограничувањата на TypeScript.
Исто така, се чини дека има проп типови во SFC датотеката е подобро, но не можам да кажам дека преместувањето на нив во посебна датотека го направи многу полошо.
Можете да го најдете кодот од овој напис на GitHub.
GitHub