כאשר אתה יוצר רכיבים עבור הפרויקט שלך, הכל מתחיל כיף וקל.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
});
זה עדיין בסדר; זה הגיוני. אחרי הכל, אתה לא יכול לקבל את הכפתורים "בטל" ו "OK" באותו צבע, ואתה צריך אותם כדי להגיב אינטראקציות משתמשים.
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! -->
כמובן, תזדקק לרכיב "רמה שנייה" אשר מכסה את הכפתור שלך (זה גם יטפל בהקצאות היפר-קישור default וכמה דברים מעניינים אחרים, אבל אני אשחרר אותם למען הפשטות):
<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">
אבל לא יהיה autocomplete עבור תוספים נגזרים, אשר לא מגניב:
אנחנו יכולים להפיץ תרומות בשקט, אבל 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>
זה יעבוד. IDE יהיה autocomplete מתאים. יהיה לנו הרבה כאב ומצטער לתמוך בו.
ברור, העיקרון של "אל תחזור על עצמך" לא היה מיושם כאן, מה שאומר שאנחנו נצטרך לסנכרן כל עדכון. יום אחד, תצטרך להוסיף תמיכה נוספת, ותצטרך למצוא כל רכיב אשר מכסה את הבסיס. כן, כפתור ו- 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
's props, הם לא מתפשטים עם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
תגיות abstract interfacesאם אי פעם עבדת עם שפות אוריינטציה אובייקטים מתאימות, אתה כנראה יודע על דברים כגון:השתקפותלמרבה הצער, ב-TypeScript, ממשקים הם אפשריים; הם אינם קיימים בזמן ביצועים, ואנחנו לא יכולים בקלות לגלות אילו שדות שייכים ל-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.
זה גם נראה כי יש סוגים prop בתוך קובץ SFC הוא טוב יותר, אבל אני לא יכול לומר כי העברת אותם לקובץ נפרד גרמה לכך הרבה יותר גרוע.
אתה יכול למצוא את הקוד מאמר זה על GitHub.
Github