当我们谈论或讨论在vue.js中创建UI组件时,经常提出可重用性。是的,vue.js的关键原理之一是其基于组件的体系结构,可促进可重复性和模块化。但是这是什么意思?
假设您创建一个可重复使用的组件:
- 您或您的同事可以在系统的另一部分重复使用(没有双关语)吗?
- 有了新的要求,您可能必须考虑修改“可重复使用的组件”。
- 如果您需要将“可重复使用的组件”拆分到以便将拆分组件应用于另一个地方怎么办?
在vue.js中创建可重复使用的组件可能很棘手。在本文中,我将探讨可重复使用的组件的概念,应用它们时面临的问题以及为什么要尽可能地克服这些问题至关重要。
什么是可重复使用的组件?
可重复使用的组件是UI构建块,可以在应用程序的不同部分甚至在多个项目中使用。它们封装了特定的功能或UI模式,并且可以轻松地集成到应用程序的其他部分,而无需进行广泛的修改。
可重复使用的组件的好处
通过在vue.js中使用可重复使用的组件,您可以获得几个好处:
- 效率:允许开发人员写一次代码并多次重复使用。这减少了冗余并节省了宝贵的发展时间。
- 标准化:促进vue.js项目的一致性和标准化。它们确保在整个应用程序中保持相同的设计模式,样式和功能。
- 可伸缩性:使扩展和调整项目成长更容易。通过将应用程序分解为较小的可重复使用的组件,处理复杂功能并添加新功能变得更加可管理。
- 合作:促进在VUE.JS项目中工作的团队成员之间的合作。它们提供了共享的词汇和一组UI元素,团队中的每个人都可以使用和理解。
应用可重复使用概念时的3个问题
虽然可重复使用性是vue.js组件中的理想特征,但几个问题可能使得很难实现:
-
修改现有组件:一个问题是修改应用程序中已经使用的现有组件。可能需要更改组件以支持现有和新要求。更改已在应用程序其他部分中已经使用的组件可能会引入意外的副作用并在其他领域中破坏功能。平衡更改的需求与保持兼容性可能很复杂。
-
设计组件的一致性和灵活性:另一个问题是在可重复使用的组件的不同实例上保持一致性,同时允许自定义和灵活性。可重复使用的组件应足够多功能,以适应不同的设计要求和样式。但是,在不牺牲组件的核心功能和一致性的情况下提供自定义选项可能很棘手。
-
管理组件依赖性和状态:使用可重复使用的组件涉及管理依赖项并确保每个组件保持独立和独立。组件不应对外部资源或应用程序的状态管理系统有严格的依赖性。这可以轻松地集成到不同的项目中,并减少冲突或意外副作用的可能性。
案例研究
假设客户需要内部员工目录系统。该项目是根据敏捷方法参与的,所有要求在开发前都无法收集。有3个阶段(原型,第1阶段和阶段2)。出于此次演示的目的,我将重点关注以下的卡组件:
原型
作为原型阶段的一部分,我需要提供一个用户配置文件页面。用户配置文件将包含一个基本的用户卡组件,其中包括用户头像和名称。
// 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
在第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>
在此阶段,用户卡组件在这两个页面上都可以重复使用。
阶段2
用户反馈员工目录页面混乱。太多信息使页面难以使用。因此,客户希望用户详细信息在悬停后显示在工具提示中。用户设置页面上的要求保持不变。
// 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>
要查看原型,第1阶段和第2阶段的代码,您可以在live demo中找到它们。
这个新要求会引起头痛:
- 我是否修改了现有的用户卡组件以支持工具提示要求并影响用户设置页面中用户卡组件的风险?或
- 我是否复制了现有的用户卡组件并添加工具提示功能?
因为我们不想打破生产中已经存在的东西,所以我们倾向于选择后一种选项。起初,这可能是有道理的,但可能会造成很大的破坏,尤其是对于大型和连续的项目:
- 大型代码库:导致更大的代码库,因为每个重复的组件都添加了不必要的代码行。很难维护,因为每当需要更新或错误修复时,开发人员需要在多个位置进行更改。它还增加了不一致的机会。
- 短期增益,长期痛苦:在短期内看起来像是一种快速简便的解决方案,尤其是在处理紧迫的期限或紧急要求时。但是,随着项目的增长,维护重复的组件变得越来越困难和耗时。需要在多个实例上复制对重复组件的修改或更新,从而导致错误的机会更高。
- 系统性能:可能会对系统性能产生负面影响。冗余代码增加了应用程序的大小,从而导致渲染时间较慢并增加了内存使用情况。这可能会导致次优的用户体验并降低系统效率。
如何克服上述问题
要准备好可重复使用的组件在整个项目中可能并不总是相同的。这听起来很陈词滥调,但是如果您考虑一下,要求总是在不断发展。您无法控制未来,除非目前尽力而为。当然,经验可以帮助您设计更好的组件,但需要时间。
重构可重复使用的组件
根据我的经验,我将重新设计和重构可重复使用的组件。重构是重组代码的过程,同时不更改其原始功能。我敢肯定,有很多方法可以重构,对我来说,我将组件重构并将其分解为较小的组件。较小的组件可以灵活地在整个系统中应用它们。让我们看一下我将如何应用上面提到的案例研究。
注意:重构UI组件需要纪律。另外,有时可能会具有挑战性,因为您需要在项目交付截止日期和清洁程序代码之间取得平衡。
将解决方案应用于案例研究
首先,我将现有的用户卡组件分为4个组件:
- 卡片组件
- 化身组件
- 名称组件
- 用户详细信息组件
// 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
<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
<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
<script setup lang="ts">
import { defineProps } 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
<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
<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>
至于员工目录页面,我计划为2个组合组件
- 由卡,头像和名称组件组成的基本用户卡组件。
- 由卡,工具提示和用户详细信息组成的用户工具提示组件。
// 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
<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>
有关此案例研究的重构代码的更多详细信息,请查看solution。
注意:您可能会注意到解决方案提供的基于原子设计概念。这个概念首先可以最大程度地减少“可重复性”挑战。如果您对如何应用于vue.js感兴趣,请参阅我的同事的article。
单位测试有帮助吗?
有些人可能认为可重复使用组件的编写单元测试可以缓解此问题。真正的全面测试覆盖范围有助于确保对组件的修改和增强并不会意外打破功能。
但是,单位测试并不能使组件更加重复使用。它只是使它更加健壮。实际上,重构为较小的组件将任务分解为特定的部分,并使写作单元测试更易于管理。
结论
在vue.js中创建实际可重复使用的组件可能会具有挑战性,这是由于与修改现有组件,保持一致性以及管理依赖性和状态有关的问题。但是,可重复使用的组件的好处使其值得克服这些问题。可重复使用的组件增强了代码组织,提高开发效率并促进创建一致的用户界面。当我们面临新的要求或任务时,我们将改进,以便我们可以更好地设计可重复使用的组件。
Berryjam:VUE 3&NUXT的UI组件分析仪
如果您有兴趣了解更多有关使用UI组件的方式,或者您的前端团队如何利用可共享的UI组件,请考虑查看Berryjam。它是免费的!
我真的很感激,并赋予我继续创建更多内容的力量。先感谢您! ð