亮网络解锁器,解锁网络数据的无限可能 了解详情
写点什么

Vue.js 组件的复用性:真正可复用还是伪装的可复用?

作者 | Fang Tanbamrung

  • 2023-10-27
    北京
  • 本文字数:10146 字

    阅读完需:约 33 分钟

Vue.js组件的复用性:真正可复用还是伪装的可复用?

大家讨论在 Vue.js 中创建 UI 组件时,总会提到可复用性的问题。没错,Vue.js 的一大核心原则就是其基于组件的架构,相应的好处自然是有助于可复用性和模块化。但这俩时髦词汇到底该怎么理解?


假设我们创建了一个可复用的组件:


  • 那我或我的同事真能在系统的其他部分复用这个组件吗?

  • 面对新需求,我们可能还得修改这个“可复用组件”。

  • 如果需要拆分这个“可复用组件”,以便把拆分出来的新组件应用到其他位置,又该如何操作?


在 Vue.js 中创建可复用组件的具体过程其实颇为棘手。在本文中,我们将具体探讨可复用组件的概念、实际应用时面临的问题,以及为什么有必要花心思克服这一道道难关。


可复用组件是个啥?


可复用组件是指一个个 UI 构建块,它们能在应用程序的各个位置、甚至是不同应用的相应位置上发挥作用。它们封装有特定的功能或 UI 模式,能够轻松集成至应用程序的其他部分,而且无需进行重大修改。


可复用组件的优势


通过在 Vue.js 中使用可复用组件,我们可以获得以下好处。


  • 提升效率:允许开发人员一次编写代码并多次重复使用,减少冗余内容并节约下宝贵的开发时间。

  • 贯彻标准化:促进各 Vue.js 项目之间的一致性和标准化,确保整个应用程序当中贯彻相同的设计模式、样式与功能。

  • 增强可扩展性:随着项目发展,我们可以轻松实现扩展和调整。通过将应用程序拆分成更小且可复用的组件,复杂功能的处理和新功能的添加也将变得更容易管理。

  • 促进协作:有助于各 Vue.js 项目团队成员之间的协作。可复用组件将帮助每一位团队成员使用并理解相同的表达和 UI 元素集。


运用可复用概念时的三个关键问题


虽然 Vue.js 组件将可复用性作为一大原则特性,但以下几个现实问题却往往会阻碍其具体实现。


  1. 修改现有组件:第一个问题,就是需要修改应用程序中正在使用的现有组件。该组件可能需要进行调整,从而同时支持原有和新的需求。但对应用程序中其他部分组件进行变更,有可能带来意想不到的副作用并破坏其他位置上的功能。在变更需求与保持兼容性之间寻求平衡往往相当复杂。

  2. 在组件设计中考虑一致性和灵活性:第二个问题,就是如何在可复用组件的不同实例之间保持一致性,同时保留灵活的可定制空间。可复用组件应当具备充分的通用性,从而适应不同的设计要求和风格。当然,在不牺牲组件核心功能与一致性的情况下提供定制选项也绝非易事。

  3. 管理组件依赖项和状态:要想让可复用组件发挥作用,就必须管理好相关依赖项并保证各个组件独立且自包含。具体来讲,各组件不应紧密依赖于外部资源或应用程序的状态管理系统。只有这样,我们才能将可复用组件轻松整合至不同项目当中,减少引发冲突或意外副作用的可能性。


案例研究


比方说,客户想为内部员工创建一套目录系统。这个项目基于敏捷开发方法,在开发之前没法充分收集需求。项目进度共分为三个阶段(原型设计阶段、第一阶段和第二阶段),出于演示需要,下面我将重点关注其中的 Card 组件。



原型设计阶段


在原型设计阶段,我们需要提供一个 User Profile 用户资料页面。用户的个人资料将包含最基本的 User Card 组件,其中又分为 User Avatar 头像和 Name 姓名。



// 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>
复制代码


第一阶段


在第一阶段,客户那边希望能在 User Card 组件上添加 User Detail 客户细节(包括出生日期、年龄、手机号码和邮箱地址)。



//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>
复制代码


此外,客户还希望添加 Employee Directory 员工目录页面,其中以卡片格式显示用户资料。



// 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> 
复制代码


在此阶段,User Card 组件在两个页面上均可复用。


第二阶段


用户反馈称,员工目录页面过于混乱,大量信息导致阅读起来非常难受。因此,客户希望在鼠标悬停时通过提示来展现用户资料。而 User Setting 用户设置页面部分的内容则比较合理,可以不加变动。



// 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 issueconst 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>
复制代码


感兴趣的朋友可以通过实时演示,查看原型设计阶段、第一阶段和第二阶段的代码。


https://codesandbox.io/s/reusable-components-in-vue-wwsd5y?file=/src/components/Prototype.vue


这个新要求着实令人头痛:


  • 我们要不要修改现有 User Card 组件来支持弹出提示?但这可能会影响到 User Setting 页面中的 User Card 组件,给应用程序造成意外干扰;

  • 或者,我们可以直接复制现有 User Card 组件,再向其中添加弹出提示?但这意味着 User Card 组件将不可复用。



因为我们实在不想破坏已经能够正常起效的东西,所以这里更倾向于后一种选择。这乍看之下似乎是个完美的解决方案,但在大规模、长期运行的项目当中,却也有可能带来巨大的破坏性影响:


  1. 代码库膨胀:这种方式会令代码库变得越来越大,因为对组件的每次复制都会增加不必要的代码行。这将使得项目愈发难以维护,因为每当需要更新或者做 bug 修复时,开发人员都得在多个位置上进行更改。这样还增加了引发一致性冲突的可能性。

  2. 短期收益,长期痛苦:短期之内,这似乎是个快速且简单的解决方案,能很好地应对紧迫的时间期限或者急切的突发需求。但随着项目的发展,对大量重复组件的维护会变得越来越困难且耗时。对重复组件的修改或更新都将需要在多个实例之间反复进行,大大拉高产生错误的几率。

  3. 影响系统性能:这种方式会对系统性能产生负面影响。冗余代码会增加应用程序的体积,导致渲染时间变长并增加内存占用量。最终,这一切会致使用户体验不佳、降低系统运行效率。


如何克服上述难题


请做好心理准备,可复用组件在整个项目周期中往往需要适应和调整,很难长时间保持不变。这看似是句废话,但如果认真想想,大家就会发现这真的是句至理名言——因为需求在变,所以围绕需求建立的整个体系都必须得变。当然,丰富的经验能帮助大家在初步设计时留出更大的适应空间,但也会相应影响开发速度。


重构可复用组件


就个人经验来讲,我更倾向于重新设计和重构这些可复用组件。所谓重构,就是在不改变其原始功能的情况下对代码做重新整理。可以选择的重构方法有很多,我个人会将组件重构并拆分成更多小型组件。这些更小的组件能在整个系统中灵活发挥作用。有了这个基本思路,下面来看我们要如何具体解决前文案例中提出的问题。


请注意:重构 UI 组件也需要遵守一定原则。另外,重构可能也会面临挑战,特别是如何在项目交付期限和代码清晰度之间取得平衡。


尝试解决前文案例


首先,我们将现有 User Card 组件拆分成 4 个组件:


  • Card 卡片组件

  • Avatar 头像组件

  • Name 姓名组件

  • User Detail 用户资料组件



// 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 弹出提示组件。将其设定为独立组件之后,我也可以在系统的其他位置对它进行复用。



// 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>
复制代码


最后,我把这些组件整合起来,如下所示。在其中的 User Setting 用户设置页面中,我会使用 User Card 组件,其中包含 Card、Avatar、Name 和 User Detail 组件。



// 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>
复制代码


至于 Employee Directory 员工目录页面,我打算使用 2 个组合组件:


  • 其中的基本 User Card 组件由 Card、Avatar 和 Name 组件构成。

  • User Tooltip 弹出提示组件则由 Card、Tooltip 和 User Detail 组件构成。



// 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>
复制代码


关于本案例中代码重构的更多细节信息,请参阅我分享的完整解决方案:


https://codesandbox.io/s/reusable-components-in-vue-wwsd5y?file=/src/components/solution/UserCardWithTooltip.vue


请注意:很多朋友可能发现,我提供的解决方案基于原子设计思路。这个概念本身就能最大限度缓解“可复用”方面的挑战。如果您对 Vue.js 中的原子设计思维感兴趣,这里向大家推荐另外一篇文章:


https://dev.to/berryjam/introducing-atomic-design-in-vuejs-1l2h


单元测试能帮上忙吗?


很多朋友可能觉得,为那些可复用组件编写单元测试应该能帮得上忙。毕竟在全面覆盖之后,单元测试有助于保证针对各组件的修改和增强不会意外破坏其原有功能。


但这里要给大家泼点冷水,单元测试本身并不会让组件变得更可复用,而只是让组件更加健壮。而且根据任务中的特定部分将组件进一步拆解,倒是可以让单元测试变得更易于编写和管理。


总结


受到修改现有组件、保持一致性、管理依赖项和系统状态等一系列现实问题的影响,在 Vue.js 中实际构建可复用组件往往充满挑战。但考虑到可复用组件所带来的种种助益,我们当然有理由迎难而上、努力克服这些障碍。可复用组件能够增强代码组织结构、提高开发效率,也有助于建立起更趋一致的用户界面。面对新的任务或者要求,我们不妨从设计可复用组件入手,从当下开始改善整个项目的结构和质量。


欢迎评论区说出你的建议。



原文链接:


https://dev.to/fangtanbamrung/challenges-of-reusable-components-in-vuejs-and-how-to-overcome-them-4g6d

相关阅读:


Vue.js 最佳静态站点生成器对比

强烈推荐:一个 Vue.js 在线挑战平台

Vue.js 前后端同构方案之准备篇—代码优化

从 React 切换到 Vue.js

2023-10-27 16:076338

评论

发布
暂无评论
发现更多内容

MobTech全面助力开发与运营用户进行APP生命周期智能管理

MobTech袤博科技

大数据 智能推送

轻量应用服务器价值典范,云耀云服务器助力企业穿越经济周期

轶天下事

华为云盘古大模型for医学,“良医小慧”让智慧诊疗触手可及

彭飞

Mac系统设置维护软件 TinkerTool System激活最新版

mac大玩家j

系统优化 Mac软件 系统清理工具

简化 Go 开发:使用强大的工具提高生产力

SEAL安全

Go 语言 开发. 企业号9月PK榜

什么是立方体led显示屏?立方体led显示屏适合用在什么地方?

Dylan

设计 模块 LED显示屏 led显示屏厂家

在AI的风口上,百度营销如何助力企业抢占先机?

彭飞

技术向上,场景向下丨华为云828 B2B企业节打通云上路径

轶天下事

K-最近邻算法(KNN)

小魏写代码

游戏服务商Latis Global参展2023 ChinaJoy B2B

科技热闻

Mac电脑文本识别 TextSniper 免激活最新版

胖墩儿不胖y

Mac软件 文本识别工具

HarmonyOS线性容器特性及使用场景

HarmonyOS开发者

HarmonyOS

从繁琐到一键直达:秒验助力实现优化用户登录体验

MobTech袤博科技

大数据 智能推送

Mac Office安装许可工具后,软件显示只读模式,如何解决?

展初云

Office Mac软件

运行Adobe应用提示非正版This non-genuine Adobe app has been disabled如何处理

展初云

ps adobe Mac软件

聚焦私域营销降本提效,国联股份与火山引擎数智平台展开合作

字节跳动数据平台

大数据 数字化转型 数据平台 火山引擎 企业号9月PK榜

快速读懂Etcd

Quincy

golang 源码 分布式 etcd

技术贴 | 深度解析 PostgreSQL Protocol v3.0(二)— 扩展查询

KaiwuDB

一键登录是如何为应用开发者实现降本增效的

MobTech袤博科技

大数据 智能推送

轻量应用服务器选哪家?华为云耀云服务器L实例告诉你如何选择

轶天下事

华为云耀云服务器 L 实例:为你揭开轻量应用服务器的神秘面纱

轶天下事

中小企业请收藏丨轻量应用服务器企业选购避坑指南

轶天下事

遥遥领先的不仅是华为Mate60 Pro+,华为云正在数字赋能万千中小企业

轶天下事

打造承载百倍级增长后台背后的力量

优测云服务平台

性能优化 后台开发 性能测试 压力测试 性能压测

GreatSQL一个关于主从复制的限制描述与规避

GreatSQL

主从复制 greatsql 运维实战

Vue.js组件的复用性:真正可复用还是伪装的可复用?_架构/框架_InfoQ精选文章