Нова історія

Vue.js: розповсюдження проп як про

за Andrei Sieedugin7m2025/05/03
Read on Terminal Reader

Надто довго; Читати

Коли ви створюєте компоненти для свого проекту, все починається весело і легко. Створіть «MyButton.vue» і додайте деякий стиль, і ось так. Тоді ви відразу ж усвідомлюєте, що вам потрібно десяток профілів, тому що ваша команда дизайну хоче, щоб він був різних кольорів і розмірів, з іконами зліва і праворуч, з лічильниками.
featured image - Vue.js: розповсюдження проп як про
Andrei Sieedugin HackerNoon profile picture

Коли ви створюєте компоненти для свого проекту, все починається весело і легко.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%або додаючи автофокус - ми всі знаємо, як це виглядає просто в Figma, поки реальне життя не вдарить важко.


Тепер уявіть кнопку посилання: вона виглядає однаково, але коли ви натискаєте її, ви повинні перейти до зовнішнього або внутрішнього посилання.<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">Але не буде автокомплекту для похідних пропів, що не круто:


Only "href" and "to"


Ми можемо розповсюджувати прописки тихо, але IDE нічого про них не знає, і це ганьба.


Просте і очевидно рішення полягає в тому, щоб розповсюджувати їх прямо:

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


Все добре, але ми передаємо трохи більше, ніж хочемо:


W3C disapproves


Як бачите, тепер наша підземна кнопка також має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
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

ПОВІСИТИ БИРКИ

ЦЯ СТАТТЯ БУЛА ПРЕДСТАВЛЕНА В...

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks
OSZAR »