Плагіни
Сховища Pinia можна повністю розширити завдяки API низького рівня. Ось список речей, які ви можете зробити:
- Додавати нові властивості до сховищ
- Додавати нові параметри під час визначення сховищ
- Додавати нові методи до сховищ
- Обгортати існуючі методи
- Перехоплювати дії та їх результати
- Застосовувати побічні ефекти, такі як Local Storage
- Застосовувати лише до певних сховищ
Плагіни додаються до екземпляра pinia за допомогою pinia.use(). Найпростішим прикладом є додавання статичної властивості до всіх сховищ шляхом повернення об'єкта:
import { createPinia } from 'pinia'
// додайте властивість під назвою `secret` до кожного сховища,
// який створюється після встановлення цього плагіна, це
// може бути в іншому файлі
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' }
}
const pinia = createPinia()
// передавання плагіна pinia
pinia.use(SecretPiniaPlugin)
// в іншому файлі
const store = useStore()
store.secret // 'the cake is a lie'import { createPinia } from 'pinia'
// додайте властивість під назвою `secret` до кожного сховища,
// який створюється після встановлення цього плагіна, це
// може бути в іншому файлі
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' }
}
const pinia = createPinia()
// передавання плагіна pinia
pinia.use(SecretPiniaPlugin)
// в іншому файлі
const store = useStore()
store.secret // 'the cake is a lie'Це корисно для додавання глобальних об'єктів, таких як маршрутизатор, модальний компонент або менеджер повідомлень.
Вступ
Плагін Pinia - це функція, яка за бажанням повертає властивості для додавання до сховища. Вона приймає один необов'язковий аргумент, context:
export function myPiniaPlugin(context) {
context.pinia // pinia, створена за допомогою `createPinia()`
context.app // поточний застосунок, створений за допомогою `createApp()` (лише Vue 3)
context.store // сховище, яке доповнено плагіном
context.options // об'єкт параметрів, що визначає сховище, передається в `defineStore()`
// ...
}export function myPiniaPlugin(context) {
context.pinia // pinia, створена за допомогою `createPinia()`
context.app // поточний застосунок, створений за допомогою `createApp()` (лише Vue 3)
context.store // сховище, яке доповнено плагіном
context.options // об'єкт параметрів, що визначає сховище, передається в `defineStore()`
// ...
}Потім ця функція передається до pinia за допомогою pinia.use():
pinia.use(myPiniaPlugin)pinia.use(myPiniaPlugin)Плагіни застосовуються лише до сховищ, створених після самих плагінів і після передачі pinia застосунку, інакше вони не застосовуватимуться.
Розширення сховища
Ви можете додати властивості до кожного сховища, просто повернувши їх об'єкт у плагін:
pinia.use(() => ({ hello: 'world' }))pinia.use(() => ({ hello: 'world' }))Ви також можете встановити властивість безпосередньо в store, але якщо можливо, використовуйте версії для повернення, щоб їх можна було автоматично відстежувати інструментами розробників:
pinia.use(({ store }) => {
store.hello = 'world'
})pinia.use(({ store }) => {
store.hello = 'world'
})Будь-яка властивість, яка повертається плагіном, автоматично відстежуватиметься інструментами розробника, тому, щоб зробити hello видимим у інструментах розробника, переконайтеся, що додали його до store._customProperties лише в режимі розробки, якщо ви хочете мати можливість налагодження його в інструментах розробки:
// з прикладу вище
pinia.use(({ store }) => {
store.hello = 'world'
// переконайтеся, що ваш компонувальник впорається з цим.
// webpack і vite мають робити це за замовчуванням
if (process.env. NODE_ENV === 'development') {
// додайте будь-які ключі, які ви встановили в сховищі
store._customProperties.add('hello')
}
})// з прикладу вище
pinia.use(({ store }) => {
store.hello = 'world'
// переконайтеся, що ваш компонувальник впорається з цим.
// webpack і vite мають робити це за замовчуванням
if (process.env.NODE_ENV === 'development') {
// додайте будь-які ключі, які ви встановили в сховищі
store._customProperties.add('hello')
}
})Зауважте, що кожне сховище обгорнуто reactive, автоматично розгортає будь-який Ref (ref(), computed(), ...), який воно містить:
const sharedRef = ref('shared')
pinia.use(({ store }) => {
// кожне сховище має окрему властивість `hello`
store.hello = ref('secret')
// воно автоматично розгортається
store.hello // 'secret'
// усі сховища спільно використовують властивість `shared`
store.shared = sharedRef
store.shared // 'shared'
})const sharedRef = ref('shared')
pinia.use(({ store }) => {
// кожне сховище має окрему властивість `hello`
store.hello = ref('secret')
// воно автоматично розгортається
store.hello // 'secret'
// усі сховища спільно використовують властивість `shared`
store.shared = sharedRef
store.shared // 'shared'
})Ось чому ви можете отримати доступ до всіх обчислених властивостей без .value і чому вони реактивні.
Додавання нового стану
Якщо ви хочете додати нові властивості стану до сховища або властивостей, які призначені для використання під час гідрації, вам треба додати їх у двох місцях:
- У
store, щоб ви могли отримати доступ до нього за допомогоюstore.myState - У
store.$state, щоб його можна було використовувати в інструментах розробника та серіалізувати під час SSR.
Крім того, вам, звичайно, доведеться використовувати ref() (або інший реактивний API), щоб поширити значення для різних доступів:
import { toRef, ref } from 'vue'
pinia.use(({ store }) => {
// щоб правильно обробити SSR, нам потрібно переконатися, що ми не
// перевизначаємо існуюче значення
if (!Object.prototype.hasOwnProperty(store.$state, 'hasError')) {
// визначено в плагіні, тому кожне сховище має окрему властивість стану
const hasError = ref(false)
// встановлення змінної у `$state` дозволяє її серіалізувати під час SSR
store.$state.hasError = hasError
}
// нам потрібно перенести посилання зі стану в сховище, таким чином обидва
// доступи: store.hasError і store.$state.hasError працюватимуть і спільно
// використовуватимуть ту саму змінну
// Дивіться https://ua.vuejs.org/api/reactivity-utilities.html#toref
store.hasError = toRef(store.$state, 'hasError')
// у цьому випадку краще не повертати `hasError`, оскільки воно все одно
// відображатиметься в розділі `state` в інструментах розробника, і якщо ми
// повернемо його, інструменти розробника відобразять його двічі.
})import { toRef, ref } from 'vue'
pinia.use(({ store }) => {
// щоб правильно обробити SSR, нам потрібно переконатися, що ми не
// перевизначаємо існуюче значення
if (!Object.prototype.hasOwnProperty(store.$state, 'hasError')) {
// визначено в плагіні, тому кожне сховище має окрему властивість стану
const hasError = ref(false)
// встановлення змінної у `$state` дозволяє її серіалізувати під час SSR
store.$state.hasError = hasError
}
// нам потрібно перенести посилання зі стану в сховище, таким чином обидва
// доступи: store.hasError і store.$state.hasError працюватимуть і спільно
// використовуватимуть ту саму змінну
// Дивіться https://ua.vuejs.org/api/reactivity-utilities.html#toref
store.hasError = toRef(store.$state, 'hasError')
// у цьому випадку краще не повертати `hasError`, оскільки воно все одно
// відображатиметься в розділі `state` в інструментах розробника, і якщо ми
// повернемо його, інструменти розробника відобразять його двічі.
})Зауважте, що зміни стану або доповнення, які відбуваються в плагіні (зокрема виклик store.$patch()), відбуваються до того, як сховище буде активним, тому не активують жодних підписок.
WARNING
Якщо ви використовуєте Vue 2, Pinia підпадає під ті самі застереження щодо реагування, що і Vue. Вам потрібно буде використовувати Vue.set() (Vue 2.7) або set() (з @vue/composition-api для Vue <2.7) для створення нових властивостей стану, таких як secret і hasError:
import { set, toRef } from '@vue/composition-api'
pinia.use(({ store }) => {
if (!Object.prototype.hasOwnProperty(store.$state, 'secret')) {
const secretRef = ref('secret')
// Якщо дані призначені для використання під час SSR, ви повинні встановити
// їх у властивості, щоб вони були серіалізовані та підібрані під час
// гідрації
set(store.$state, 'secret', secretRef)
}
// встановіть його також безпосередньо в сховищі, щоб ви могли отримати доступ
// до нього обома способами: `store.$state.secret` / `store.secret`
set(store, 'secret', toRef(store.$state, 'secret'))
store.secret // 'secret'
})import { set, toRef } from '@vue/composition-api'
pinia.use(({ store }) => {
if (!Object.prototype.hasOwnProperty(store.$state, 'secret')) {
const secretRef = ref('secret')
// Якщо дані призначені для використання під час SSR, ви повинні встановити
// їх у властивості, щоб вони були серіалізовані та підібрані під час
// гідрації
set(store.$state, 'secret', secretRef)
}
// встановіть його також безпосередньо в сховищі, щоб ви могли отримати доступ
// до нього обома способами: `store.$state.secret` / `store.secret`
set(store, 'secret', toRef(store.$state, 'secret'))
store.secret // 'secret'
})Скидання стану, доданого в плагінах
За замовчуванням $reset() не скидає стан, доданий плагінами, але ви можете змінити це, щоб також скинути доданий стан:
import { toRef, ref } from 'vue'
pinia.use(({ store }) => {
// це той самий код, що й вище для посилання
if (!Object.prototype.hasOwnProperty(store.$state, 'hasError')) {
const hasError = ref(false)
store.$state.hasError = hasError
}
store.hasError = toRef(store.$state, 'hasError')
// не забудьте встановити контекст (`this`) для сховища
const originalReset = store.$reset.bind(store)
// перевизначити функцію $reset
return {
$reset() {
originalReset()
store.hasError = false
}
}
})import { toRef, ref } from 'vue'
pinia.use(({ store }) => {
// це той самий код, що й вище для посилання
if (!Object.prototype.hasOwnProperty(store.$state, 'hasError')) {
const hasError = ref(false)
store.$state.hasError = hasError
}
store.hasError = toRef(store.$state, 'hasError')
// не забудьте встановити контекст (`this`) для сховища
const originalReset = store.$reset.bind(store)
// перевизначити функцію $reset
return {
$reset() {
originalReset()
store.hasError = false
}
}
})Додавання нових зовнішніх властивостей
Під час додавання зовнішніх властивостей, екземплярів класів, які надходять з інших бібліотек, або просто речей, які не є реактивними, ви повинні огорнути об'єкт за допомогою markRaw() перед тим, як передати його в pinia. Ось приклад додавання маршрутизатора до кожного сховища:
import { markRaw } from 'vue'
// адаптуйте це залежно від того, де знаходиться ваш маршрутизатор
import { router } from './router'
pinia.use(({ store }) => {
store.router = markRaw(router)
})import { markRaw } from 'vue'
// адаптуйте це залежно від того, де знаходиться ваш маршрутизатор
import { router } from './router'
pinia.use(({ store }) => {
store.router = markRaw(router)
})Виклик $subscribe всередині плагінів
Ви також можете використовувати store.$subscribe і store.$onAction у плагінах:
pinia.use(({ store }) => {
store.$subscribe(() => {
// реагувати на зміни сховища
})
store.$onAction(() => {
// реагувати на дії сховища
})
})pinia.use(({ store }) => {
store.$subscribe(() => {
// реагувати на зміни сховища
})
store.$onAction(() => {
// реагувати на дії сховища
})
})Додавання нових опцій
Під час визначення сховищ можна створювати нові параметри, щоб пізніше використовувати їх із плагінів. Наприклад, ви можете створити опцію debounce, яка дозволить вам відміняти будь-яку дію:
defineStore('search', {
actions: {
searchContacts() {
// ...
},
},
// це буде прочитано плагіном пізніше
debounce: {
// відкласти дію searchContacts на 300 мс
searchContacts: 300,
},
})defineStore('search', {
actions: {
searchContacts() {
// ...
},
},
// це буде прочитано плагіном пізніше
debounce: {
// відкласти дію searchContacts на 300 мс
searchContacts: 300,
},
})Потім плагін може прочитати цю опцію, щоб завершити дії та замінити оригінальні:
// використовуйте будь-яку бібліотеку debounce
import debounce from 'lodash/debounce'
pinia.use(({ options, store }) => {
if (options.debounce) {
// ми замінюємо дії новими
return Object.keys(options.debounce).reduce((debouncedActions, action) => {
debouncedActions[action] = debounce(
store[action],
options.debounce[action]
)
return debouncedActions
}, {})
}
})// використовуйте будь-яку бібліотеку debounce
import debounce from 'lodash/debounce'
pinia.use(({ options, store }) => {
if (options.debounce) {
// ми замінюємо дії новими
return Object.keys(options.debounce).reduce((debouncedActions, action) => {
debouncedActions[action] = debounce(
store[action],
options.debounce[action]
)
return debouncedActions
}, {})
}
})Зауважте, що спеціальні параметри передаються як 3-й аргумент під час використання синтаксису налаштування:
defineStore(
'search',
() => {
// ...
},
{
// це буде прочитано плагіном пізніше
debounce: {
// відкласти дію searchContacts на 300 мс
searchContacts: 300,
},
}
)defineStore(
'search',
() => {
// ...
},
{
// це буде прочитано плагіном пізніше
debounce: {
// відкласти дію searchContacts на 300 мс
searchContacts: 300,
},
}
)TypeScript
Усе, що показано вище, можна зробити за допомогою підтримки введення тексту, тож вам ніколи не потрібно використовувати any або @ts-ignore.
Typing plugins
Плагін Pinia можна ввести таким чином:
import { PiniaPluginContext } from 'pinia'
export function myPiniaPlugin(context: PiniaPluginContext) {
// ...
}import { PiniaPluginContext } from 'pinia'
export function myPiniaPlugin(context: PiniaPluginContext) {
// ...
}Введення нових властивостей сховища
Додаючи нові властивості до сховищ, ви також повинні розширити інтерфейс PiniaCustomProperties.
import 'pinia'
import type { Router } from 'vue-router'
declare module 'pinia' {
export interface PiniaCustomProperties {
// за допомогою сетера ми можемо дозволити як strings, так і refs
set hello(value: string | Ref<string>)
get hello(): string
// ви також можете визначити простіші значення
simpleNumber: number
// визначте тип маршрутизатора, доданий плагіном вище (#adding-new-external-properties)
router: Router
}
}import 'pinia'
import type { Router } from 'vue-router'
declare module 'pinia' {
export interface PiniaCustomProperties {
// за допомогою сетера ми можемо дозволити як strings, так і refs
set hello(value: string | Ref<string>)
get hello(): string
// ви також можете визначити простіші значення
simpleNumber: number
// визначте тип маршрутизатора, доданий плагіном вище (#adding-new-external-properties)
router: Router
}
}Потім його можна безпечно писати та читати:
pinia.use(({ store }) => {
store.hello = 'Hola'
store.hello = ref('Hola')
store.simpleNumber = Math.random()
// @ts-expect-error: ми вказали тип невірно
store.simpleNumber = ref(Math.random())
})pinia.use(({ store }) => {
store.hello = 'Hola'
store.hello = ref('Hola')
store.simpleNumber = Math.random()
// @ts-expect-error: ми вказали тип невірно
store.simpleNumber = ref(Math.random())
})PiniaCustomProperties - це загальний тип, який дозволяє посилатися на властивості сховища. Уявіть наступний приклад, де ми копіюємо початкові опції як $options (це працюватиме лише для опційних сховищ):
pinia.use(({ options }) => ({ $options: options }))pinia.use(({ options }) => ({ $options: options }))Ми можемо правильно типізувати це за допомогою 4 загальних типів PiniaCustomProperties:
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomProperties<Id, S, G, A> {
$options: {
id: Id
state?: () => S
getters?: G
actions?: A
}
}
}import 'pinia'
declare module 'pinia' {
export interface PiniaCustomProperties<Id, S, G, A> {
$options: {
id: Id
state?: () => S
getters?: G
actions?: A
}
}
}TIP
При розширенні типів у дженериках вони повинні бути названі точно так, як у вихідному коді. Id не може мати назву id або I, а S не може мати назву State. Ось що означає кожна літера:
- S: Стан
- G: Гетери
- A: Дії
- SS: Налаштовуване сховище / Сховище
Типізація нового стану
Додаючи нові властивості стану (до обох, store і store.$state), натомість вам потрібно додати тип до PiniaCustomStateProperties. На відміну від PiniaCustomProperties, він отримує лише загальний State:
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomStateProperties<S> {
hello: string
}
}import 'pinia'
declare module 'pinia' {
export interface PiniaCustomStateProperties<S> {
hello: string
}
}Типізація нових параметрів створення
Створюючи нові параметри для defineStore(), вам слід розширити DefineStoreOptionsBase. На відміну від PiniaCustomProperties, він надає лише два дженерики: State та Store типи, дозволяючи вам обмежити те, що можна визначити. Наприклад, ви можете використовувати назви дій:
import 'pinia'
declare module 'pinia' {
export interface DefineStoreOptionsBase<S, Store> {
// дозволяє визначити кількість мс для будь-якої дії
debounce?: Partial<Record<keyof StoreActions<Store>, number>>
}
}import 'pinia'
declare module 'pinia' {
export interface DefineStoreOptionsBase<S, Store> {
// дозволяє визначити кількість мс для будь-якої дії
debounce?: Partial<Record<keyof StoreActions<Store>, number>>
}
}TIP
Існує також тип StoreGetters для отримання гетерів з типу Store. Ви також можете розширити параметри setup сховищ або опційних stores лише шляхом розширення типів DefineStoreOptions і DefineSetupStoreOptions відповідно.
Nuxt.js
Якщо використовуєте pinia разом із Nuxt, спочатку вам потрібно створити плагін Nuxt. Це дасть вам доступ до екземпляра pinia:
// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'
function MyPiniaPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation) => {
// відреагувати на зміни сховища
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)
})
// Зауважте, що це має бути типізовано, якщо ви використовуєте TS
return { creationTime: new Date() }
}
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(MyPiniaPlugin)
})// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'
function MyPiniaPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation) => {
// відреагувати на зміни сховища
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)
})
// Зауважте, що це має бути типізовано, якщо ви використовуєте TS
return { creationTime: new Date() }
}
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(MyPiniaPlugin)
})Зауважте, що в наведеному вище прикладі використовується TypeScript, вам потрібно видалити анотації типу PiniaPluginContext і Plugin, а також їх імпорт, якщо ви використовуєте файл .js.
Nuxt.js 2
Якщо ви використовуєте Nuxt.js 2, типи дещо відрізняються:
// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'
import { Plugin } from '@nuxt/types'
function MyPiniaPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation) => {
// відреагувати на зміни сховища
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)
})
// Зауважте, що це має бути типізоване, якщо ви використовуєте TS
return { creationTime: new Date() }
}
const myPlugin: Plugin = ({ $pinia }) => {
$pinia.use(MyPiniaPlugin)
}
export default myPlugin// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'
import { Plugin } from '@nuxt/types'
function MyPiniaPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation) => {
// відреагувати на зміни сховища
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)
})
// Зауважте, що це має бути типізоване, якщо ви використовуєте TS
return { creationTime: new Date() }
}
const myPlugin: Plugin = ({ $pinia }) => {
$pinia.use(MyPiniaPlugin)
}
export default myPlugin