8,553 讀數
8,553 讀數

Vue.js:像Pro一样推广优惠

经过 Andrei Sieedugin7m2025/05/03
Read on Terminal Reader

太長; 讀書

当你为你的项目创建组件时,一切都开始变得有趣和容易。创建“MyButton.vue”,添加一些风格,然后你立即意识到你需要十几个特许,因为你的设计团队希望它有不同的颜色和大小,左边和右边有图标,有计数器。毕竟,你不能有同一颜色的“取消”和“OK”按钮,你需要它们来响应用户的互动。
featured image - Vue.js:像Pro一样推广优惠
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
});


毕竟,你不能有相同颜色的“取消”和“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>标签每次,但请不要。你也可以添加tohref支持你的初始成分,但你很快就会感到窒息:

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


它将奏效. 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>


一切都很好,但我们比我们想要的多一点:


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>


现在,我们只传递了必须传递的东西,但所有这些字符串都看起来不太棒,不是吗? 这是最悲伤的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);


现在,我们只需要更新propsToPassMyLinkButton.vue:

const propsToPass = computed(() =>
  Object.fromEntries(
    Object.entries(props).filter(([key, _]) =>
      Object.hasOwn(defaultMyButtonProps, key)
    )
  )
);


为了完成这项工作,我们需要明确定义所有undefinednull场地在defaultMyButtonProps否则,对象不会“拥有”。


这样一来,每当你添加到基本组件时,你也需要将它添加到具有默认值的对象中。所以,是的,它又有两个位置,也许它不比上一章的解决方案更好。

I’m Done

我做到了

它不是一个杰作,但它可能是我们在TypeScript的局限性下所能做的最好的。


它也似乎在SFC文件中有 prop类型更好,但我不能说将它们移动到一个单独的文件使它变得更糟,但它绝对使prop重新使用更好,所以我会认为这是一个小胜利在我们称之为工作的无尽的战斗。


您可以在GitHub上找到这篇文章的代码。

吉普赛

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks