8,559 avläsningar
8,559 avläsningar

Vue.js: Sprid Props som en Pro

förbi Andrei Sieedugin7m2025/05/03
Read on Terminal Reader

För länge; Att läsa

När du skapar komponenter för ditt projekt börjar allt roligt och enkelt. Skapa 'MyButton.vue' och lägg till lite styling, och voilà. 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 till vänster och höger, med räknare. 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.
featured image - Vue.js: Sprid Props som en Pro
Andrei Sieedugin HackerNoon profile picture

När du skapar komponenter för ditt projekt börjar allt roligt och enkelt.MyButton.vueLä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 tilltoochhrefProps 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 ett

Tja, 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:


Only "href" and "to"


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 det

Det ä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.vuefilen på grund av vissa internascript setupmagiska, så vi måste skapa en separat.tsPå 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:


W3C disapproves


Som ni kan se, nu har vår underliggande knapp också enhrefDet ä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änssnitt

Om 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 enMyLinkButtonVi 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.vueDet blir ännu renare:

const props = withDefaults(defineProps<MyButtonProps>(), defaultMyButtonProps);


Nu behöver vi bara uppdaterapropsToPassiMyLinkButton.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 allaundefinedochnullfält idefaultMyButtonPropsAnnars ”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 gjord

Det ä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
L O A D I N G
. . . comments & more!

About Author

Andrei Sieedugin HackerNoon profile picture
Andrei Sieedugin@smileek
Senior frontend developer with product management experience

HÄNG TAGGAR

DENNA ARTIKEL PRESENTERAS I...

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks