vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue可重用组件

一文详解Vue中可重用组件的3个主要问题

作者:王大冶

当我们谈论或讨论在 Vue 中创建用户界面组件时,经常会提到可重用性,在 Vue 中创建真正的可重用组件可能很棘手,在本文中,我将探讨可重用组件的概念、应用这些组件时面临的问题,以及为什么必须尽可能克服这些问题,需要的朋友可以参考下

当我们谈论或讨论在 Vue 中创建用户界面组件时,经常会提到可重用性。没错,Vue 的关键原则之一就是其基于组件的架构,这促进了可重用性和模块化。但这到底意味着什么呢?

比方说,你创建了一个可重复使用的组件:

在 Vue 中创建真正的可重用组件可能很棘手。在本文中,我将探讨可重用组件的概念、应用这些组件时面临的问题,以及为什么必须尽可能克服这些问题。

什么是可重用组件?

可重用组件是用户界面构件,可用于应用程序的不同部分,甚至多个项目。它们封装了特定的功能或用户界面模式,可以轻松集成到应用程序的其他部分,而无需进行大量修改。

可重复使用组件的优势

应用可重复使用概念时的 3 个问题

虽然可重用性是 Vue. 组件的一个理想特性,但有几个问题会使其难以实现:

案例

比方说,客户想要一个内部员工目录系统。该项目采用敏捷方法,开发前无法收集所有需求。项目分为三个阶段(原型、第一阶段和第二阶段)。在本演示中,我将重点介绍一个卡片组件,如下所示:

原型

作为原型阶段的一部分,我需要提供一个用户配置文件页面。用户配置文件将包含一个基本的用户卡组件,其中包括用户头像和姓名。

// Prototype.vue
<script setup lang="ts">
    import { defineProps, computed, Teleport, ref } from "vue";
    interface Props {
        firstName: string;
        lastName: string;
        image?: string;
    }
    const props = defineProps<Props>();
</script>
<template>
    <div class="app-card">
        <img
            class="user-image"
            :src="image"
            alt="avatar" />
        <div>
            <div>
                <label> {{ firstName }} {{ lastName }} </label>
            </div>
        </div>
    </div>
</template>
<style scoped>
    .app-card {
        padding-left: 10px;
        padding-right: 10px;
        padding-top: 5px;
        padding-bottom: 5px;
        background: white;
        box-shadow: 0 0 5px;
        border-radius: 5px;
        border: none;
        font-size: 1.5em;
        transition: 0.3s;
        display: flex;
        align-items: center;
    }
    .app-card label {
        font-weight: 600;
    }
    .app-card:hover {
        background: rgba(128, 128, 128, 0.5);
    }
    .user-image {
        width: 100px;
    }
</style>

第 1 阶段

在第一阶段,客户希望在用户卡组件中添加用户详细信息(生日、年龄、电话号码和电子邮件)。

//Phase1.vue
<script setup lang="ts">
    import { defineProps, computed } from "vue";
    interface Props {
        firstName: string;
        lastName: string;
        image?: string;
        birthDay?: string;
        phone?: string;
        email?: string;
    }
    const props = defineProps<Props>();
    const age = computed(() => {
        if (!props.birthDay) {
            return "0";
        }
        const birthYear = new Date(props.birthDay).getFullYear();
        const currentYear = new Date().getFullYear();
        return currentYear - birthYear;
    });
</script>
<template>
    <div
        ref="cardRef"
        class="app-card">
        <img
            class="user-image"
            :src="image"
            alt="avatar" />
        <div>
            <div>
                <label> {{ firstName }} {{ lastName }} </label>
            </div>
            <div>
                <div>
                    <label> Birth day: </label>
                    <span>
                        {{ birthDay }}
                    </span>
                </div>
                <div>
                    <label> Age: </label>
                    <span>
                        {{ age }}
                    </span>
                </div>
                <div>
                    <label> Phone number: </label>
                    <span>
                        {{ phone }}
                    </span>
                </div>
                <div>
                    <label> Email: </label>
                    <span>
                        {{ email }}
                    </span>
                </div>
            </div>
        </div>
    </div>
</template>
<style scoped>
    .app-card {
        padding-left: 10px;
        padding-right: 10px;
        padding-top: 5px;
        padding-bottom: 5px;
        background: white;
        box-shadow: 0 0 5px;
        border-radius: 5px;
        border: none;
        font-size: 1.5em;
        transition: 0.3s;
        display: flex;
        align-items: center;
    }
    .app-card label {
        font-weight: 600;
    }
    .app-card:hover {
        background: rgba(128, 128, 128, 0.5);
        color: black;
    }
    .user-image {
        width: 100px;
    }
</style>

此外,客户还希望添加员工名录页面,并以卡片格式显示用户资料。

// SearchPage
<template>
    <div>
        <SearchInput v-model:value="searchValue" />
        <template
            :key="item.id"
            v-for="item of list">
            <div style="margin-bottom: 5px; margin-top: 5px">
                <UserCard v-bind="item" />
            </div>
        </template>
    </div>
</template>
<script lang="ts">
    import SearchInput from "../components/SearchInput.vue";
    import UserCard from "../components/Phase1.vue";
    import { ref, watch } from "vue";
    export default {
        name: "Search",
        components: {
            SearchInput,
            UserCard,
        },
        setup() {
            const searchValue = ref<string>();
            const list = ref();
            fetch("https://dummyjson.com/users")
                .then((res) => res.json())
                .then((res) => (list.value = res.users));
            watch(searchValue, (v) => {
                fetch(`https://dummyjson.com/users/search?q=${v}`)
                    .then((res) => res.json())
                    .then((res) => (list.value = res.users));
            });
            watch(list, (v) => console.log(v));
            return {
                searchValue,
                list,
            };
        },
    };
</script>

在这一阶段,用户卡组件可在两个页面上重复使用。

第二阶段

用户反馈 "员工名录 "页面杂乱无章。过多的信息使页面难以使用。因此,客户希望在鼠标悬停时以工具提示的形式显示用户详细信息。用户设置页面的要求保持不变。

// Phase 2
<script setup lang="ts">
import {
  defineProps,
  computed,
  Teleport,
  ref,
  onMounted,
  onBeforeUnmount,
} from "vue";
interface Props {
  firstName: string;
  lastName: string;
  image?: string;
  birthDate?: string;
  phone?: string;
  email?: string;
  address?: string;
}
const props = defineProps<Props>();
const targetRef = ref<HTMLDiveElement>();
const isMouseOver = ref(false);
const dropdownRef = ref<HTMLDivElement>();
const dropdownStyle = ref({});
// add modal element in body to prevent overflow issue
const modalElement = document.createElement("div");
modalElement.id = "modal";
document.body.appendChild(modalElement);
const age = computed(() => {
  if (!props.birthDate) {
    return "0";
  }
  const birthYear = new Date(props.birthDate).getFullYear();
  const currentYear = new Date().getFullYear();
  return currentYear - birthYear;
});
const onMouseOver = () => {
  if (isMouseOver.value) {
    return;
  }
  isMouseOver.value = true;
  const dimension = targetRef.value.getBoundingClientRect();
  dropdownStyle.value = {
    width: `${dimension.width}px`,
    left: `${dimension.x}px`,
    top: `${window.scrollY + dimension.y + dimension.height + 5}px`,
  };
};
const onMouseLeave = () => {
  isMouseOver.value = false;
};
</script>
<template>
  <div
    ref="targetRef"
    @mouseover="onMouseOver"
    @mouseleave="onMouseLeave"
    class="app-card"
  >
    <img class="user-image" :src="image" alt="avatar" />
    <div>
      <div>
        <label> {{ firstName }} {{ lastName }} </label>
      </div>
    </div>
  </div>
  <Teleport to="#modal">
    <div
      ref="dropdownRef"
      :style="dropdownStyle"
      style="position: absolute"
      v-show="isMouseOver"
    >
      <div class="app-card">
        <div>
          <div>
            <label> Birth day: </label>
            <span>
              {{ birthDate }}
            </span>
          </div>
          <div>
            <label> Age: </label>
            <span>
              {{ age }}
            </span>
          </div>
          <div>
            <label> Phone number: </label>
            <span>
              {{ phone }}
            </span>
          </div>
          <div>
            <label> Email: </label>
            <span>
              {{ email }}
            </span>
          </div>
        </div>
      </div>
    </div>
  </Teleport>
</template>
<style scoped>
.app-card {
  padding-left: 10px;
  padding-right: 10px;
  padding-top: 5px;
  padding-bottom: 5px;
  background: white;
  box-shadow: 0 0 5px;
  border-radius: 5px;
  border: none;
  font-size: 1.5em;
  transition: 0.3s;
  display: flex;
  align-items: center;
}
.app-card label {
  font-weight: 600;
}
.app-card:hover {
  background: rgba(128, 128, 128, 0.5);
  color: black;
}
.user-image {
  width: 100px;
}
</style>

这项新要求令人头疼:

因为我们不想破坏已经投入生产的项目,所以我们倾向于选择后一种方案。起初,这可能是有道理的,但它可能会造成相当大的损害,尤其是对于大型和连续性项目而言:

如何克服上述问题

在整个项目中,可重复使用的组件可能不会始终保持不变,这一点要有心理准备。这听起来可能很老套,但仔细想想,需求总是在不断变化的。你无法控制未来,只能尽力而为。当然,经验会帮助你设计出更好的组件,但这需要时间

重构可重用组件

根据我的经验,我将重新设计和重构可重用的组件。重构是一个在不改变代码原有功能的前提下重组代码的过程。我相信重构的方法有很多,对我来说,我会重构并将组件分解成更小的组件。较小的组件可以在整个系统中灵活应用。让我们看看我将如何应用上述案例研究。

注意:重构用户界面组件需要严谨的态度。此外,有时这也很具有挑战性,因为需要在项目交付期限和更简洁的代码之间取得平衡。

将解决方案应用于案例研究

首先,我将把现有的用户卡组件拆分成 4 个组件:

** Card.vue**

// Card.vue
<template>
    <div class="app-card">
        <slot></slot>
    </div>
</template>
<style scoped>
    .app-card {
        padding-left: 15px;
        padding-right: 15px;
        padding-top: 10px;
        padding-bottom: 10px;
        border-radius: 5px;
        border: none;
        background: white;
        color: black;
        font-size: 1.5em;
        transition: 0.3s;
        display: flex;
        align-items: center;
        box-shadow: 0 0 5px;
    }
    .app-card:hover {
        background: rgba(128, 128, 128, 0.5);
        color: black;
    }
</style>

Avatar.vue

// Avatar.vue
<script setup lang="ts">
    import { defineProps } from "vue";
    interface Props {
        image: string;
    }
    const props = defineProps<Props>();
</script>
<template>
    <img
        class="user-image"
        :src="image"
        alt="avatar" />
</template>
<style scoped>
    .user-image {
        width: 100px;
    }
</style>

UserName.vue

// UserName.vue
<script setup lang="ts">
    import { defineProps } from "vue";
    interface Props {
        firstName: string;
        lastName: string;
    }
    const props = defineProps<Props>();
</script>
<template>
    <label> {{ firstName }} {{ lastName }} </label>
</template>

Description Item

efineProps } from "vue";
    interface Props {
        label: string;
        value: string | number;
    }
    const props = defineProps<Props>();
</script>
<template>
    <div>
        <label> {{ label }}: </label>
        <span>
            {{ value }}
        </span>
    </div>
</template>
<style scoped>
    label {
        font-weight: 600;
    }
</style>

UserDescription.vue

// UserDescription.vue
<script setup lang="ts">
    import DescriptionItem from "./DescriptionItem.vue";
    import { defineProps, computed } from "vue";
    interface Props {
        birthDate: string;
        phone: string;
        email: string;
    }
    const props = defineProps<Props>();
    const age = computed(() => {
        if (!props.birthDate) {
            return "0";
        }
        const birthYear = new Date(props.birthDate).getFullYear();
        const currentYear = new Date().getFullYear();
        return currentYear - birthYear;
    });
</script>
<template>
    <div>
        <DescriptionItem
            label="Birth day"
            :value="birthDate" />
        <DescriptionItem
            label="Age"
            :value="age" />
        <DescriptionItem
            label="Phone number"
            :value="phone" />
        <DescriptionItem
            label="Email"
            :value="email" />
    </div>
</template>

之后,我将创建一个工具提示组件。创建一个单独的工具提示可以让我在系统的其他部分重复使用它。

Tooltip.vue

// Tooltip.vue
<script setup lang="ts">
import {
  Teleport,
  computed,
  ref,
  onMounted,
  onBeforeUnmount,
  watch,
} from "vue";
const isMouseOver = ref(false);
const targetRef = ref<HTMLDivElement>();
const dropdownStyle = ref({});
const dropdownRef = ref<HTMLDivElement>();
const existModalElement = document.getElementById("modal");
if (!existModalElement) {
  // add modal element in body to prevent overflow issue
  const modalElement = document.createElement("div");
  modalElement.id = "modal";
  document.body.appendChild(modalElement);
}
const onMouseOver = () => {
  if (isMouseOver.value) {
    return;
  }
  isMouseOver.value = true;
  const dimension = targetRef.value.getBoundingClientRect();
  dropdownStyle.value = {
    width: `${dimension.width}px`,
    left: `${dimension.x}px`,
    top: `${window.scrollY + dimension.y + dimension.height + 5}px`,
  };
};
const onMouseLeave = () => {
  isMouseOver.value = false;
};
</script>
<template>
  <div @mouseover="onMouseOver" @mouseleave="onMouseLeave" ref="targetRef">
    <slot name="default" />
  </div>
  <Teleport to="#modal">
    <div
      ref="dropdownRef"
      :style="dropdownStyle"
      style="position: absolute"
      v-show="isMouseOver"
    >
      <Card>
        <slot name="overlay" />
      </Card>
    </div>
  </Teleport>
</template>

最后,我将把这些组件组合在一起,如下所示。

在用户设置页面,我将使用用户卡组件,它由卡片、头像、姓名组件和用户详情组件组成。

// UserWithDescription.vue
<script setup lang="ts">
import AppCard from "./Card.vue";
import DescriptionItem from "./DescriptionItem.vue";
import Avatar from "./Avatar.vue";
import UserName from "./UserName.vue";
import UserDescription from "./UserDescription.vue";
import { defineProps } from "vue";
interface Props {
  firstName: string;
  lastName: string;
  image?: string;
  birthDate?: string;
  phone?: string;
  email?: string;
  address?: string;
}
const props = defineProps<Props>();
</script>
<template>
  <AppCard>
    <Avatar :image="image" />
    <div>
      <div>
        <UserName :firstName="firstName" :lastName="lastName" />
      </div>
      <UserDescription v-bind="props" />
    </div>
  </AppCard>
</template>

至于 "员工名录 "页面,我计划由两个部分组成

UserCard.vue

// UserCard.vue
<script setup lang="ts">
    import AppCard from "./Card.vue";
    import DescriptionItem from "./DescriptionItem.vue";
    import Avatar from "./Avatar.vue";
    import UserName from "./UserName.vue";
    import { defineProps } from "vue";
    interface Props {
        firstName: string;
        lastName: string;
        image?: string;
    }
    const props = defineProps<Props>();
</script>
<template>
    <AppCard>
        <Avatar :image="image" />
        <div>
            <div>
                <UserName
                    :firstName="firstName"
                    :lastName="lastName" />
            </div>
        </div>
    </AppCard>
</template>

** UserCardWithTooltip.vue**

// UserCardWithTooltip.vue
<script setup lang="ts">
    import ToolTip from "./Tooltip.vue";
    import UserDescription from "./UserDescription.vue";
    import UserCard from "./UserCard.vue";
    import Card from "./Card.vue";
    import { defineProps } from "vue";
    interface Props {
        firstName: string;
        lastName: string;
        image?: string;
        birthDate?: string;
        phone?: string;
        email?: string;
    }
    const props = defineProps<Props>();
</script>
<template>
    <ToolTip>
        <UserCard v-bind="props" />
        <template #overlay>
            <Card>
                <UserDescription v-bind="props" />
            </Card>
        </template>
    </ToolTip>
</template>

注:你可能会注意到,所提供的解决方案基于原子设计概念。该概念首先可以将 "可重用性 "挑战降至最低。如果您对如何将其应用于 Vue.js 感兴趣,请参阅我同事的文章。

单元测试有帮助吗?

有些人可能会认为,为可重用组件编写单元测试会缓解这一问题。的确,全面的测试覆盖有助于确保对组件的修改和增强不会意外破坏功能。

然而,单元测试并不能使组件变得更可重用。它只是让组件更健壮而已。事实上,重构为更小的组件可以将任务分解为特定的部分,使单元测试的编写更易于管理。

结论

在 Vue中创建实际的可重用组件可能具有挑战性,这是因为需要解决修改现有组件、保持一致性以及管理依赖关系和状态等相关问题。然而,可重用组件的好处使得克服这些问题是值得的。可重用组件能加强代码组织、提高开发效率,并有助于创建一致的用户界面。当我们面对新的需求或任务时,我们将不断改进,以便更好地设计可重用组件。

以上就是一文详解Vue中的可重用组件的详细内容,更多关于Vue可重用组件的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文