当你为你的项目创建组件时,一切都开始有趣和容易。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! -->
当然,你需要一个“二级”组件来包裹你的按钮(它还会处理默认的超链接概述和其他一些有趣的事情,但为了简单,我会忽略它们):
<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">
,而且会很好,但不会有衍生补丁的自动补充,这并不酷:
我们可以默默地传播代理,但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 将有适当的自动完成. 我们将有很大的痛苦和遗憾支持它。
显然,“不要重复自己”的原则在这里没有应用,这意味着我们将需要同步每个更新。有一天,你需要添加另一个支持,你将不得不找到包裹基本的每个组件。
毕竟,它是丑陋的,我们拥有的副本越多,它就越丑陋。
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>
现在,我们只传递了必须传递的东西,但所有这些字符串都看起来不太棒,不是吗? 这是最悲伤的TypeScript故事,伙计们。
Interfaces vs Abstract Interfaces
接口 vs 抽象接口如果你曾经使用过适当的面向对象的语言,那么你可能知道诸如反思遗憾的是,在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文件中有 prop类型更好,但我不能说将它们移动到一个单独的文件使它变得更糟,但它绝对使prop重新使用更好,所以我会认为这是一个小胜利在我们称之为工作的无尽的战斗。
您可以在GitHub上找到这篇文章的代码。
吉普赛