När du skapar komponenter för ditt projekt börjar allt roligt och enkelt.MyButton.vue
Lägg till lite styling och så är det.
<template>
<button class="my-fancy-style">
<slot></slot>
</button>
</template>
Då inser du omedelbart att du behöver ett dussin props, eftersom ditt designteam vill att det ska vara av olika färger och storlekar, med ikoner på vänster och höger, med räknare ...
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
});
Det är fortfarande okej; det är meningsfullt.När allt kommer omkring kan du inte ha "Avbryt" och "Ok" knappar av samma färg, och du behöver dem för att reagera på användarinteraktioner.
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
});
Tja, du får idén: det kommer att finnas något vilt, som att passerawidth: 100%
eller lägga till autofokus – vi vet alla hur enkelt det ser ut i Figma tills det verkliga livet slår hårt.
Nu föreställ dig en länkknapp: den ser likadan ut, men när du trycker på den bör du gå till den externa eller interna länken.<RouterLink>
eller<a>
taggar varje gång, men snälla inte. du kan också lägga tillto
ochhref
Props till din ursprungliga komponent, men du kommer att känna kvävning ganska snart:
<component
:is="to ? RouterLink : href ? 'a' : 'button'"
<!-- ugh! -->
Naturligtvis behöver du en "andra nivån" -komponent som omsluter din knapp (det kommer också att hantera standard hyperlänkar och några andra intressanta saker, men jag kommer att utelämna dem för enkelhetens skull):
<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>
Och det är där vår historia börjar.
Square One
torget ettTja, i grund och botten kommer det att fungera, jag kommer inte att ljuga. du kan fortfarande skriva<MyLinkButton :counter=“2">
Men det kommer inte att finnas någon autokomplett för de härledda propparna, vilket inte är coolt:
Vi kan sprida props tyst, men IDE vet inte något om dem, och det är synd.
Den enkla och uppenbara lösningen är att sprida dem uttryckligen:
<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>
Det kommer att fungera. IDE kommer att ha rätt autokomplete. Vi kommer att ha mycket smärta och ångra att stödja det.
Självklart tillämpas inte principen "Gör inte upprepning" här, vilket innebär att vi måste synkronisera varje uppdatering. En dag måste du lägga till en annan prop, och du måste hitta varje komponent som omsluter den grundläggande. Ja, Knapp och LinkButton är förmodligen tillräckligt, men föreställ dig TextInput och ett dussin komponenter som är beroende av det: PasswordInput, EmailInput, NumberInput, DateInput, HellKnowsWhatElseInput.
När allt kommer omkring är det fult. Och ju fler props vi har, desto fult blir det.
Clean It Up
Städa upp detDet är ganska svårt att återanvända en anonym typ, så låt oss ge den ett namn.
// MyButton.props.ts
export interface MyButtonProps {
theme?: ComponentTheme;
small?: boolean;
icon?: IconSvg;
rightIcon?: IconSvg;
counter?: number;
disabled?: boolean;
loading?: boolean;
}
Vi kan inte exportera ett gränssnitt från.vue
filen på grund av vissa internascript setup
magiska, så vi måste skapa en separat.ts
På den ljusa sidan, se vad vi har här:
const props = withDefaults(defineProps<MyButtonProps>(), {
theme: ComponentTheme.BLUE,
icon: undefined,
rightIcon: undefined,
counter: undefined,
});
Mycket renare, är det inte? och här är den ärvda:
interface MyLinkButtonProps {
to?: RouteLocationRaw;
href?: string;
}
const props = defineProps<MyButtonProps & MyLinkButtonProps>();
Men här är ett problem: nu, när grundläggande proppar behandlas somMyLinkButton
”s props, de sprids inte medv-bind=”$attrs”
längre, så vi måste göra det själva.
<!-- 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>
Det är allt bra, men vi passerar lite mer än vi vill:
Som ni kan se, nu har vår underliggande knapp också enhref
Det är inte en tragedi, bara lite röran och extra bytes, även om det inte är coolt.
<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>
Nu passerar vi bara det som måste passeras, men alla de strängbokstäverna ser inte fantastiska ut, gör de?
Interfaces vs Abstract Interfaces
Gränssnitt vs abstrakta gränssnittOm du någonsin har arbetat med lämpliga objektorienterade språk vet du förmodligen om sådana saker somReflektionerTyvärr, i TypeScript, gränssnitt är ephemeral; de existerar inte vid körtid, och vi kan inte lätt ta reda på vilka fält som tillhörMyButtonProps
.
Det betyder att vi har två alternativ. För det första kan vi hålla saker och ting som de är: varje gång vi lägger till enMyLinkButton
Vi måste också utesluta den frånpropsToPass
(Och även om vi glömmer bort det, är det inte en stor sak).
Det kan låta meningslöst, men låt mig koda något: det kommer inte att vara hemskt, jag lovar.Det därförskräckligt .
// MyButton.props.ts
export const defaultMyButtonProps: MyButtonProps = {
theme: ComponentTheme.BLUE,
small: false,
icon: undefined,
rightIcon: undefined,
counter: undefined,
disabled: false,
loading: false,
};
Det är inte meningsfullt att skapa ett objekt bara för att skapa ett objekt, men vi kan använda det för standardprops.MyButton.vue
Det blir ännu renare:
const props = withDefaults(defineProps<MyButtonProps>(), defaultMyButtonProps);
Nu behöver vi bara uppdaterapropsToPass
iMyLinkButton.vue
:
const propsToPass = computed(() =>
Object.fromEntries(
Object.entries(props).filter(([key, _]) =>
Object.hasOwn(defaultMyButtonProps, key)
)
)
);
För att göra detta arbete måste vi uttryckligen definiera allaundefined
ochnull
fält idefaultMyButtonProps
Annars ”har” objektet inte sitt eget.
På så sätt, när du lägger till en prop till den grundläggande komponenten, måste du också lägga till den till objektet med standardvärden. Så, ja, det är två platser igen, och kanske är det inte bättre än lösningen från föregående kapitel.
I’m Done
Jag är gjordDet är inte ett mästerverk, men det är förmodligen det bästa vi kan göra inom TypeScript begränsningar.
Det verkar också som att ha prop typer inuti SFC-filen är bättre, men jag kan inte säga att flytta dem till en separat fil gjorde det mycket värre.
Du kan hitta koden från den här artikeln på GitHub.
GitHub