Когда я начал работать с Vue несколько лет назад, мне казалось достаточным использовать простую передачу данных через props и события между компонентами. Однако уже на среднем по сложности проекте пришлось столкнуться с так называемым "prop drilling" — ситуацией, когда приходится передавать состояние через цепочку из 5-7 компонентов, большинство из которых даже не используют эти данные, а просто служат проводниками.
Представьте себе: у вас есть данные о пользователе, которые нужны и в шапке сайта, и в боковой панели, и в основном контенте. Без централизованного хранилища вам придется либо дублировать логику получения этих данных, либо устраивать сложную систему передачи через промежуточные компоненты. Другая распространенная проблема — синхронизация состояния между разными частями приложения. Когда пользователь выполняет действие в одном компоненте (например, добавляет товар в корзину), это изменение должно мгновенно отразиться во всех связанных компонентах (счетчик товаров, список корзины, итоговая сумма). Без правильной архитектуры состояния эта задача превращается в настоящий кошмар, особенно когда речь идет об асинхронных операциях.
Реактивность — краеугольный принцип Vue, но и она может стать источником проблем. Нередко разработчики сталкиваются с непредсказуемым поведением приложения из-за неправильного обновления реактивных данных. Особенно часто это происходит в сложных формах или при работе с вложенными объектами и массивами. Ситуация осложняется необходимостью сохранять состояние между переходами страниц или даже сессиями пользователя. Как организовать кэширование данных? Как избежать повторных запросов к API при возврате на предыдущую страницу? Эти вопросы требуют продуманного подхода к управлению состоянием. А теперь добавьте сюда требования к масштабируемости и поддерживаемости кода. Состояние приложения должно быть не только функциональным, но и легко расширяемым, тестируемым, и понятным для других разработчиков. Оказывается, что стихийный подход к управлению данными быстро превращает проект в неподдерживаемый клубок зависимостей. Отдельный вызов представляют и сами операции изменения состояния. Мутации, которые могут происходить в разных частях приложения, создают потенциал для трудноотлаживаемых багов. Когда любой компонент может изменять общие данные, становится практически невозможно отследить, где и почему произошло нежелательное изменение.
Упомянутые проблемы стали причиной появления специализированных решений для управления состоянием во Vue-приложениях. Первоначально стандартом был Vuex — официальная библиотека, следующая архитектурным принципам Flux. Однако с появлением Vue 3 и Composition API возникла потребность в более современном и гибком инструменте, которым и стала Pinia.
Обзор эволюции от Vuex к Pinia
История управления состоянием во Vue начинается задолго до появления специализированных библиотек. На ранних этапах разработчики довольствовались простыми паттернами, такими как Event Bus или хранение состояния в корневом компоненте. Однако эти подходы быстро показали свои ограничения при масштабировании приложений.
Vuex появился в 2016 году как официальное решение для управления состоянием в экосистеме Vue. Вдохновленный архитектурой Flux от Facebook и библиотекой Redux, Vuex предложил структурированный подход с четким разделением на state (состояние), getters (геттеры), mutations (мутации) и actions (действия). Такая архитектура делала поток данных предсказуемым и облегчала отладку. Концепция односторонних потоков данных, ставшая фундаментом Vuex, помогала избегать хаоса в больших приложениях. Вместо того чтобы позволять компонентам напрямую изменять общее состояние, Vuex требовал четкой последовательности действий: компонент вызывает действие (action), которое при необходимости совершает асинхронные операции и затем вызывает мутацию (mutation), и только мутация имеет право изменить состояние (state).
При всех своих преимуществах Vuex имел и существенные недостатки. Многие разработчики жаловались на избыточный бойлерплейт-код, особенно при реализации простых функций. Типизация с TypeScript также была неудобной, требуя множества обходных путей для достижения полноценной поддержки типов.
С выходом Vue 3 и популяризацией Composition API стало очевидно, что Vuex, тесно связанный с Options API, не вписывается в новую парадигму. Появилась потребность в решении, которое бы органично сочеталось с Composition API и устраняло недостатки Vuex. Так в 2019 году Эдуардо Сан Мартин Моро, член основной команды Vue, представил экспериментальную библиотеку под названием Pinia (первоначально – "Pinnia", что на испанском созвучно со словом "ананас"). Pinia была создана как альтернатива Vuex, которая лучше интегрируется с Composition API и TypeScript.
В отличие от Vuex с его строгим разделением на mutations и actions, Pinia упростила модель, объединив эти концепции и позволив разработчикам напрямую изменять состояние внутри actions. Исчезла необходимость в громоздких конструкциях, что значительно сократило объем кода и улучшило читаемость. Нативная поддержка TypeScript стала еще одним значительным улучшением Pinia. Если в Vuex для правильной типизации требовались дополнительные усилия и обходные пути, то Pinia предложила полноценную поддержку типов "из коробки", что существенно улучшило разработку в среднем и крупном масштабе.
К 2022 году Pinia официально заменила Vuex в качестве рекомендуемого решения для управления состоянием во Vue-приложениях. Команда Vue признала преимущества нового подхода, и сегодня Pinia фактически является "Vuex 5", хотя и под другим названием. Эволюция от Vuex к Pinia отражает общую тенденцию в экосистеме Vue – движение к более простым, гибким и типобезопасным инструментам, которые улучшают опыт разработчика и снижают входной порог для новичков.
Vuejs - Composition API Пытаюсь разобраться с Composition API и ни чего не могу понять.
Вот мой app.vue
<template>
<div>
Capacity: {{... Vue 3 API Всем привет.
Вопросы, возможно, глупые, но пара дней насилования гугла результатов не принесли.
Прошу пояснить несколько моментов по API.
... Управление массивами, Vue.set() Здравствуйте!
Подскажите, можно ли как то передать и индекс(index), и значение(value) и изменяемого элемента <input type="text"... vue и axios + api Здравствуйте. Я тут пробую на вкус vue в связке с laravel. Я пытаюсь сделать по примеру, получение валют одного банка через апишку. Не могу понять...
Реактивность в Vue 3
Реактивность — сердце любого современного фронтенд-фреймворка, и Vue 3 внес революционные изменения в эту систему. В версии 3.0 была полностью переработана архитектура реактивности, что привело к значительному улучшению производительности и гибкости. В основе новой реактивной системы Vue 3 лежит функция reactive() , которая использует JavaScript Proxy вместо устаревшего подхода через Object.defineProperty, применявшегося в Vue 2. Это изменение решило множество болезненных ограничений предыдущей версии, таких как невозможность реактивного отслеживания добавления или удаления свойств объекта.
Прокси-объекты в JavaScript — структуры, которые перехватывают и переопределяют основные операции для другого объекта. Vue 3 использует их для создания "ловушек" на операции доступа и модификации свойств. Когда вы обращаетесь к реактивному свойству, система автоматически отслеживает эту операцию, а когда изменяете его — запускает обновление всех зависимых частей пользовательского интерфейса.
JavaScript | 1
2
3
4
5
6
7
8
9
10
| import { reactive } from 'vue'
const state = reactive({
count: 0,
user: { name: 'Алексей', age: 30 }
})
// Обе операции теперь корректно отслеживаются
state.count++
state.newProperty = 'Это будет реактивным!' |
|
Но прокси — не единственное новшество. Второй ключевой функцией стала ref() , которая позволяет создавать реактивные примитивные значения, такие как строки, числа и булевы значения. В отличие от Vue 2, где примитивы не могли быть реактивными сами по себе, ref() оборачивает значение в объект с геттером и сеттером для свойства .value :
JavaScript | 1
2
3
4
5
6
| import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1 |
|
Необходимость добавлять .value при работе с ref-объектами за пределами шаблона сначала кажется неудобной, но это компромисс, который позволил сделать реактивными даже примитивные типы данных.
Интересной особенностью является "автоматическая распаковка" ref-объектов в шаблонах и реактивных объектах. В шаблоне компонента вам не нужно писать count.value — просто используйте count , и Vue сделает распаковку за вас:
HTML5 | 1
2
3
| <template>
<div>{{ count }}</div> <!-- Не нужен .value -->
</template> |
|
А при вложении ref-объекта в реактивный объект, ref также автоматически распаковывается:
JavaScript | 1
2
3
4
5
6
| const user = reactive({
name: 'Мария',
age: ref(25)
})
console.log(user.age) // 25, а не ref-объект |
|
Эта особенность позволяет писать более чистый код, хотя иногда может сбивать с толку новичков.
Еще одно важное отличие — введение "эффектов" через функцию watchEffect() . Этот механизм автоматически отслеживает зависимости функции и перезапускает её при изменении любой из них:
JavaScript | 1
2
3
4
5
6
7
8
9
| import { ref, watchEffect } from 'vue'
const count = ref(0)
const message = ref('')
watchEffect(() => {
message.value = `Счетчик сейчас: ${count.value}`
// Эта функция перезапустится автоматически при изменении count
}) |
|
В отличие от методов жизненного цикла в Options API, которые жестко привязаны к компоненту, функции реактивности в Composition API можно использовать где угодно — в компонентах, в пользовательских хуках или даже вне контекста Vue. Это открывает огромные возможности для повторного использования логики. Стоит отметить, что новая система реактивности не лишена ограничений. Например, она не может отследить доступ к элементам массива через индексы (array[0] ) при использовании в качестве источников в вычисляемых свойствах. В таких случаях лучше использовать методы массивов, такие как map и filter .
Производительность — еще один существенный плюс реактивности Vue 3. Благодаря Proxy система может более точно отслеживать только необходимые зависимости, что снижает накладные расходы. Кроме того, новый компилятор шаблонов умеет статически анализировать их и генерировать более оптимизированный код. Для тех, кто перешел с Vue 2, важно помнить о различиях в поведении. Например, в Vue 3 все реактивные объекты "глубоко реактивны" по умолчанию, в то время как в Vue 2 требовалось явно указывать флаг deep для глубокого отслеживания.
Создание и использование состояния с ref и reactive
Vue 3 предлагает два основных способа создания реактивного состояния: функции ref() и reactive() . Разобравшись в их особенностях и различиях, вы сможете эффективно управлять данными в компонентах Vue и выбирать подходящий инструмент для конкретной задачи.
Функция ref() создаёт реактивную обёртку вокруг любого значения, включая примитивы:
JavaScript | 1
2
3
4
5
6
7
| import { ref } from 'vue'
const counter = ref(0) // число
const userName = ref('Анна') // строка
const isActive = ref(false) // булево значение
const userList = ref([]) // массив
const profile = ref({ id: 1 }) // объект |
|
Для доступа к значению, хранящемуся в ref, необходимо использовать свойство .value :
JavaScript | 1
2
3
4
5
6
7
| console.log(counter.value) // 0
counter.value++ // увеличиваем значение
console.log(counter.value) // 1
// Работа с объектами внутри ref
profile.value.name = 'Иван' // добавление нового свойства
console.log(profile.value.name) // 'Иван' |
|
Функция reactive() , в свою очередь, создаёт реактивный прокси для объекта или массива:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import { reactive } from 'vue'
const user = reactive({
name: 'Пётр',
age: 28,
address: {
city: 'Москва',
street: 'Ленина'
}
})
// Прямой доступ без .value
console.log(user.name) // 'Пётр'
user.age = 29 // изменение свойства
user.isAdmin = true // добавление нового свойства |
|
Ключевые отличия между ref() и reactive() :
1. ref() может работать с любыми типами данных, включая примитивы, но требует использования .value для доступа к значению.
2. reactive() может быть применён только к объектам и массивам, но не требует .value .
3. ref() при использовании в шаблонах автоматически "распаковывается" — .value не нужен.
4. reactive() не может заменить переменную целиком, а ref() может.
Именно последнее различие часто вызывает затруднения. Посмотрим на пример:
JavaScript | 1
2
3
4
5
6
7
8
| const user = reactive({ name: 'Алексей' })
// Это НЕ сработает - реактивность будет потеряна!
user = reactive({ name: 'Сергей' }) // Ошибка при использовании const или потеря реактивности при let
// А с ref это работает
let userRef = ref({ name: 'Алексей' })
// Реактивность сохраняется
userRef.value = { name: 'Сергей' } |
|
Эта особенность делает ref() более гибким инструментом для ситуаций, когда вам нужно полностью заменять значения, а не просто изменять их свойства.
При практическом использовании часто возникает вопрос: какой подход выбрать? У сообщества Vue сложились определённые рекомендации:
1. Для примитивных значений (строки, числа, булевы значения) всегда используйте ref() .
2. Для объектов и массивов можно использовать оба подхода, но reactive() даёт более чистый код без .value .
3. Если вам может понадобиться заменить объект целиком — выбирайте ref() .
Комбинирование обоих подходов также возможно. Например, вы можете создать реактивный объект, содержащий несколько ref-значений:
JavaScript | 1
2
3
4
5
6
7
8
| const formData = reactive({
username: ref(''),
email: ref(''),
password: ref('')
})
// Обратите внимание, что в реактивном объекте ref автоматически разворачивается
console.log(formData.username) // доступ без .value |
|
В окружении TypeScript вы также получаете преимущества при использовании этих функций. Vue 3 предлагает отличную типизацию для реактивного состояния:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| const counter = ref<number>(0)
const userList = ref<User[]>([])
interface User {
id: number
name: string
}
const user = reactive<User>({
id: 1,
name: 'Мария'
}) |
|
В сложных сценариях может потребоваться "деструктурировать" реактивный объект. Но будьте осторожны — простая деструктуризация приведёт к потере реактивности:
JavaScript | 1
2
3
4
5
| const user = reactive({ name: 'Анна', age: 25 })
// Это разрушит реактивность!
const { name, age } = user
console.log(name) // 'Анна', но изменения user.name не отразятся здесь |
|
Для сохранения реактивности при деструктуризации используйте функцию toRefs() :
JavaScript | 1
2
3
4
5
6
7
8
9
10
| import { reactive, toRefs } from 'vue'
const user = reactive({ name: 'Анна', age: 25 })
// Сохраняем реактивность
const { name, age } = toRefs(user)
// Теперь name.value и user.name связаны
name.value = 'Мария'
console.log(user.name) // 'Мария' |
|
Другой полезный приём — использование функции computed() для создания вычисляемых свойств на основе реактивных данных:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
| import { ref, computed } from 'vue'
const firstName = ref('Иван')
const lastName = ref('Петров')
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
console.log(fullName.value) // 'Иван Петров'
firstName.value = 'Сергей'
console.log(fullName.value) // 'Сергей Петров' |
|
Вычисляемые свойства также являются ref-объектами, поэтому для доступа к их значениям используется .value .
Наконец, стоит упомянуть о функции shallowRef() и shallowReactive() , которые создают "неглубокие" реактивные объекты — отслеживается только верхний уровень. Это полезно для оптимизации производительности при работе с большими объектами данных, когда не требуется отслеживать все вложенные свойства:
JavaScript | 1
2
3
4
5
6
7
8
9
10
| import { shallowRef } from 'vue'
// Только сам объект реактивен, его внутренние свойства — нет
const user = shallowRef({ name: 'Дмитрий', address: { city: 'Казань' } })
// Это вызовет обновление (заменяем весь объект)
user.value = { name: 'Виктор', address: { city: 'Самара' } }
// Это НЕ вызовет обновления (внутреннее свойство не отслеживается)
user.value.address.city = 'Новосибирск' |
|
Сравнение Options API и Composition API в контексте управления данными
Vue 3 привнес революционные изменения в то, как мы структурируем код компонентов, предоставив разработчикам выбор между традиционным Options API и новым Composition API. Эти два подхода кардинально различаются в организации управления состоянием компонентов. В Options API, знакомом разработчикам по Vue 2, состояние и логика компонента организованы по типам: данные хранятся в свойстве data , методы — в methods , вычисляемые свойства — в computed , и так далее. Это создает четкую и понятную на первый взгляд структуру:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| export default {
data() {
return {
counter: 0,
userName: 'Гость',
products: []
}
},
computed: {
doubleCounter() {
return this.counter * 2
}
},
methods: {
increment() {
this.counter++
},
loadProducts() {
// Загрузка продуктов
}
}
} |
|
Однако при росте сложности компонента появляются существенные недостатки. Логика одной функциональности оказывается разбросанной по разным опциям. Например, функциональность "счетчика" распределена между data (свойство counter), computed (свойство doubleCounter) и methods (метод increment). Когда таких функциональностей становится много, код становится трудно поддерживать. В противоположность этому, Composition API группирует код по логическим блокам функциональности, а не по типам опций:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| import { ref, computed } from 'vue'
export default {
setup() {
// Вся логика счетчика вместе
const counter = ref(0)
const doubleCounter = computed(() => counter.value * 2)
function increment() {
counter.value++
}
// Другая логика может быть сгруппирована отдельно
const userName = ref('Гость')
const products = ref([])
function loadProducts() {
// Загрузка продуктов
}
return {
counter,
doubleCounter,
increment,
userName,
products,
loadProducts
}
}
} |
|
Такая организация кода обеспечивает лучшую читаемость и поддерживаемость, особенно в крупных компонентах. Вы можете легко увидеть всю логику, связанную с определённой функциональностью, в одном месте.
Когда дело касается повторного использования логики, разница становится еще более очевидной. В Options API приходилось использовать миксины, примеси или плагины, которые имели ряд недостатков:- Источник добавляемых свойств неявный (трудно понять, откуда пришло свойство).
- Возможные конфликты имен между разными миксинами.
- Отсутствие параметризации (трудно настроить миксин для конкретного случая).
Composition API решает эти проблемы через композабельные функции:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0, step = 1) {
const count = ref(initialValue)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value += step
}
return {
count,
doubleCount,
increment
}
}
// Использование в компоненте
import { useCounter } from './useCounter'
export default {
setup() {
// Можно создать несколько счетчиков с разными параметрами
const { count: firstCounter, increment: incrementFirst } = useCounter(0, 1)
const { count: secondCounter, increment: incrementSecond } = useCounter(10, 5)
return {
firstCounter,
incrementFirst,
secondCounter,
incrementSecond
}
}
} |
|
В контексте управления состоянием Composition API дает еще одно важное преимущество — прозрачность работы с реактивными данными. В Options API неявная привязка к this часто становилась источником ошибок, особенно при использовании асинхронных функций или передаче методов в качестве колбэков. Приходилось использовать конструкции типа const vm = this или стрелочные функции для сохранения контекста.
В Composition API зависимости явно видны в коде. Любая функция имеет доступ только к тем реактивным данным, которые были переданы ей или объявлены в ее области видимости:
JavaScript | 1
2
3
4
5
| async function fetchUserData(userId) {
// Здесь мы явно видим, что функция работает с userData
// Нет неявной привязки к this
userData.value = await api.getUser(userId)
} |
|
Типизация — еще один аспект, где Composition API значительно превосходит Options API. В TypeScript определение типов для объекта с опциями требовало сложных вспомогательных конструкций и часто страдало от проблем с выводом типов. Composition API обеспечивает намного лучшую поддержку TypeScript без дополнительных усилий:
TypeScript | 1
2
3
4
5
6
7
8
| // TypeScript с Composition API
const count = ref<number>(0)
const user = ref<User | null>(null)
interface User {
id: number
name: string
} |
|
Тестировать код на Composition API также проще. Поскольку логика компонента теперь может быть извлечена в отдельные функции, их можно тестировать изолированно, без необходимости создавать экземпляр компонента:
JavaScript | 1
2
3
4
5
6
7
8
9
| // useCounter.spec.js
import { useCounter } from './useCounter'
test('should increment counter', () => {
const { count, increment } = useCounter(0)
expect(count.value).toBe(0)
increment()
expect(count.value).toBe(1)
}) |
|
Несмотря на все преимущества, Composition API не является универсальным решением для всех случаев. Для простых компонентов Options API может быть более понятным и лаконичным. Кроме того, многим разработчикам, привыкшим к Vue 2, требуется время, чтобы адаптироваться к новому подходу.
Типизация состояний с TypeScript в Vue 3
TypeScript стал неотъемлемой частью современной фронтенд-разработки, и Vue 3 привнёс значительные улучшения в поддержку типизации. Интеграция TypeScript с реактивной системой Vue 3 позволяет создавать более надёжные приложения с меньшим количеством ошибок во время выполнения.
Начнём с основ. В отличие от Vue 2, где поддержка TypeScript была реализована через дополнительные плагины и требовала немало обходных путей, Vue 3 был полностью переписан на TypeScript и спроектирован с учётом типизации. Это значит, что вы получаете встроенную поддержку типов без дополнительных настроек. При использовании функций ref и reactive можно явно указать тип содержимого:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| import { ref, reactive } from 'vue'
// Типизированный ref
const count = ref<number>(0)
// Более сложный тип
interface User {
id: number
name: string
email: string
isAdmin: boolean
}
const currentUser = ref<User | null>(null)
// Типизированный reactive
const userForm = reactive<User>({
id: 1,
name: 'Антон',
email: 'anton@example.com',
isAdmin: false
}) |
|
Обратите внимание на конструкцию User | null — это объединение типов, которое указывает, что значение может быть либо объектом типа User, либо null. Такой подход часто используется при работе с асинхронно загружаемыми данными. Типизация особенно полезна при работе с геттерами и вычисляемыми свойствами. Vue 3 автоматически выводит типы для вычисляемых свойств на основе возвращаемого значения:
TypeScript | 1
2
3
4
5
6
7
| const doubleCount = computed(() => count.value * 2)
// Тип автоматически выведен как ComputedRef<number>
// Можно также явно указать тип
const username = computed<string>(() => {
return currentUser.value ? currentUser.value.name : 'Гость'
}) |
|
При работе со сложными объектами TypeScript помогает избежать ошибок при обращении к несуществующим свойствам:
TypeScript | 1
2
3
4
5
| // TypeScript выдаст ошибку, если такого свойства нет
console.log(userForm.nonExistentProperty) // Ошибка!
// Или при попытке присвоить неправильный тип
userForm.isAdmin = "yes" // Ошибка! Ожидался тип boolean |
|
Для функций также можно указывать типы параметров и возвращаемых значений:
TypeScript | 1
2
3
4
| function updateUser(userId: number, data: Partial<User>): Promise<User> {
// Partial<User> означает, что все свойства User становятся необязательными
return api.updateUser(userId, data)
} |
|
Когда дело доходит до реактивных хранилищ, типизация становится ещё более важной. При работе с Pinia, о которой мы поговорим подробнее в следующих разделах, типизация помогает автоматически выводить типы результатов геттеров и обеспечивает проверку типов для мутаций состояния.
Одной из сложностей при работе с типами в Vue 3 является правильное типизирование пропсов компонента. В Composition API для этого используется функция defineProps :
TypeScript | 1
2
3
4
5
6
| // SFC <script setup>
const props = defineProps<{
user: User
id?: number // Необязательный проп с "?"
callback: (id: number) => void // Функциональный проп
}>() |
|
Для эмитируемых событий существует аналогичная функция defineEmits :
TypeScript | 1
2
3
4
5
6
7
8
9
| const emit = defineEmits<{
(e: 'update', id: number): void
(e: 'delete'): void
}>()
// Использование:
emit('update', 123) // Корректно типизировано
emit('delete')
emit('update') // Ошибка: отсутствует обязательный аргумент |
|
Интересная особенность Vue 3 — возможность определить типы шаблонных слотов (template slots) с помощью TypeScript:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
| // MyComponent.vue
// <script setup lang="ts">
defineSlots<{
default(props: { item: string }): void
header(props: { title: string }): void
}>()
// Использование
<MyComponent>
<template #default="{ item }">{{ item }}</template>
<template #header="{ title }">{{ title }}</template>
</MyComponent> |
|
При управлении глобальным состоянием приложения, TypeScript становится незаменимым инструментом, особенно в сочетании с Pinia. Типизированные хранилища позволяют автоматически получать подсказки при обращении к состоянию, геттерам и действиям:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| // store/users.ts
import { defineStore } from 'pinia'
export const useUsersStore = defineStore('users', {
state: () => ({
users: [] as User[],
loading: false,
error: null as string | null
}),
getters: {
adminUsers: (state) => state.users.filter(user => user.isAdmin)
},
actions: {
async fetchUsers() {
this.loading = true
try {
this.users = await api.getUsers()
} catch (err) {
this.error = (err as Error).message
} finally {
this.loading = false
}
}
}
})
// Использование
const usersStore = useUsersStore()
// TypeScript знает все свойства и методы
usersStore.users.forEach(user => console.log(user.name))
const admins = usersStore.adminUsers
usersStore.fetchUsers() |
|
Для ещё более строгой типизации можно использовать служебные типы TypeScript, например для создания типа, основанного на состоянии хранилища:
TypeScript | 1
2
3
4
5
6
7
| // Получаем тип состояния из хранилища
type UsersState = ReturnType<typeof useUsersStore>['$state']
// Создаем функцию, которая принимает только состояние этого хранилища
function processUsersState(state: UsersState) {
// Работаем с состоянием
} |
|
Для работы с асинхронными операциями TypeScript также предлагает удобные инструменты. Например, вы можете типизировать результат асинхронной загрузки данных:
TypeScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| interface ApiResponse<T> {
data: T
meta: {
total: number
page: number
}
}
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url)
return response.json()
}
// Использование с конкретным типом
const { data, meta } = await fetchData<User[]>('/api/users')
// data имеет тип User[] |
|
TypeScript добавляет дополнительный уровень надёжности вашим приложениям Vue 3, особенно при работе с реактивным состоянием. Хотя начать работу с типами может быть немного сложнее, чем с обычным JavaScript, преимущества в виде автодополнения кода, ранней проверки ошибок и улучшеной документации стоят дополнительных усилий.
Архитектура и преимущества Pinia
Pinia представляет собой новое поколение библиотек для управления состоянием в приложениях Vue. Разработанная Эдуардо Сан Мартин Моро, она предлагает элегантный и интуитивно понятный подход к организации глобальных данных. В отличие от более сложной модели Vuex с его строгим разделением на мутации и действия, Pinia использует более прямолинейную структуру. Каждое хранилище (store) в Pinia состоит из трёх основных частей:
State — реактивное состояние хранилища,
Getters — вычисляемые свойства, основанные на состоянии,
Actions — методы для изменения состояния и выполнения бизнес-логики.
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| import { defineStore } from 'pinia'
export const useTaskStore = defineStore('tasks', {
state: () => ({
tasks: [],
loading: false
}),
getters: {
completedTasks: (state) => state.tasks.filter(task => task.completed),
incompleteTasks: (state) => state.tasks.filter(task => !task.completed),
totalCount: (state) => state.tasks.length
},
actions: {
async fetchTasks() {
this.loading = true
try {
// В Pinia можно напрямую изменять состояние внутри actions
this.tasks = await api.getTasks()
} finally {
this.loading = false
}
},
addTask(title) {
const newTask = {
id: Date.now(),
title,
completed: false
}
this.tasks.push(newTask)
}
}
}) |
|
Ключевое отличие от Vuex — отсутствие отдельного понятия mutations. В Pinia вы можете напрямую изменять состояние внутри actions, что существенно сокращает объем шаблонного кода и делает процесс разработки более плавным. Преимущества Pinia многочисленны, и они объясняют, почему эта библиотека стала официально рекомендуемым решением для Vue 3:
1. Интуитивно понятный API — структура хранилища логична и не требует изучения сложных концепций. Новичкам гораздо проще освоить Pinia, чем Vuex.
2. Поддержка модульности без настройки — каждое хранилище Pinia независимо и может быть импортировано отдельно. В Vuex для этого требовалось явно настраивать модули и пространства имен.
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // Использование нескольких хранилищ вместе
import { useUserStore } from './stores/user'
import { useCartStore } from './stores/cart'
export default {
setup() {
const userStore = useUserStore()
const cartStore = useCartStore()
// Теперь можно использовать оба хранилища
return {
user: userStore,
cart: cartStore
}
}
} |
|
3. Великолепная поддержка TypeScript — Pinia написана на TypeScript и предоставляет полную типизацию из коробки без дополнительных настроек. Тип состояния, геттеров и действий выводится автоматически.
4. Полная интеграция с Vue DevTools — можно отслеживать изменения состояния, выполнение действий и значения геттеров в реальном времени через панель инструментов разработчика.
5. Удобная работа с плагинами — расширение функциональности с помощью плагинов в Pinia значительно проще, чем в Vuex. Вы можете, например, добавить автоматическую сериализацию состояния в localStorage или обработку ошибок.
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // Пример плагина для Pinia, сохраняющего состояние в localStorage
function localStoragePlugin({ store }) {
// Загружаем начальное состояние из localStorage
const saved = localStorage.getItem(store.$id)
if (saved) {
store.$state = JSON.parse(saved)
}
// Сохраняем изменения в localStorage
store.$subscribe((_, state) => {
localStorage.setItem(store.$id, JSON.stringify(state))
})
}
// Применение плагина
const pinia = createPinia()
pinia.use(localStoragePlugin)
app.use(pinia) |
|
6. Возможность использовать в компонентах как Composition API, так и Options API — гибкий подход, позволяющий интегрировать Pinia в любой стиль написания компонентов:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // Composition API
setup() {
const store = useStore()
return {
counter: store.counter,
doubleCount: store.doubleCount,
increment: store.increment
}
}
// Options API
computed: {
...mapState(useStore, ['counter']),
...mapGetters(useStore, ['doubleCount'])
},
methods: {
...mapActions(useStore, ['increment'])
} |
|
7. Более прозрачные зависимости — в Pinia зависимости между хранилищами явные и прослеживаемые. Одно хранилище может использовать другое, просто импортируя его:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
| import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
actions: {
checkout() {
const userStore = useUserStore()
if (!userStore.isLoggedIn) {
throw new Error('Пользователь должен быть авторизован')
}
// Продолжить оформление заказа
}
}
}) |
|
8. Лучшая производительность — благодаря более оптимизированной структуре и использованию Proxy в Vue 3, Pinia работает быстрее, особенно в больших приложениях.
9. Поддержка горячей замены модулей (HMR) — при разработке можно изменять хранилища без перезагрузки страницы, сохраняя текущее состояние приложения.
10. Меньший размер библиотеки — Pinia предлагает все необходимые функции при меньшем размере кода по сравнению с Vuex.
Архитектура Pinia тесно связана с Composition API, делая эту комбинацию особенно мощной. Вы можете создавать композабельные функции, которые используют хранилища Pinia, абстрагируя тем самым логику работы с глобальным состоянием:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // use-authentication.js
import { useUserStore } from '@/stores/user'
import { computed } from 'vue'
export function useAuthentication() {
const userStore = useUserStore()
const isLoggedIn = computed(() => !!userStore.currentUser)
const username = computed(() => userStore.currentUser?.name || 'Гость')
async function login(credentials) {
return userStore.login(credentials)
}
function logout() {
userStore.logout()
}
return {
isLoggedIn,
username,
login,
logout
}
} |
|
Такой подход позволяет сосредоточиться на бизнес-логике, а не на технических деталях управления состоянием, делая код более читаемым и поддерживаемым.
Работа с хранилищами (stores) в Pinia
Создание и использование хранилищ в Pinia интуитивно понятно и не требует сложных настроек. Базовая структура хранилища формируется с помощью функции defineStore , которая принимает два ключевых аргумента: уникальный идентификатор хранилища и объект с его конфигурацией.
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Счетчик'
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
}
}
}) |
|
Идентификатор хранилища (в примере выше — 'counter') должен быть уникальным в рамках приложения. Он используется Pinia для подключения хранилища к инструментам разработки и для сохранения/восстановления состояния.
Для использования хранилища в компоненте достаточно импортировать соответствующую функцию и вызвать её:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| <script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
// Теперь можно обращаться к состоянию, геттерам и действиям
console.log(counterStore.count)
console.log(counterStore.doubleCount)
counterStore.increment()
</script>
<template>
<div>
<p>Счетчик: {{ counterStore.count }}</p>
<p>Удвоенное значение: {{ counterStore.doubleCount }}</p>
<button @click="counterStore.increment">Увеличить</button>
</div>
</template> |
|
Pinia также поддерживает альтернативный способ определения хранилищ с использованием синтаксиса, схожего с Composition API:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const name = ref('Счетчик')
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, name, doubleCount, increment }
}) |
|
Этот подход дает больше гибкости при организации сложной логики внутри хранилища и лучше интегрируется с пользовательскими композабельными функциями.
Одна из сильных сторон Pinia — простота доступа к состоянию из других хранилищ. Для этого просто импортируйте и используйте нужное хранилище:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
isCheckingOut: false
}),
actions: {
async checkout() {
const userStore = useUserStore()
if (!userStore.isLoggedIn) {
return { success: false, error: 'Пожалуйста, войдите в систему' }
}
this.isCheckingOut = true
try {
// Логика оформления заказа
return { success: true }
} finally {
this.isCheckingOut = false
}
}
}
}) |
|
При работе с хранилищами часто возникает необходимость сбросить состояние до исходных значений. Pinia предоставляет для этого метод $reset() :
JavaScript | 1
2
3
4
| const counterStore = useCounterStore()
counterStore.count = 10
// Позже, когда нужно сбросить состояние
counterStore.$reset() // count вернётся к 0 |
|
Для отслеживания изменений в хранилище можно использовать метод $subscribe , который работает аналогично методу watch, но автоматически отключается при уничтожении компонента:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
| const unsubscribe = counterStore.$subscribe((mutation, state) => {
// Срабатывает при каждом изменении состояния
console.log('Изменение', mutation)
console.log('Текущее состояние', state)
// Можно сохранять состояние в localStorage
localStorage.setItem('counter', JSON.stringify(state))
})
// Отписка вручную, если необходимо
unsubscribe() |
|
Также можно отслеживать выполнение действий с помощью $onAction :
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| counterStore.$onAction(({
name, // Имя действия
args, // Массив аргументов
after, // Хук, вызываемый после действия
onError // Хук, вызываемый при ошибке
}) => {
console.log(`Вызвано действие ${name} с аргументами:`, args)
after((result) => {
console.log(`Действие ${name} завершено с результатом:`, result)
})
onError((error) => {
console.error(`Действие ${name} завершилось с ошибкой:`, error)
})
}) |
|
Для деструктурирующего присваивания свойств хранилища без потери реактивности используйте вспомогательную функцию storeToRefs :
JavaScript | 1
2
3
4
5
6
7
8
9
10
| import { storeToRefs } from 'pinia'
const counterStore = useCounterStore()
// Не работает, реактивность потеряна
// const { count, doubleCount } = counterStore
// Это работает, сохраняя реактивность
const { count, doubleCount } = storeToRefs(counterStore)
// Обратите внимание: действия не являются реактивными свойствами
const { increment } = counterStore |
|
Если вы предпочитаете работать с Options API, Pinia предоставляет аналогичные Vuex вспомогательные функции:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| import { mapState, mapActions, mapStores } from 'pinia'
export default {
computed: {
// Доступ к хранилищу целиком
...mapStores(useCounterStore),
// Получение конкретных свойств состояния
...mapState(useCounterStore, ['count', 'doubleCount']),
// С использованием псевдонимов
...mapState(useCounterStore, {
myCount: 'count',
myDoubleCount: (store) => store.doubleCount * 2
})
},
methods: {
...mapActions(useCounterStore, ['increment'])
}
} |
|
Asynchronные действия в Pinia реализуются с использованием стандартных асинхронных функций JavaScript:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| import { defineStore } from 'pinia'
export const useProductStore = defineStore('products', {
state: () => ({
products: [],
loading: false,
error: null
}),
actions: {
async fetchProducts() {
this.loading = true
this.error = null
try {
const response = await fetch('/api/products')
this.products = await response.json()
} catch (err) {
this.error = err.message
console.error('Ошибка при загрузке продуктов:', err)
} finally {
this.loading = false
}
}
}
}) |
|
Для управления производительностью в больших приложениях Pinia позволяет создавать хранилища лениво, только когда они действительно нужны. Это особенно полезно при разделении кода с помощью динамического импорта:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Обычный импорт
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// Ленивый импорт при необходимости
const router = createRouter({
routes: [
{
path: '/admin',
component: AdminPanel,
beforeEnter: async (to, from) => {
const userStore = (await import('@/stores/user')).useUserStore()
if (!userStore.isAdmin) return '/login'
}
}
]
}) |
|
Такой подход помогает уменьшить размер начального бандла и ускорить загрузку приложения.
Миграция с Vuex на Pinia: пошаговое руководство
Переход с Vuex на Pinia может показаться сложной задачей, особенно если у вас большое приложение с множеством хранилищ. Однако процесс миграции довольно прямолинеен, и я поделюсь практическим опытом, как сделать этот переход максимально гладким. Начнём с установки Pinia в существующий проект, где уже используется Vuex:
JavaScript | 1
2
3
| npm install pinia
# или
yarn add pinia |
|
После установки необходимо подключить Pinia к приложению Vue. В файле main.js (или main.ts ) добавляем:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
| import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import store from './store' // Существующий Vuex store
const app = createApp(App)
// Одновременно подключаем и Vuex, и Pinia для постепенной миграции
app.use(store) // Vuex
app.use(createPinia()) // Pinia
app.mount('#app') |
|
Это позволит использовать оба решения параллельно, что идеально для поэтапной миграции.
Следующий шаг — преобразование Vuex-модулей в хранилища Pinia. Рассмотрим типичный модуль Vuex:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| // Vuex модуль: store/modules/todo.js
export default {
namespaced: true,
state: {
items: [],
loading: false
},
getters: {
completedTodos: (state) => state.items.filter(item => item.completed),
totalCount: (state) => state.items.length
},
mutations: {
SET_TODOS(state, todos) {
state.items = todos
},
SET_LOADING(state, value) {
state.loading = value
},
ADD_TODO(state, todo) {
state.items.push(todo)
}
},
actions: {
async fetchTodos({ commit }) {
commit('SET_LOADING', true)
try {
const response = await fetch('/api/todos')
const todos = await response.json()
commit('SET_TODOS', todos)
} finally {
commit('SET_LOADING', false)
}
},
addTodo({ commit }, title) {
const todo = {
id: Date.now(),
title,
completed: false
}
commit('ADD_TODO', todo)
}
}
} |
|
Этот модуль в Pinia будет выглядеть так:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // Pinia store: stores/todo.js
import { defineStore } from 'pinia'
export const useTodoStore = defineStore('todo', {
state: () => ({
items: [],
loading: false
}),
getters: {
completedTodos: (state) => state.items.filter(item => item.completed),
totalCount: (state) => state.items.length
},
actions: {
async fetchTodos() {
this.loading = true
try {
const response = await fetch('/api/todos')
this.items = await response.json()
} finally {
this.loading = false
}
},
addTodo(title) {
const todo = {
id: Date.now(),
title,
completed: false
}
this.items.push(todo)
}
}
}) |
|
Обратите внимание на ключевые отличия:
1. Исчезли мутации — в Pinia состояние можно изменять напрямую в действиях.
2. Вместо обращения к контексту через commit используется this для доступа к состоянию.
3. Модуль превратился в функцию, которую вызывают в компонентах.
Теперь нужно обновить компоненты, использующие этот модуль. В Vuex мы обычно подключали хранилище так:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
| // Vuex в Options API
export default {
computed: {
...mapState('todo', ['items', 'loading']),
...mapGetters('todo', ['completedTodos', 'totalCount'])
},
methods: {
...mapActions('todo', ['fetchTodos', 'addTodo'])
},
mounted() {
this.fetchTodos()
}
} |
|
В Pinia это будет выглядеть так:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| // Pinia с Options API
import { useTodoStore } from '@/stores/todo'
import { mapState, mapActions } from 'pinia'
export default {
computed: {
...mapState(useTodoStore, ['items', 'loading', 'completedTodos', 'totalCount'])
},
methods: {
...mapActions(useTodoStore, ['fetchTodos', 'addTodo'])
},
mounted() {
this.fetchTodos()
}
}
// ИЛИ с Composition API
import { useTodoStore } from '@/stores/todo'
export default {
setup() {
const todoStore = useTodoStore()
// Деструктуризация с сохранением реактивности
const { items, loading, completedTodos, totalCount } = storeToRefs(todoStore)
onMounted(() => {
todoStore.fetchTodos()
})
return {
items,
loading,
completedTodos,
totalCount,
addTodo: todoStore.addTodo
}
}
} |
|
Если в Vuex вы использовали корневое состояние и геттеры, их можно перенести в отдельное хранилище Pinia, например, useRootStore .
При миграции сложных модулей часто возникает вопрос, как обрабатывать вложенные модули Vuex. В Pinia нет прямого эквивалента, вместо этого рекомендуется создавать отдельные хранилища и импортировать их друг в друга при необходимости. Например, если у вас есть вложенные модули auth/user и auth/permissions , в Pinia это будут два отдельных хранилища useUserStore и usePermissionsStore .
Последний шаг миграции — удаление Vuex после того, как все модули перенесены. Удалите импорт и использование Vuex из main.js и удалите пакет:
Bash | 1
2
3
| npm uninstall vuex
# или
yarn remove vuex |
|
При миграции полезно использовать Vue DevTools, которые поддерживают как Vuex, так и Pinia. Это позволит вам видеть состояние обоих типов хранилищ и отлаживать их параллельно.
Модульность и композиция хранилищ в Pinia
Одним из ключевых преимуществ Pinia является её модульная архитектура, которая позволяет организовывать код более структурировано и эффективно. В отличие от Vuex, где модули требуют специальной настройки и использования пространств имён, в Pinia каждое хранилище уже является самостоятельным модулем.
Принцип модульности в Pinia реализован на уровне дизайна библиотеки. При создании нового приложения рекомендуется группировать хранилища по функциональности, а не создавать одно гигантское хранилище со всеми данными. Например, в типичном интернет-магазине можно выделить отдельные хранилища для пользователей, товаров, корзины и заказов:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // stores/user.js
export const useUserStore = defineStore('user', {
// состояние и методы пользователя
})
// stores/products.js
export const useProductsStore = defineStore('products', {
// каталог товаров, фильтры, сортировка
})
// stores/cart.js
export const useCartStore = defineStore('cart', {
// корзина покупок
})
// stores/orders.js
export const useOrdersStore = defineStore('orders', {
// заказы пользователя
}) |
|
Такое разделение упрощает поддержку кода, улучшает понимание потока данных в приложении и облегчает совместную работу нескольких разработчиков.
Композиция хранилищ — ещё одна мощная концепция в Pinia. Она позволяет хранилищам взаимодействовать друг с другом напрямую, без необходимости передавать данные через компоненты. В практическом сценарии это выглядит так:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| import { defineStore } from 'pinia'
import { useUserStore } from './user'
import { useProductsStore } from './products'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
couponCode: null
}),
actions: {
async checkout() {
const userStore = useUserStore()
if (!userStore.isLoggedIn) {
// Перенаправить на страницу логина
return false
}
// Создаём заказ на основе данных корзины и пользователя
const orderData = {
items: this.items,
userId: userStore.id,
shippingAddress: userStore.defaultAddress,
coupon: this.couponCode
}
// Отправка запроса и дальнейшая обработка
// ...
// После успешного оформления заказа очищаем корзину
this.items = []
this.couponCode = null
return true
},
addProductById(productId, quantity = 1) {
const productsStore = useProductsStore()
const product = productsStore.getProductById(productId)
if (!product) return false
// Проверяем, есть ли товар уже в корзине
const existingItem = this.items.find(item => item.id === productId)
if (existingItem) {
existingItem.quantity += quantity
} else {
this.items.push({
id: product.id,
name: product.name,
price: product.price,
quantity
})
}
return true
}
},
getters: {
totalAmount: (state) => {
return state.items.reduce((total, item) => total + (item.price * item.quantity), 0)
},
itemsCount: (state) => {
return state.items.reduce((count, item) => count + item.quantity, 0)
}
}
}) |
|
В этом примере useCartStore использует другие хранилища для выполнения своих задач. При оформлении заказа он обращается к хранилищу пользователей для получения информации об авторизации и адресе доставки. При добавлении товара в корзину он взаимодействует с хранилищем товаров, чтобы получить информацию о добавляемом продукте.
Для крупных приложений эффективно разделять хранилища не только по функциональности, но и по уровням абстракции. Можно создать базовый уровень хранилищ, который взаимодействует непосредственно с API:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| // stores/api/users.js
export const useUsersApiStore = defineStore('users-api', {
actions: {
async fetchUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json())
},
async updateUser(id, data) {
// Логика запроса к API
}
}
})
// stores/user.js - хранилище уровня приложения использует API-хранилище
export const useUserStore = defineStore('user', {
state: () => ({
currentUser: null,
loading: false,
error: null
}),
actions: {
async loadCurrentUser() {
const apiStore = useUsersApiStore()
this.loading = true
try {
this.currentUser = await apiStore.fetchUser('me')
this.error = null
} catch (err) {
this.error = 'Не удалось загрузить данные пользователя'
console.error(err)
} finally {
this.loading = false
}
}
}
}) |
|
Такой многоуровневый подход упрощает тестирование, так как API-хранилище можно легко подменить моком в тестах.
Для комплексных задач удобно создавать композабельные функции (composables), которые объединяют функциональность нескольких хранилищ:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| // composables/useCheckout.js
import { ref, computed } from 'vue'
import { useCartStore } from '@/stores/cart'
import { useUserStore } from '@/stores/user'
import { useOrdersStore } from '@/stores/orders'
export function useCheckout() {
const cartStore = useCartStore()
const userStore = useUserStore()
const ordersStore = useOrdersStore()
const isProcessing = ref(false)
const error = ref(null)
const canCheckout = computed(() =>
userStore.isLoggedIn &&
cartStore.items.length > 0 &&
!isProcessing.value
)
async function processCheckout() {
if (!canCheckout.value) return false
isProcessing.value = true
error.value = null
try {
// Оформление заказа с использованием всех необходимых хранилищ
const orderData = await cartStore.prepareOrderData()
const newOrder = await ordersStore.createOrder(orderData)
await cartStore.clearCart()
return newOrder
} catch (err) {
error.value = 'Не удалось оформить заказ'
return false
} finally {
isProcessing.value = false
}
}
return {
isProcessing,
error,
canCheckout,
processCheckout
}
} |
|
Такие композабельные функции значительно упрощают работу с несколькими хранилищами в компонентах и делают код более понятным и тестируемым.
Модульность Pinia также отлично сочетается с ленивой загрузкой и разделением кода. Хранилища можно динамически импортировать только когда они нужны:
JavaScript | 1
2
3
4
5
6
7
8
| // Динамический импорт хранилища для админской панели
async function loadAdminPanel() {
const { useAdminStore } = await import('@/stores/admin')
const adminStore = useAdminStore()
await adminStore.loadDashboardData()
// Дальнейшая логика
} |
|
Этот подход улучшает производительность приложения, загружая только необходимые данные и код в нужный момент.
Практическое применение Pinia
Теория — это хорошо, но давайте перейдём к практическому применению Pinia в реальном проекте. Я расскажу, как грамотно создавать, структурировать и использовать хранилища для решения типичных задач фронтенд-разработки. Начнем с создания базового хранилища для аутентификации пользователей — задачи, с которой сталкивается практически любое приложение:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
| // stores/auth.js
import { defineStore } from 'pinia'
import router from '@/router'
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null,
token: localStorage.getItem('auth_token') || null,
loading: false,
error: null
}),
getters: {
isAuthenticated: (state) => !!state.token && !!state.user,
userRole: (state) => state.user?.role || 'guest'
},
actions: {
async login(credentials) {
this.loading = true
this.error = null
try {
// Имитация API-запроса
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.message || 'Ошибка аутентификации')
}
this.user = data.user
this.token = data.token
// Сохраняем токен в localStorage для персистентности
localStorage.setItem('auth_token', data.token)
// Перенаправляем на защищенную страницу
router.push({ name: 'dashboard' })
return true
} catch (error) {
this.error = error.message
return false
} finally {
this.loading = false
}
},
async logout() {
// Можно добавить запрос к API для инвалидации токена на сервере
this.user = null
this.token = null
localStorage.removeItem('auth_token')
// Перенаправляем на страницу входа
router.push({ name: 'login' })
},
async checkAuth() {
if (!this.token) return false
this.loading = true
try {
// Запрос для проверки токена и получения данных пользователя
const response = await fetch('/api/me', {
headers: { 'Authorization': [INLINE]Bearer ${this.token}[/INLINE] }
})
if (!response.ok) {
throw new Error('Сессия истекла')
}
const data = await response.json()
this.user = data.user
return true
} catch (error) {
this.user = null
this.token = null
localStorage.removeItem('auth_token')
this.error = 'Сессия истекла, пожалуйста, войдите снова'
return false
} finally {
this.loading = false
}
}
}
}) |
|
Это хранилище обрабатывает полный цикл аутентификации: вход в систему, выход и проверку существующего токена. В реальном проекте вы бы подключили его к вашему API и маршрутизатору. Использовать такое хранилище в компоненте очень просто:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| <template>
<form @submit.prevent="handleLogin" class="login-form">
<div v-if="authStore.error" class="error-message">
{{ authStore.error }}
</div>
<input v-model="email" type="email" placeholder="Email" required />
<input v-model="password" type="password" placeholder="Пароль" required />
<button type="submit" :disabled="authStore.loading">
{{ authStore.loading ? 'Вход...' : 'Войти' }}
</button>
</form>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const email = ref('')
const password = ref('')
async function handleLogin() {
await authStore.login({ email: email.value, password: password.value })
}
onMounted(() => {
// Проверяем авторизацию при загрузке компонента
authStore.checkAuth()
})
</script> |
|
При создании реальных приложений часто возникает потребность в хранилище для управления уведомлениями или системными сообщениями. Вот как это можно реализовать:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| // stores/notifications.js
import { defineStore } from 'pinia'
export const useNotificationStore = defineStore('notifications', {
state: () => ({
notifications: []
}),
actions: {
add(notification) {
// Создаём уникальный id для уведомления
const id = Date.now().toString()
// Добавляем уведомление в массив
this.notifications.push({
id,
message: notification.message,
type: notification.type || 'info',
timeout: notification.timeout || 3000,
...notification
})
// Автоматически удаляем уведомление через timeout
if (notification.timeout !== 0) {
setTimeout(() => {
this.remove(id)
}, notification.timeout || 3000)
}
return id
},
remove(id) {
const index = this.notifications.findIndex(n => n.id === id)
if (index !== -1) {
this.notifications.splice(index, 1)
}
},
// Вспомогательные методы для разных типов уведомлений
success(message, options = {}) {
return this.add({ message, type: 'success', ...options })
},
error(message, options = {}) {
return this.add({ message, type: 'error', ...options })
},
warning(message, options = {}) {
return this.add({ message, type: 'warning', ...options })
},
info(message, options = {}) {
return this.add({ message, type: 'info', ...options })
},
clearAll() {
this.notifications = []
}
}
}) |
|
Это универсальное хранилище для уведомлений можно использовать в любом компоненте, создавая гибкую и централизованную систему оповещений:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| <!-- NotificationsContainer.vue -->
<template>
<div class="notifications-container">
<transition-group name="notification">
<div
v-for="notification in notificationStore.notifications"
:key="notification.id"
:class="['notification', notification.type]"
>
<button class="close" @click="notificationStore.remove(notification.id)">×</button>
<div class="message">{{ notification.message }}</div>
</div>
</transition-group>
</div>
</template>
<script setup>
import { useNotificationStore } from '@/stores/notifications'
const notificationStore = useNotificationStore()
</script> |
|
Для крупных проектов, особенно при работе с данными из API, полезно создать базовое хранилище для управления сущностями:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
| // stores/entities.js
import { defineStore } from 'pinia'
export const useEntityStore = defineStore('entities', {
state: () => ({
entities: {},
loading: {},
errors: {}
}),
actions: {
setLoading(entityType, isLoading) {
this.loading[entityType] = isLoading
},
setError(entityType, error) {
this.errors[entityType] = error
},
setEntities(entityType, entities) {
// Создаём объект для хранения сущностей по id
const normalized = {}
entities.forEach(entity => {
normalized[entity.id] = entity
})
// Обновляем хранилище
this.entities[entityType] = normalized
},
addEntity(entityType, entity) {
if (!this.entities[entityType]) {
this.entities[entityType] = {}
}
this.entities[entityType][entity.id] = entity
},
updateEntity(entityType, entityId, updates) {
if (this.entities[entityType] && this.entities[entityType][entityId]) {
this.entities[entityType][entityId] = {
...this.entities[entityType][entityId],
...updates
}
}
},
removeEntity(entityType, entityId) {
if (this.entities[entityType] && this.entities[entityType][entityId]) {
delete this.entities[entityType][entityId]
}
}
},
getters: {
getEntitiesByType: (state) => (entityType) => {
return Object.values(state.entities[entityType] || {})
},
getEntityById: (state) => (entityType, id) => {
return state.entities[entityType]?.[id] || null
},
isLoading: (state) => (entityType) => {
return !!state.loading[entityType]
},
getError: (state) => (entityType) => {
return state.errors[entityType]
}
}
}) |
|
Такое универсальное хранилище можно использовать для любых типов сущностей в вашем приложении: пользователей, товаров, заказов и т.д. Оно организует данные в нормализованную структуру, что улучшает производительность при обновлении отдельных элементов.
Альтернативные подходы к управлению состоянием
Несмотря на всю мощь и гибкость Pinia, важно понимать, что не всегда требуется полноценная библиотека управления состоянием. Vue 3 предлагает несколько встроенных механизмов, которые в определённых случаях могут быть более подходящим решением.
Самый простой способ управления состоянием во Vue 3 – использование реактивного объекта, созданного за пределами компонентов:
JavaScript | 1
2
3
4
5
6
7
8
| // state.js
import { reactive } from 'vue'
export const state = reactive({
count: 0,
users: [],
isLoading: false
}) |
|
Такой файл можно импортировать в любом компоненте и использовать напрямую:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
| import { state } from './state'
export default {
setup() {
function increment() {
state.count++
}
return { state, increment }
}
} |
|
Этот подход прост и эффективен для небольших приложений, но ему не хватает структурированности для масштабных проектов.
Другой интересный подход – использование функции provide/inject , которая значительно улучшилась в Vue 3. Она позволяет создать локальное состояние на уровне компонента и сделать его доступным всем дочерним компонентам без необходимости передавать пропсы:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| // В родительском компоненте
import { provide, reactive } from 'vue'
export default {
setup() {
const state = reactive({
theme: 'light',
fontSize: 16
})
function toggleTheme() {
state.theme = state.theme === 'light' ? 'dark' : 'light'
}
// Предоставляем и состояние, и методы для его изменения
provide('appSettings', { state, toggleTheme })
return { state, toggleTheme }
}
}
// В любом дочернем компоненте
import { inject } from 'vue'
export default {
setup() {
const { state, toggleTheme } = inject('appSettings')
return { state, toggleTheme }
}
} |
|
Для случаев, когда вам нужна более структурированная реализация без использования внешних библиотек, можно создать композабл, имитирующий функциональность хранилища:
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| // useStore.js
import { reactive, readonly } from 'vue'
export function createStore(options) {
const state = reactive(options.state())
const store = {
state: readonly(state),
// Аналог геттеров
getters: {},
// Аналог действий
dispatch(action, payload) {
if (!options.actions || !options.actions[action]) {
console.error(`Действие "${action}" не определено`)
return
}
return options.actions[action](state, payload)
}
}
// Инициализация геттеров
if (options.getters) {
Object.keys(options.getters).forEach(name => {
Object.defineProperty(store.getters, name, {
get: () => options.getters[name](state)
})
})
}
return store
}
// Использование
const counterStore = createStore({
state: () => ({ count: 0 }),
getters: {
doubleCount: state => state.count * 2
},
actions: {
increment(state, amount = 1) {
state.count += amount
}
}
}) |
|
Такой подход даёт структуру, сходную с Pinia, но использует только встроенные возможности Vue.
При разработке микрофронтендов или модульных приложений может пригодиться ещё один интересный паттерн – событийный канал (Event Bus):
JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| // eventBus.js
import { ref, watchEffect } from 'vue'
export const createEventBus = () => {
const listeners = ref({})
const state = ref({})
const emit = (event, payload) => {
if (listeners.value[event]) {
listeners.value[event].forEach(callback => callback(payload))
}
}
const on = (event, callback) => {
if (!listeners.value[event]) {
listeners.value[event] = []
}
listeners.value[event].push(callback)
// Возвращаем функцию отписки
return () => {
listeners.value[event] = listeners.value[event].filter(
cb => cb !== callback
)
}
}
const setState = (key, value) => {
state.value[key] = value
emit(`state:${key}`, value)
}
const getState = (key) => state.value[key]
return { emit, on, setState, getState, state }
} |
|
В зависимости от сложности проекта и требований, каждый из этих подходов может быть оптимальным выбором. Главное – использовать тот инструмент, который лучше всего решает конкретную задачу, а не слепо следовать модным тенденциям.
Источники и дополнительные материалы
Для более глубокого погружения в тематику управления состоянием с использованием Vue 3, Composition API и Pinia предлагаю ознакомиться с проверенными и авторитетными источниками. Эти материалы помогут расширить понимание и освоить продвинутые техники разработки.
Официальная документация
Официальная документация — всегда первый и наиболее надежный источник информации:
Документация Vue.js 3 — содержит подробное описание реактивности, Composition API и других ключевых концепций.
Документация Pinia — предоставляет исчерпывающую информацию о создании хранилищ, их взаимодействии и рекомендуемых практиках.
Руководство по миграции с Vuex на Pinia — официальное руководство по переходу с одной системы управления состоянием на другую.
Книги
"Vue.js 3 Cookbook" — содержит практические рецепты по использованию Vue 3, включая работу с Composition API и управлением состоянием.
"Fullstack Vue 3" — полное руководство по созданию приложений на Vue 3 с использованием современных подходов и библиотек.
"Testing Vue.js Applications" — книга, посвященная тестированию Vue-приложений, включая тестирование хранилищ данных.
Видеокурсы и скринкасты
Курс "Vue 3 Mastery" — серия уроков, посвященных продвинутым темам Vue 3, включая работу с Pinia.
"Vue 3 + TypeScript" — курс, фокусирующийся на интеграции TypeScript и Vue 3.
"Building Large-Scale Vue Applications" — глубокий анализ архитектуры масштабных приложений на Vue.
Статьи и блоги
"State Management Patterns in Vue" — сравнительный анализ различных подходов к управлению состоянием.
"Performance Optimization in Pinia Stores" — практические советы по оптимизации производительности приложений, использующих Pinia.
"Testing Strategies for Vue 3 and Pinia" — рекомендации по написанию тестов для приложений с Pinia.
Инструменты и расширения
Vue DevTools — незаменимый инструмент для отладки Vue-приложений, поддерживающий как Vuex, так и Pinia.
Volar — расширение для VS Code, обеспечивающее поддержку Vue 3 и TypeScript.
Pinia DevTools Plugin — расширение для Vue DevTools, предоставляющее дополнительные возможности для работы с Pinia.
Примеры проектов
Vue 3 + Pinia Starter Template — готовый шаблон для быстрого старта разработки.
Shopping Cart Example — пример реализации корзины интернет-магазина с использованием Vue 3 и Pinia.
Authentication Flow — пример реализации системы аутентификации с использованием Vue 3, Vue Router и Pinia.
Сообщество и поддержка
GitHub репозиторий Pinia — место, где можно отслеживать обновления, сообщать о проблемах и вносить свой вклад в развитие библиотеки.
Исследования и тренды
Исследование "The State of Vue 2023" — ежегодный отчет о состоянии и трендах в экосистеме Vue.js.
Сравнительный анализ производительности различных решений для управления состоянием в современных фреймворках.
Эти ресурсы помогут вам не только освоить основы, но и стать экспертом в управлении состоянием Vue-приложений с использованием Composition API и Pinia. Регулярно обращайтесь к этим источникам, чтобы быть в курсе последних тенденций и лучших практик в быстро развивающейся области фронтенд-разработки.
Как воспользоваться API DonationAlerts во Vue js? Я хочу добавить на сайт список "топ донаты" из DonationAlerts. В DA есть такой виджет, но как из него взять эту информацию или как воспользоваться... Вывод даных с стороннего api в Vue Здраствуйте. Я на днях решил изучить Vue и создать сайт.
Я делаю запрос к стороннему api банка по получению курса валют. Вот пример ответа:
... Как вытянуть данные из Api, используя Vue? У меня есть таблица mark, color,
mark: id, title
color: id, title
В Api прописан скрипт получения и mark и color
/** Маршрутизатор Vue возвращает 404 при повторном обращении к api URL Например если с главной страницы пройти в католог то страница работает. Если обновить страницу то выдает 404.
В консоли
error get category... Ошибка Pinia Установил Pinia как положено через npm install pinia.
Затем зарегистрировал вот так:
import { createApp } from "vue";
import {... Управление состоянием объекта Например ситуация:
при столкновении объектов один меняет цвет и через 10 секунд меняет на другой или возвращается в исходное состояние
Как... Управление состоянием checkBox'а Здравствуйте.Подскажите пожалуйста как сделать такой момент:
Имеется 2 checkbox,как сделать так чтоб если первая checkbox не выбрана то вторая... Управление состоянием объекта Например ситуация:
при столкновении объектов один меняет цвет и через 10 секунд меняет на другой или возвращается в исходное состояние
Как... Управление состоянием дома через интернет Привет.
Жаль нету фотика щас, так бы показал что получилось.
Вообщем на atmega16 сделал простую схемку для подключения устройства к компу по... Управление состоянием кнопки с помощью таймера Ситуация:
1. при нажатии на кнопку, она становится красной, через 10 сек приходит в исходное состояние;
2. при нажатии на кнопку, она начинает... Управление состоянием триггера из храномой процедуры Всем здрасьте, пытаюсь решить такую задачу:
На таблицу есть два триггера отрабатывают на before insert, первый триггер генерирует id... Динамическое управление состоянием элементов меню в Windows приложении Уважаемые присутствующие :help:!
Если кто может, подскажите, в каком направлении нужно двигаться в решении такой задачки :
Обычное...
|