12 种 Vue 设计模式

对我来说,设计模式曾经是个谜,在看招聘说明时经常看到它,比如下面这两张图,就是随便翻某聘时看到的,但好长一段时间,我都没太理解啥是设计模式,只觉得它挺高大上的。

招聘要求中的设计模式1
招聘要求中的设计模式2

后来听一位大咖介绍什么是设计模式,我觉得他讲得挺好。他说,设计模式其实就是套路,你写代码写多了,发现有好多情况可以用类似的代码来解决,经过不断提炼和优化,大家见了纷纷学之,这样的代码架构就成了套路,也就是所谓的设计模式。

好的设计模式能让你的代码质量更高、更简洁,毕竟是经过大家千锤百炼之后得到公认的。这也就是为什么好多企业在招聘时需要你熟悉设计模式。

只不过,虽然不少图书或教程都介绍过 JS 或 TS 的设计模式,但有关 Vue 应用开发的设计模式的内容却很少。但这不代表 Vue 就没有设计模式,恰恰相反,Vue 还有不少个性化的设计模式。

虽然现在大家动不动就说前端已死,但搞前端开发的同学们还是得靠它吃饭啊,尤其是 Vue,作为国内前端开发的主流框架,更是前端必备的技能。为了帮大家更好地捧好这个饭碗,今天学研群就给大家整理了一篇文章,先粗浅地介绍一下 Vue 的设计模式,共有 12 种,并附上了示例代码,展示这些设计模式的工作原理,供大家参考和学习。

这篇文章是我为前端同学推出的一篇长文,也是我想推出的前端学习系列的第一篇文章,欢迎大家多留言,提出建议(如果你不想留言,点个赞也成,这样我也好知道你们是不是对这方面的内容感兴趣,就当是个小调研吧),如果大家喜欢,我后期会继续推出更多类似的内容。

再多说一句,我非常认真地对待每一篇文章,尽力给大家提供更好的阅读体验和更实用的知识,希望能和大家一起在开发学习的道路上共同进步。

1. 数据存储模式

解决状态管理最简单的方法是使用组合式函数创建共享的数据存储。

这种模式分为以下几种情况。

  • 全局状态单例
  • 导出部分或全部状态
  • 访问与修改状态的方法

示例如下。

import { reactive, toRefs, readonly } from 'vue';
import { themes } from './utils';

// 在模块作用域中创建全局状态,每次使用组合式函数时都会共享
const state = reactive({
  darkMode: false,
  sidebarCollapsed: false,

// theme 的值在组合式函数中为私有
  theme: 'nord',
});

export default () => {
  // 只暴露部分状态
  // 使用 toRefs 分享单个值
  const { darkMode, sidebarCollapsed } = toRefs(state);

  // 修改底层状态
  const changeTheme = (newTheme) => {
    if (themes.includes(newTheme)) {
      // 仅在存在该主题时才更新
      state.theme = newTheme;
    }
  }

  return {
    // 仅返回部分状态
    darkMode,
    sidebarCollapsed,
    // 仅暴露状态的只读版本
    theme: readonly(state.theme),
    // 返回修改底层状态的方法
    changeTheme,
  }
}

2. 轻组合式函数

轻组合式函数引入了额外的抽象层,把核心业务逻辑与响应式管理分离开来。这种模式使用 JS 或 TS 函数处理业务逻辑,然后在其上添加轻量的响应处理层。

import { ref, watch } from 'vue';
import { convertToFahrenheit } from './temperatureConversion';

export function useTemperatureConverter(celsiusRef: Ref<number>) {
  const fahrenheit = ref(0);

  watch(celsiusRef, (newCelsius) => {
    // 业务逻辑包含在这个函数中
    fahrenheit.value = convertToFahrenheit(newCelsius);
  });

  return { fahrenheit };
}

3. 谦卑组件模式

所谓谦卑(humble),在这里可以理解为组件要谦逊,一个组件只专注实现一个功能,别逞能,不要在一个组件中实现过多的功能,这样才能让组件的复用、测试与维护更加容易。

这种模式要求组件专注于页面呈现与用户输入,不要处理业务逻辑。依据该模式设计的组件代码更易懂,预测数据流也更加容易。

<template>
  <div class="max-w-sm rounded overflow-hidden shadow-lg">
    <img class="w-full" :src="userData.image" alt="User Image" />
    <div class="px-6 py-4">
      <div class="font-bold text-xl mb-2">
        {{ userData.name }}
      </div>
      <p class="text-gray-700 text-base">
        {{ userData.bio }}
      </p>
    </div>
    <div class="px-6 pt-4 pb-2">
      <button
        @click="emitEditProfile"
        class="bg-blue-500 hover:bg-blue-700 text-white
          font-bold py-2 px-4 rounded"
      >
        Edit Profile
      </button>
    </div>
  </div>
</template>

<script setup>
defineProps({
  userData: Object,
});

const emitEditProfile = () => {
  emit('edit-profile');
};
</script>

4. 提取条件分支

为了简化多条件分支的模板,可以把多个分支的内容抽取成相互独立的组件。这种模式让代码更易读、更好维护。

读者读到这里可能会觉得怎么翻来覆去都是这句话?其实设计模式的主要目的就让你的代码架构更清晰、更简洁、更好懂、更好维护,所以就只能这么说了,大家主要看代码啊,这些代码表述得还是挺清楚的,别只看文字,好好看代码。

<!--提取之前 -->
<template>
  <div v-if="condition">
    <!-- 条件为真时的代码 -->
  </div>
  <div v-else>
    <!-- 条件为假时的代码 -->
  </div>
</template>

<!-- 提取之后 -->
<template>
  <TrueConditionComponent v-if="condition" />
  <FalseConditionComponent v-else />
</template>

5. 提取组合式函数

把处理业务逻辑的代码提取为组合式函数,即便只使用一次的逻辑代码也应该提取出来。

组合式函数可以让组件的代码更简单、更容易理解和维护(不好意思,还是这些目的,不想继续看的,迅速下滑,去点个在看,讨伐一下我)。在组合式函数中添加方法与状态也更简单,例如撤销与重做等功能。这种模式有利于分离 UI 与业务逻辑。

import { ref, watch } from 'vue';

export function useExampleLogic(initialValue: number) {
  const count = ref(initialValue);

  const increment = () => {
    count.value++;
  };
  const decrement = () => {
    count.value--;
  };

  watch(count, (newValue, oldValue) => {
    console.log(`Count changed from ${oldValue} to ${newValue}`);
  });

  return { count, increment, decrement };
}

<template>
  <div class="flex flex-col items-center justify-center">
    <button
      @click="decrement"
      class="bg-blue-500 text-white p-2 rounded"
    >
      Decrement
    </button>
    <p class="text-lg my-4">Count: {{ count }}</p>
    <button
      @click="increment"
      class="bg-green-500 text-white p-2 rounded"
    >
      Increment
    </button>
  </div>
</template>

<script setup lang="ts">
  import { useExampleLogic } from './useExampleLogic';
  const { count, increment, decrement } = useExampleLogic(0);
</script>

6. 列表组件模式

列表组件的代码有时会导致混乱,让模板难以管理,这时你可以把 v-for 循环中涉及的代码提取到子组件里。

<!-- 提取之前:直接在父组件中使用 v-for -->
<template>
  <div v-for="item in list" :key="item.id">
    <!-- 这里是一堆针对 item 的代码 -->
  </div>
</template>

<!-- 把 v-for 相关的代码提取到子组件中 -->
<template>
  <NewComponentList :list="list" />
</template>

7. 保留对象模式

要想简化组件代码,可以把整个对象传递给组件,不要把对象的内容分别传递给组件的每个 props。然而,这种模式可能会提高组件对对象结构的依赖程度,因此,这种设计模式并不适用于通用组件。

<!-- 把整个对象传递给组件 -->
<template>
  <CustomerDisplay :customer="activeCustomer" />
</template>

<!-- CustomerDisplay.vue -->
<template>
  <div>
    <p>Name: {{ customer.name }}</p>
    <p>Age: {{ customer.age }}</p>
    <p>Address: {{ customer.address }}</p>
  </div>
</template>

8. 控制器组件模式

控制器组件桥接了 UI(谦卑组件)与业务逻辑(组合式函数),可以集中管理状态与交互操作,协调应用的整体行为。鉴于,控制器组件集中管理状态的特性,在单元测试与集成测试时可以不用关心 UI 的实现细节,让业务逻辑测试更容易。

<!-- TaskController.vue -->
<script setup>
  import useTasks from './composables/useTasks';

  // 包含业务逻辑的组合式函数
  const { tasks, addTask, removeTask } = useTasks();
</script>

<template>
  <!-- 实现 UI 的谦卑组件 -->
  <TaskInput @add-task="addTask" />
  <TaskList :tasks="tasks" @remove-task="removeTask" />
</template>

9. 策略模式

策略模式能根据运行时的条件动态切换组件,使代码更易读、更灵活。根据用户权限动态切换菜单组件的显示,就是策略模式的典型应用场景。

<template>
  <component :is="currentComponent" />         
</template>

<script setup>
  import { computed } from 'vue';
  import ComponentOne from './ComponentOne.vue';
  import ComponentTwo from './ComponentTwo.vue';
  import ComponentThree from './ComponentThree.vue';

  const props = defineProps({
    conditionType: String,
  });

  const currentComponent = computed(() => {
    switch (props.conditionType) {
      case 'one':
        return ComponentOne;
      case 'two':
        return ComponentTwo;
      case 'three':
        return ComponentThree;
      default:
        return DefaultComponent;
    }
  });
</script>

10. 隐式组件

隐式组件模式的思路就是把复杂的大组件分割为多个更小、更专注的组件(有点类似谦卑模式)。如果不同的性质的代码可以独立使用,则可以进行组件分割。

<!-- 重构前 -->
<template>
  <!-- 这里可以分割为图表组件 -->
  <DataDisplay
    :chart-data="data"
    :chart-options="chartOptions"
  />

  <!-- 这里可以分割为表格组件 -->
  <DataDisplay
    :table-data="data"
    :table-settings="tableSettings"
  />
</template>

<!-- 重构后 -->
<template>
  <Chart :data="data" :options="chartOptions" />
  <table :data="data" :settings="tableSettings" />
</template>

11. 内幕交易模式

内幕交易模式解决的是 Vue 中父子组件过度耦合的问题,这种模式通过在父组件中内联子组件进行简化,有利于形成连贯且一致的组件架构。

<!-- ParentComponent.vue -->
<template>
  <div>
    <!-- 子组件使用的都是来自父组件的数据,它存在的目的是什么? -->
    <ChildComponent
      :user-name="userName"
      :email-address="emailAddress"
      :phone-number="phoneNumber"
      @user-update="(val) => $emit('user-update', val)"
      @email-update="(val) => $emit('email-update', val)"
      @phone-update="(val) => $emit('phone-update', val)"
    />
  </div>
</template>

<script setup>
  defineProps({
    userName: String,
    emailAddress: String,
    phoneNumber: String,
  });
  defineEmits(['user-update', 'email-update', 'phone-update']);
</script>

12. 长组件

什么样的组件是长组件?简单说,让人难以理解的组件就是长组件。

长组件模式的原则是把组件拆分为多个命名清晰的小组件,可以提升代码的整体质量,易于理解。

<!-- 之前:又长又复杂的组件 -->
<template>
  <div>
    <!-- 这里是一堆 HTML 代码和业务逻辑 -->
  </div>
</template>

<!-- 之后:分解为多个小组件,通过组件名就可以了解该组件的功能 -->
<template>
  <ComponentPartOne />
  <ComponentPartTwo />
</template>

希望这篇文章能让大家初步了解 Vue 的设计模式。这 12 种模式都是 Vue 前端开发时常用的,当然,如果你使用 React 或 Vanilla JS,也可以参考。

我会继续努力,在成为全栈开发者的道路上继续奔驰,并为大家带来更多优质内容。如果你觉得这篇文章有用,记得点赞、在看和分享哦!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353

推荐阅读更多精彩内容