8,553 letture
8,553 letture

Vue.js: Propagando Props come un Pro

di Andrei Sieedugin7m2025/05/03
Read on Terminal Reader

Troppo lungo; Leggere

Quando crei componenti per il tuo progetto, tutto inizia divertente e facile. Crea 'MyButton.vue' e aggiungi qualche stile, e ecco. Allora ti rendi subito conto che hai bisogno di una dozzina di props, perché il tuo team di progettazione vuole che siano di colori e dimensioni diversi, con icone a sinistra e a destra, con contatori. Dopo tutto, non puoi avere i pulsanti "Cancel" e "Ok" dello stesso colore, e hai bisogno di loro per reagire alle interazioni utente.
featured image - Vue.js: Propagando Props come un Pro
Andrei Sieedugin HackerNoon profile picture

Quando crei componenti per il tuo progetto, tutto inizia divertente e facile.MyButton.vueAggiungi un po’ di stile e ecco.

<template>
  <button class="my-fancy-style">
    <slot></slot>
  </button>
</template>


Poi ti rendi subito conto che hai bisogno di una dozzina di props, perché il tuo team di progettazione vuole che siano di diversi colori e dimensioni, con icone a sinistra e a destra, con contatori...

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
});


Dopo tutto, non puoi avere i pulsanti “Cancel” e “Ok” dello stesso colore, e hai bisogno di loro per reagire alle interazioni degli utenti.

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
});


Bene, hai l'idea: ci sarà qualcosa di selvaggio, come passarewidth: 100%o aggiungendo l'autofocus - tutti sappiamo come sembra semplice in Figma fino a quando la vita reale colpisce duramente.


Ora immaginate un pulsante di collegamento: sembra lo stesso, ma quando lo premete, dovreste andare al link esterno o interno.<RouterLink>o<a>tag ogni volta, ma per favore non. Puoi anche aggiungeretoehrefi vantaggi per la tua componente iniziale, ma ti sentirai soffocato abbastanza presto:

<component
  :is="to ? RouterLink : href ? 'a' : 'button'"
  <!-- ugh! -->


Naturalmente, avrai bisogno di un componente di "secondo livello" che avvolga il tuo pulsante (trattare anche i contorni di collegamento ipertestuale predefiniti e alcune altre cose interessanti, ma li ometterò per semplicità):

<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>

Ed è qui che inizia la nostra storia.

Square One

Piazza Uno

Bene, fondamentalmente funziona, non mentirò. puoi ancora digitare<MyLinkButton :counter=“2">Ma non ci sarà alcun autocomplete per i props derivati, che non è cool:


Only "href" and "to"


Possiamo propagare i props in silenzio, ma l’IDE non sa nulla di loro, e questo è un peccato.


La soluzione semplice e ovvia è propagarla esplicitamente:

<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>


L'IDE avrà l'autocomplete appropriato. Avremo un sacco di dolore e rimpianti di sostenerlo.


Ovviamente, il principio "Non ripetere te stesso" non è stato applicato qui, il che significa che dovremo sincronizzare ogni aggiornamento. Un giorno, dovrai aggiungere un altro prop, e dovrai trovare ogni componente che avvolge il componente di base. Sì, Button e LinkButton sono probabilmente sufficienti, ma immaginate TextInput e una dozzina di componenti che dipendono da esso: PasswordInput, EmailInput, NumberInput, DateInput, HellKnowsWhatElseInput.


Dopo tutto, è brutta. E più props abbiamo, più brutta diventa.

Clean It Up

pulire in alto

È abbastanza difficile riutilizzare un tipo anonimo, quindi diamo un nome.

// MyButton.props.ts

export interface MyButtonProps {
  theme?: ComponentTheme;
  small?: boolean;
  icon?: IconSvg;
  rightIcon?: IconSvg;
  counter?: number;
  disabled?: boolean;
  loading?: boolean;
}


Non è possibile esportare un'interfaccia dal.vuefile a causa di alcuni interniscript setupmagia, quindi dobbiamo creare una.tsSul lato luminoso, guarda cosa abbiamo qui:

const props = withDefaults(defineProps<MyButtonProps>(), {
  theme: ComponentTheme.BLUE,
  icon: undefined,
  rightIcon: undefined,
  counter: undefined,
});


Molto più pulito, non è vero?Ecco quello ereditato:

interface MyLinkButtonProps {
  to?: RouteLocationRaw;
  href?: string;
}

const props = defineProps<MyButtonProps & MyLinkButtonProps>();


Tuttavia, ecco un problema: ora, quando i props di base sono trattati comeMyLinkButton“I profili, non sono propagati conv-bind=”$attrs”Ancora, quindi dobbiamo farlo noi stessi.

<!-- 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>


Tutto bene, ma passiamo un po' di più di quanto vogliamo:


W3C disapproves


Come potete vedere, ora il nostro pulsante sottostante ha anche unhrefNon è una tragedia, solo un po 'di confusione e byte extra, anche se non cool.

<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>


Ora, passiamo solo ciò che deve essere passato, ma tutte quelle righe letterali non sembrano fantastiche, no?

Interfaces vs Abstract Interfaces

Interfacce versus interfacce astratte

Se hai mai lavorato con linguaggi orientati agli oggetti, probabilmente conosci cose come:riflessioneSfortunatamente, in TypeScript, le interfacce sono effimere; non esistono al tempo di esecuzione, e non possiamo facilmente scoprire quali campi appartengono allaMyButtonProps.


Ciò significa che abbiamo due opzioni.In primo luogo, possiamo mantenere le cose come sono: ogni volta che aggiungiamo unMyLinkButtonBisogna anche escludere dapropsToPass(e anche se ci dimentichiamo di questo, non è una grande cosa).


Il secondo modo è quello di usare oggetti invece di interfacce. Può sembrare insensato, ma lascia che io codifichi qualcosa: non sarà orribile; prometto.QuestoIl terribile.

// MyButton.props.ts

export const defaultMyButtonProps: MyButtonProps = {
  theme: ComponentTheme.BLUE,
  small: false,
  icon: undefined,
  rightIcon: undefined,
  counter: undefined,
  disabled: false,
  loading: false,
};


Non ha senso creare un oggetto solo per creare un oggetto, ma possiamo usarlo per i props predefiniti.MyButton.vuediventa ancora più pulito:

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


Ora abbiamo solo bisogno di aggiornarepropsToPassinMyLinkButton.vue:

const propsToPass = computed(() =>
  Object.fromEntries(
    Object.entries(props).filter(([key, _]) =>
      Object.hasOwn(defaultMyButtonProps, key)
    )
  )
);


Per fare questo lavoro, dobbiamo definire esplicitamente tutti iundefinedenullI campi indefaultMyButtonPropsAltrimenti, l’oggetto non “haOwn”.


In questo modo, ogni volta che si aggiunge un prop al componente di base, sarà anche necessario aggiungerlo all'oggetto con i valori predefiniti. Così, sì, è di nuovo in due luoghi, e forse non è migliore della soluzione del capitolo precedente.

I’m Done

Sono fatto

Non è un capolavoro, ma è probabilmente il meglio che possiamo fare entro i limiti di TypeScript.


Sembra anche che avere i tipi di prop all'interno del file SFC sia meglio, ma non posso dire che spostarli in un file separato lo abbia reso molto peggio.Ma ha sicuramente reso il riutilizzo del prop migliore, quindi lo considererò una piccola vittoria in una battaglia infinita che chiamiamo lavoro.


Puoi trovare il codice di questo articolo su GitHub.

di Github

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks