当你为你的项目创建组件时,一切都开始有趣和容易。 再加上一些风格,就这样了。 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 }); 好吧,你得到了这个想法:会有一些野蛮的东西,比如过去。 或者添加自动焦点 - 我们都知道在Figma中看起来很简单,直到现实生活变得艰难。 width: 100% 现在想象一下链接按钮:它看起来相同,但当你按下它时,你应该去外部或内部链接。 或 标签每次,但请不要。你也可以添加 和 支持你的初始成分,但你很快就会感到窒息: <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>(); 然而,这里有一个问题:现在,当基本的补丁被视为 ’s props,他们不被传播与 更重要的是,我们必须自己去做。 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> 一切都很好,但我们比我们想要的多一点: 正如你所看到的,现在我们的底层按钮也有一个 它不是一个悲剧,只是有点混乱和额外的字节,虽然不是很酷。 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上找到这篇文章的代码。 吉普赛