Ketika Anda membuat komponen untuk proyek Anda, semuanya dimulai dengan menyenangkan dan mudah.MyButton.vue
Tambahkan sedikit styling, dan inilah.
<template>
<button class="my-fancy-style">
<slot></slot>
</button>
</template>
Kemudian Anda segera menyadari bahwa Anda membutuhkan selusin props, karena tim desain Anda ingin itu memiliki warna dan ukuran yang berbeda, dengan ikon di kiri dan kanan, dengan counter...
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
});
Setelah semua, Anda tidak dapat memiliki tombol “Batalkan” dan “Ok” warna yang sama, dan Anda membutuhkannya untuk bereaksi terhadap interaksi pengguna.
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
});
Nah, Anda mendapatkan ide: akan ada sesuatu yang liar, seperti melewatiwidth: 100%
atau menambahkan autofocus - kita semua tahu bagaimana terlihat sederhana di Figma sampai kehidupan nyata memukul keras.
Sekarang bayangkan tombol tautan: terlihat sama, tetapi ketika Anda menekannya, Anda harus pergi ke tautan eksternal atau internal.<RouterLink>
atau<a>
tag setiap kali, tapi tolong jangan. Anda juga dapat menambahkanto
danhref
props ke komponen awal Anda, tetapi Anda akan merasa tersedak cukup cepat:
<component
:is="to ? RouterLink : href ? 'a' : 'button'"
<!-- ugh! -->
Tentu saja, Anda akan membutuhkan komponen "tingkat kedua" yang membungkus tombol Anda (itu juga akan menangani ikhtisar hyperlink default dan beberapa hal menarik lainnya, tetapi saya akan melewatkan mereka demi kesederhanaan):
<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>
Di sinilah kisah kita dimulai.
Square One
Pantai SatuNah, pada dasarnya itu akan bekerja, saya tidak akan berbohong. Anda masih bisa mengetik<MyLinkButton :counter=“2">
Tapi tidak akan ada autocomplete untuk props yang berasal, yang tidak keren:
Kita dapat menyebarkan props diam-diam, tetapi IDE tidak tahu apa-apa tentang mereka, dan itu malu.
Solusi yang sederhana dan jelas adalah menyebarkannya secara eksplisit:
<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>
Ini akan bekerja. IDE akan memiliki autocomplete yang tepat. Kami akan memiliki banyak rasa sakit dan menyesal mendukungnya.
Jelas, prinsip “Jangan Ulangi Diri Sendiri” tidak diterapkan di sini, yang berarti bahwa kita akan perlu menyinkronkan setiap pembaruan. Suatu hari, Anda akan perlu menambahkan prop lain, dan Anda harus menemukan setiap komponen yang membungkus komponen dasar. Ya, Button dan LinkButton mungkin cukup, tetapi bayangkan TextInput dan selusin komponen yang bergantung padanya: PasswordInput, EmailInput, NumberInput, DateInput, HellKnowsWhatElseInput. Menambahkan prop tidak harus menyebabkan penderitaan.
Setelah semua, itu jelek. dan semakin banyak props yang kita miliki, semakin jelek itu menjadi.
Clean It Up
Bersihkan ke atasSangat sulit untuk menggunakan kembali jenis anonim, jadi mari kita berikan namanya.
// MyButton.props.ts
export interface MyButtonProps {
theme?: ComponentTheme;
small?: boolean;
icon?: IconSvg;
rightIcon?: IconSvg;
counter?: number;
disabled?: boolean;
loading?: boolean;
}
Kami tidak dapat mengekspor antarmuka dari.vue
Berdasarkan beberapa faktor internalscript setup
keajaiban, maka kita perlu membuat.ts
Di sisi yang cerah, lihat apa yang kami dapatkan di sini:
const props = withDefaults(defineProps<MyButtonProps>(), {
theme: ComponentTheme.BLUE,
icon: undefined,
rightIcon: undefined,
counter: undefined,
});
Terlalu bersih, bukan? dan inilah yang diwariskan:
interface MyLinkButtonProps {
to?: RouteLocationRaw;
href?: string;
}
const props = defineProps<MyButtonProps & MyLinkButtonProps>();
Namun, di sini ada masalah: sekarang, ketika props dasar diperlakukan sebagaiMyLinkButton
‘s props, mereka tidak disebarkan denganv-bind=”$attrs”
lebih, jadi kita harus melakukannya sendiri.
<!-- 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>
Itu semua baik-baik saja, tetapi kita melewatkan sedikit lebih dari yang kita inginkan:
Seperti yang Anda lihat, sekarang tombol bawah kami juga memilikihref
Ini bukan tragedi, hanya sedikit kekacauan dan byte ekstra, meskipun tidak keren.
<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>
Sekarang, kita hanya melewati apa yang harus diturunkan, tetapi semua huruf string itu tidak terlihat hebat, apakah mereka? dan itu adalah cerita TypeScript yang paling sedih, guys.
Interfaces vs Abstract Interfaces
Interface vs Interface abstrakJika Anda pernah bekerja dengan bahasa berorientasi objek yang tepat, Anda mungkin tahu tentang hal-hal sepertiRefleksiSayangnya, di TypeScript, antarmuka efemeral; mereka tidak ada pada runtime, dan kita tidak dapat dengan mudah mencari tahu bidang mana yang termasuk dalamMyButtonProps
.
Ini berarti bahwa kita memiliki dua pilihan. pertama, kita dapat menjaga hal-hal seperti itu: setiap kali kita menambahkan propMyLinkButton
Kita juga harus mengecualikan daripropsToPass
(Dan bahkan jika kita melupakannya, itu bukan masalah besar).
Cara kedua adalah menggunakan objek alih-alih antarmuka. mungkin terdengar tidak masuk akal, tetapi biarkan saya mengkode sesuatu: itu tidak akan mengerikan; saya berjanji.yangyang mengerikan.
// MyButton.props.ts
export const defaultMyButtonProps: MyButtonProps = {
theme: ComponentTheme.BLUE,
small: false,
icon: undefined,
rightIcon: undefined,
counter: undefined,
disabled: false,
loading: false,
};
Tidak masuk akal untuk membuat objek hanya untuk membuat sebuah objek, tetapi kita dapat menggunakannya untuk props default.MyButton.vue
menjadi lebih bersih:
const props = withDefaults(defineProps<MyButtonProps>(), defaultMyButtonProps);
Sekarang, kita hanya perlu memperbaruipropsToPass
dalamMyLinkButton.vue
:
const propsToPass = computed(() =>
Object.fromEntries(
Object.entries(props).filter(([key, _]) =>
Object.hasOwn(defaultMyButtonProps, key)
)
)
);
Untuk melakukan pekerjaan ini, kita perlu secara eksplisit menentukan semuaundefined
dannull
Bidang didefaultMyButtonProps
Jika tidak, maka objek tidak akan “memiliki”.
Dengan cara ini, setiap kali Anda menambahkan prop ke komponen dasar, Anda juga harus menambahkannya ke objek dengan nilai default. Jadi, ya, itu dua tempat lagi, dan mungkin tidak lebih baik daripada solusi dari bab sebelumnya.
I’m Done
Aku sudah selesaiIni bukan karya utama, tetapi itu mungkin yang terbaik yang dapat kita lakukan dalam keterbatasan TypeScript.
Tampaknya juga bahwa memiliki jenis prop di dalam file SFC lebih baik, tetapi saya tidak bisa mengatakan bahwa memindahkan mereka ke file terpisah membuatnya jauh lebih buruk.
Anda dapat menemukan kode dari artikel ini di GitHub.
GitHub