Вступление
Pinia начиналась как эксперимент по перепроектированию того, как может выглядеть хранилище для Vue с Composition API примерно в ноябре 2019 года. С тех пор начальные принципы остались неизменными, но Pinia работает как для Vue 2, так и для Vue 3 и не требует использования composition API. API одинаков для обоих вариантов, за исключением установки и SSR, и эта документация ориентирована на Vue 3 с примечаниями для Vue 2, когда это необходимо, чтобы ее могли читать пользователи Vue 2 и Vue 3!
Почему вы должны использовать Pinia?
Pinia - это библиотека для создания хранилища для Vue, которая позволяет вам совместно использовать состояние между компонентами/страницами. Если вы знакомы с Composition API, вы, возможно, думаете, что уже можете совместно использовать глобальное состояние с помощью простого export const state = reactive({})
. Это верно для одностраничных приложений (SPA), но при использовании рендеринга на стороне сервера (SSR) это может привести к уязвимостям безопасности. Но даже в небольших одностраничных приложениях вы получаете много возможностей от использования Pinia:
- Утилиты для тестирования
- Плагины: расширение возможностей Pinia с помощью плагинов
- Полноценная поддержка TypeScript или автозаполнение для пользователей JS
- Поддержка рендеринга на стороне сервера (SSR)
- Поддержка Devtools
- Хронология для отслеживания действий, мутаций
- Хранилища отображаются в компонентах, в которых они используются
- Легкая отладка с возможностью перемещения во времени
- Горячая замена модулей (HMR)
- Изменение вашего хранилища без перезагрузки страницы
- Сохранение существующего состояния в процессе разработки
Если вы всё ещё сомневаетесь, ознакомьтесь с официальным курсом Mastering Pinia. В самом начале мы рассмотрим, как создать собственную функцию defineStore()
, а затем перейдем к официальному Pinia API.
Простой пример
Вот как выглядит использование Pinia в терминах его API (не забудьте посмотреть руководство по началу работы, чтобы получить полную инструкцию). Начнем с создания хранилища:
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => {
return { count: 0 }
},
// также может быть объявлено как
// state: () => ({ count: 0 })
actions: {
increment() {
this.count++
},
},
})
Затем вы используете его в компоненте:
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
counter.count++
// автозаполнение ✨
counter.$patch({ count: counter.count + 1 })
// или используете действие (action) вместо этого:
counter.increment()
</script>
<template>
<!-- Обращайтесь к состоянию напрямую из хранилища -->
<div>Current Count: {{ counter.count }}</div>
</template>
Вы даже можете использовать функцию (аналогичную setup()
в компоненте) для определения хранилища для более сложных случаев использования:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
})
Если вы еще не знакомы с setup()
и Composition API, не переживайте, Pinia также поддерживает набор map-помощников, аналогичных Vuex. Вы определяете хранилища так же, как и раньше, но затем используете mapStores()
, mapState()
, или mapActions()
:
const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
const useUserStore = defineStore('user', {
// ...
})
export default defineComponent({
computed: {
// другие вычисляемые свойства
// ...
// предоставляет доступ к this.counterStore и this.userStore
...mapStores(useCounterStore, useUserStore),
// предоставляет доступ только для чтения к this.count и this.double
...mapState(useCounterStore, ['count', 'double']),
},
methods: {
// предоставляет доступ к this.increment()
...mapActions(useCounterStore, ['increment']),
},
})
Более подробную информацию о каждом из помощников для отображения (map helpers) вы найдете в разделе "Основные концепции".
Официальный курс
Официальный курс по Pinia - Mastering Pinia. Написанный автором Pinia, он охватывает всё, начиная с основ и заканчивая продвинутыми темами, такими как плагины, тестирование и рендеринг на стороне сервера. Это лучший способ начать работу с Pinia и освоить её.
Почему Pinia
Pinia (произносится /piːnjʌ/
, как "peenya" на английском) - это слово, наиболее близкое к "piña" (ананас на испанском), которое является допустимым именем пакета. На самом деле ананас - это группа отдельных цветов, которые объединяются, создавая несколько плодов. Подобно хранилищам, каждый из них рождается отдельно, но в конечном итоге все они соединяются. Это также вкусный тропический фрукт, происходящий из Южной Америки.
Более реалистичный пример
Вот более полный пример использования API, которое вы будете использовать с Pinia с типами даже в JavaScript. Для некоторых людей этого может быть достаточно, чтобы начать, даже без дальнейшего чтения, но мы всё же рекомендуем ознакомиться с остальной документацией или даже пропустить этот пример и вернуться к нему после прочтения всех Основных концепций.
import { defineStore } from 'pinia'
export const useTodos = defineStore('todos', {
state: () => ({
/** @type {{ text: string, id: number, isFinished: boolean }[]} */
todos: [],
/** @type {'all' | 'finished' | 'unfinished'} */
filter: 'all',
// тип будет автоматически приведен к числу
nextId: 0,
}),
getters: {
finishedTodos(state) {
// автозаполнение! ✨
return state.todos.filter((todo) => todo.isFinished)
},
unfinishedTodos(state) {
return state.todos.filter((todo) => !todo.isFinished)
},
/**
* @returns {{ text: string, id: number, isFinished: boolean }[]}
*/
filteredTodos(state) {
if (this.filter === 'finished') {
// вызов других геттеров (getters) с автозаполнением ✨
return this.finishedTodos
} else if (this.filter === 'unfinished') {
return this.unfinishedTodos
}
return this.todos
},
},
actions: {
// любое количество аргументов, возвращает Promise или нет
addTodo(text) {
// можно изменять состояние напрямую
this.todos.push({ text, id: this.nextId++, isFinished: false })
},
},
})
Сравнение с Vuex
Pinia начала свой путь как исследование того, как может выглядеть следующая версия Vuex, включая множество идей, обсуждаемых в команде разработчиков для Vuex 5. В конечном итоге мы поняли, что Pinia уже реализует большую часть того, что мы хотели видеть в Vuex 5, и решили рекомендовать Pinia вместо него.
По сравнению с Vuex, Pinia предоставляет более простой API с меньшим количеством формальностей, предлагает API в стиле Composition API и, что самое важное, обеспечивает надежную поддержку вывода типов при использовании TypeScript.
RFCs
Изначально Pinia не проходила через процесс RFC (Request for Comments). Я тестировал идеи, опираясь на свой опыт разработки приложений, изучал код других разработчиков, работу с клиентами, использующими Pinia, и ответы на вопросы в Discord. Это позволило мне предложить решение, которое работает и адаптировано к различным случаям и размерам приложений. Ранее я часто публиковал обновления и развивал библиотеку, сохраняя её основной API неизменным.
Теперь, когда Pinia стала рекомендуемым средством управления состоянием, она подлежит тому же процессу RFC, что и другие основные библиотеки в экосистеме Vue, и её API находится в стабильном состоянии.
Сравнение с Vuex 3.x/4.x
Vuex 3.x - это Vuex для Vue 2, а Vuex 4.x - для Vue 3
API Pinia сильно отличается от Vuex ≤4, а именно:
- мутаций (mutations) больше не существуют. Их часто воспринимали как чрезмерно многословные. Изначально они вносили интеграцию с Devtools, но теперь это больше не является проблемой.
- Не нужно создавать сложные пользовательские обёртки для поддержки TypeScript; всё типизировано, и API разработано таким образом, чтобы максимально использовать вывод типов в TypeScript.
- Больше нет магических строк для инъекции; импортируйте функции, вызывайте их и наслаждайтесь автозаполнением!
- Нет необходимости динамически добавлять хранилища; они все являются динамическими по умолчанию, и вы даже не заметите этого. Обратите внимание, что вы всё равно можете вручную использовать хранилище для его регистрации в любое время, но поскольку это происходит автоматически, вам не нужно об этом беспокоиться.
- Больше нет вложенного структурирования модулей (modules). Вы по-прежнему можете неявно вкладывать хранилища, импортируя и используя одно хранилище внутри другого, но Pinia предлагает плоскую структуру по дизайну, сохраняя при этом возможности перекрестной композиции между хранилищами. Вы даже можете иметь циклические зависимости между хранилищами.
- Нет модулей с пространством имён (namespaced modules). Учитывая плоскую архитектуру хранилищ, "пространство имён" хранилищ встроено в то, как они определяются, и можно сказать, что все хранилища имеют свои собственные пространства имён.
Для более подробных инструкций о том, как преобразовать существующий проект с использованием Vuex ≤4 для использования Pinia, смотрите Руководство по миграции с Vuex.