12 шаблонов проектирования в Vue

Это перевод статьи с английского языка. Прочитать оригинальную статью

Шаблоны проектирования невероятно полезны для написания кода, который остается надежным в долгосрочной перспективе. Они позволяют нам использовать проверенные решения для проблем, с которыми сталкивается практически каждое приложение.

Однако о том, как шаблоны проектирования применимы именно к Vue, написано не так уж много.

И еще меньше информации о паттернах, которые существуют исключительно в Vue благодаря его уникальным особенностям.

Я хочу это исправить.

В этой статье я собрал 12 различных шаблонов проектирования для Vue с короткими и понятными примерами их работы. Это скорее обзор и отправная точка, а не финальная стадия изучения темы. Чтобы правильно применять эти шаблоны в нужных местах и в подходящем контексте, потребуется гораздо больше объяснений, чем помещается в одну статью!

Ах да, и хотя статья получилась длинной, в конце я добавил девять дополнительных паттернов.

1. Паттерн хранилища данных (The Data Store Pattern)

Самый простой способ решить множество проблем управления состоянием — использовать composable для создания разделяемого хранилища данных.

Этот паттерн состоит из нескольких частей:

  • Глобальный синглтон состояния
  • Экспортирование части или всего состояния
  • Методы для доступа и изменения состояния
import { reactive, toRefs, readonly } from 'vue'
import { themes } from './utils'

// 1. Создаем глобальное состояние в области модуля,
//    оно будет разделяться при каждом использовании composable
const state = reactive({
  darkMode: false,
  sidebarCollapsed: false,
  // 2. Значение темы остается приватным внутри composable
  theme: 'nord',
})

export default () => {
  // 2. Экспортируем только часть состояния
  //    Использование toRefs позволяет делиться отдельными значениями
  const { darkMode, sidebarCollapsed } = toRefs(state)

  // 3. Метод для изменения состояния
  const changeTheme = (newTheme) => {
    if (themes.includes(newTheme)) {
      // Обновляем тему только если она допустима
      state.theme = newTheme
    }
  }

  return {
    // 2. Возвращаем только часть состояния
    darkMode,
    sidebarCollapsed,
    // 2. Предоставляем только "только для чтения" версию состояния
    theme: readonly(state.theme),
    // 3. Экспортируем метод для изменения состояния
    changeTheme,
  }
}

2. Тонкие композиционные функции (Thin Composables)

Тонкие композиционные функции вводят дополнительный уровень абстракции, отделяя управление реактивностью от основной бизнес-логики. Здесь мы используем чистый JavaScript или TypeScript для бизнес-логики, представленной в виде чистых функций, с тонким слоем реактивности сверху.

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 Components Pattern)

Скромные компоненты создаются с упором на простоту, фокусируясь на отображении и вводе пользователя, а бизнес-логику оставляя в другом месте.

Следуя принципу «Пропсы вниз, события вверх», эти компоненты обеспечивают четкий и предсказуемый поток данных, что делает их простыми в повторном использовании, тестировании и поддержке.

<script setup lang="ts">
defineProps({
  userData: Object,
})

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

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

4. Извлечение условий (Extract Conditional)

Чтобы упростить шаблоны с несколькими условными ветвями, мы выносим содержимое каждой ветви в отдельные компоненты. Это улучшает читаемость и сопровождаемость кода.

<!-- До -->
<template>
  <div v-if="condition">
    <!-- Много кода для истинного условия -->
  </div>
  <div v-else>
    <!-- Много другого кода для ложного условия -->
  </div>
</template>

<!-- После -->
<template>
  <TrueConditionComponent v-if="condition" />
  <FalseConditionComponent v-else />
</template>

Полную статью об этом шаблоне вы можете прочитать здесь.

5. Выделение в composable-функции (Extract Composable)

Вынос логики в composable-функции, даже для единичных случаев. Composable-функции упрощают компоненты, делая их более понятными и удобными в поддержке.

Они также облегчают добавление связанных методов и состояний, таких как функции отмены и повтора (undo/redo). Это помогает нам отделять логику от пользовательского интерфейса.

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 }
}
<script setup lang="ts">
import { useExampleLogic } from './useExampleLogic'
const { count, increment, decrement } = useExampleLogic(0)
</script>

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

6. Паттерн компонента списка (List Component Pattern)

Большие списки в компонентах могут привести к перегруженным и неудобным для восприятия шаблонам. Решение заключается в абстрагировании логики цикла v-for в дочерний компонент.

Это упрощает родительский компонент и инкапсулирует логику итерации в выделенном компоненте списка, сохраняя все аккуратно и упорядоченно.

<!-- До: Прямой `v-for` в родительском компоненте -->
<template>
  <div v-for="item in list" :key="item.id">
    <!-- Много кода, специфичного для каждого элемента -->
  </div>
</template>

<!-- После: Абстрагирование `v-for` в дочерний компонент -->
<template>
  <NewComponentList :list="list" />
</template>

7. Шаблон "Сохранение объекта" (Preserve Object Pattern)

Передача целого объекта в компонент вместо отдельных пропсов упрощает компоненты и защищает их от изменений в будущем. Однако этот подход может создать зависимость от структуры объекта, поэтому он менее подходит для универсальных компонентов.

<!-- Использование всего объекта -->
<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. Компоненты контроллеры (Controller Components)

Компоненты контроллеры в Vue служат связующим звеном между пользовательским интерфейсом (простыми компонентами) и бизнес-логикой (композируемыми функциями). Они управляют состоянием и взаимодействиями, координируя общую логику работы приложения.

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

// Композируемые функции содержат бизнес-логику
const { tasks, addTask, removeTask } = useTasks()
</script>

<template>
  <!-- Простые компоненты предоставляют интерфейс -->
  <TaskInput @add-task="addTask" />
  <TaskList :tasks="tasks" @remove-task="removeTask" />
</template>

9. Шаблон "Стратегия" (Strategy Pattern)

Шаблон стратегии идеально подходит для обработки сложной условной логики в приложениях Vue.

Он позволяет динамически переключаться между различными компонентами в зависимости от условий выполнения, что улучшает читаемость и гибкость.

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

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

10. Скрытые компоненты (Hidden Components)

Шаблон скрытых компонентов подразумевает разделение сложного компонента на более мелкие, более специализированные, в зависимости от того, как он используется.

Если разные наборы свойств используются исключительно вместе, это может свидетельствовать о возможности разделения компонента.

<!-- До рефакторинга -->
<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. Торговля с использованием инсайдерской информации (Insider Trading)

Шаблон "Торговля с использованием инсайдерской информации" решает проблему слишком тесной связи между родительскими и дочерними компонентами в Vue. Мы упрощаем, 'инлайня' дочерние компоненты в родительский, когда это необходимо.

Этот процесс может привести к более согласованной и менее фрагментированной структуре компонентов.

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

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

12. Длинные компоненты

Что такое "слишком длинный" компонент?

Это когда его становится слишком трудно понять.

Принцип длинных компонентов предполагает создание самодокументирующихся, четко названных компонентов, что улучшает качество кода и понимание.

<!-- До: длинный и сложный компонент -->
<template>
  <div>
    <!-- Много HTML и логики -->
  </div>
</template>

<!-- После: разбиение на более мелкие компоненты, где имя говорит о том, что делает код. -->
<template>
  <ComponentPartOne />
  <ComponentPartTwo />
</template>
Copyright © 2025 Novweb. All rights reserved.