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>