Skip to content
Перевод синхронизирован с документацией от , хэш коммита 59ec609.

Плагины

Хранилища Pinia можно полностью расширить благодаря низкоуровневому API. Вот список вещей, которые вы можете сделать:

  • Добавление новых свойств в хранилища
  • Добавление новых опций при определении хранилищ
  • Добавление новых методов в хранилища
  • Оборачивание существующих методов
  • Перехват действий и их результатов
  • Реализация таких побочных эффектов (side-effects), как Local Storage
  • Применять только к определенным хранилищам

Плагины добавляются в экземпляр pinia с помощью pinia.use(). Простейший пример - добавление статического свойства ко всем хранилищам, путев возврата объекта:

js
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 - это функция, которая по желанию возвращает свойства, добавляемые в хранилище. Она принимает один необязательный аргумент - контекст:

js
export function myPiniaPlugin(context) {
  context.pinia // pinia, созданное с помощью `createPinia()`
  context.app // текущее приложение, созданное с помощью `createApp()` (только для Vue 3)
  context.store // хранилище, которое дополняется плагином
  context.options // объект опций, переданный в `defineStore()`, определяющий хранилище
  // ...
}

Затем эта функция передается в pinia с помощью функции pinia.use():

js
pinia.use(myPiniaPlugin)

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

Расширение хранилища

Вы можете добавлять свойства в каждое хранилище, просто возвращая их объект в плагине:

js
pinia.use(() => ({ hello: 'world' }))

Вы также можете установить свойство напрямую в store, но по возможности используйте версию с возвратом, чтобы они могли быть автоматически отслеживаемыми с помощью devtools:

js
pinia.use(({ store }) => {
  store.hello = 'world'
})

Любое свойство, возвращаемое плагином, будет автоматически отслеживаться devtools. Чтобы сделать hello видимым в devtools, убедитесь, что добавили его в store._customProperties только в режиме разработки, если вы хотите отлаживать его в devtools:

js
// из примера выше
pinia.use(({ store }) => {
  store.hello = 'world'
  // убедитесь, что ваш сборщик обработает этот код. webpack и vite должны делать это по умолчанию
  if (process.env.NODE_ENV === 'development') {
    // добавьте любые ключи, которые вы установили в хранилище
    store._customProperties.add('hello')
  }
})

Обратите внимание, что каждое хранилище оборачивается в reactive, автоматически раскрывая любой Ref (ref(), computed(), ...), который оно содержит:

js
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, чтобы его можно было использовать в devtools и сериализовать во время SSR.

Кроме того, для разделения значения при разных обращениях к нему, конечно же, придется использовать ref() (или другой реактивный API):

js
import { toRef, ref } from 'vue'

pinia.use(({ store }) => {
  // чтобы правильно обработать SSR, нам нужно убедиться, что мы не
  // переопределяем существующее значение
  if (!store.$state.hasOwnProperty('hasError')) {
    // hasError определяется внутри плагина, поэтому каждое хранилище имеет свое
    // индивидуальное свойство состояния
    const hasError = ref(false)
    // установка переменной на `$state` позволяет сериализовать ее во время SSR
    store.$state.hasError = hasError
  }
  // нам нужно перенести ref-ссылку из состояния в хранилище, таким образом
  // оба доступа: store.hasError и store.$state.hasError будут работать
  // и совместо использовать одну и ту же переменную
  // См. https://vuejs.org/api/reactivity-utilities.html#toref
  store.hasError = toRef(store.$state, 'hasError')

  // в этом случае лучше не возвращать hasError, так как он
  // все равно будет отображаться в разделе state в devtools
  // и если мы его вернем, devtools отобразят его дважды.
})

Обратите внимание, что изменения состояние или его дополнения, которые происходят внутри плагина (включая вызов store.$patch()), происходят до того, как хранилище будет активным и, следовательно, не вызывают никаких подписок.

Предупреждение

Если вы используетеVue 2, Pinia подвержена тем же ограничениям реактивности, что и Vue. При создании новых свойств состояния, таких как secret и hasError, вам нужно будет использовать Vue.set() (для Vue 2.7) или set() (из @vue/composition-api для Vue <2.7):

js
import { set, toRef } from '@vue/composition-api'
pinia.use(({ store }) => {
  if (!store.$state.hasOwnProperty('secret')) {
    const secretRef = ref('secret')
    // Если данные предназначены для использования во время SSR, их следует
    // устанавливать в свойстве $state, чтобы они сериализовались и
    // подхватывались во время гидратации
    set(store.$state, 'secret', secretRef)
  }
  // установить его непосредственно в хранилище, чтобы вы могли получить к нему доступ
  // обоими способами: `store.$state.secret` / `store.secret`
  set(store, 'secret', toRef(store.$state, 'secret'))
  store.secret // 'secret'
})

Сброс состояния, добавленного в плагинах

По умолчанию $reset() не сбрасывает состояние, добавленное плагинами, но вы можете переопределить его, чтобы сбрасывалось и состояние, которое вы добавляете:

js
import { toRef, ref } from 'vue'

pinia.use(({ store }) => {
  // для справки, это тот же код, что и выше
  if (!store.$state.hasOwnProperty('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
    },
  }
})

Добавление новых внешних свойств

При добавлении внешних свойств, экземпляров классов, полученных из других библиотек, или просто вещей, не являющихся реактивными, перед передачей объекта в pinia следует обернуть его с помощью markRaw(). Приведем пример добавления роутера в каждое хранилище:

js
import { markRaw } from 'vue'
// адаптируйте это в зависимости от того, где находится ваш маршрутизатор
import { router } from './router'

pinia.use(({ store }) => {
  store.router = markRaw(router)
})

Вызов $subscribe внутри плагинов

Вы можете использовать store.$subscribe и store.$onAction и внутри плагинов:

ts
pinia.use(({ store }) => {
  store.$subscribe(() => {
    // реагировать на изменения в хранилище
  })
  store.$onAction(() => {
    // реагировать на дейстия в хранилище
  })
})

Добавление новых опций

При определении хранилищ можно создавать новые опции, чтобы впоследствии использовать их в плагинах. Например, можно создать опцию debounce, которая позволяет отменить любое действие:

js
defineStore('search', {
  actions: {
    searchContacts() {
      // ...
    },
  },

  // в дальнейшем это будет прочитано плагином
  debounce: {
    // задержать выполнение действия searchContacts на 300мс
    searchContacts: 300,
  },
})

Затем плагин может прочитать эту опцию для оборачивания действий и замены исходных:

js
// используйте любую библиотеку 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
    }, {})
  }
})

Обратите внимание, что при использовании setup-синтаксиса пользовательские опции передаются в качестве 3-го аргумента:

js
defineStore(
  'search',
  () => {
    // ...
  },
  {
    // в дальнейшем это будет прочитано плагином
    debounce: {
      // задержать выполнение действия searchContacts на 300мс
      searchContacts: 300,
    },
  }
)

TypeScript

Все, что показано выше, может быть сделано с поддержкой типизации, так что вам никогда не понадобится использовать any или @ts-ignore.

Типизация плагинов

Плагин Pinia может быть типизирован следующим образом:

ts
import { PiniaPluginContext } from 'pinia'

export function myPiniaPlugin(context: PiniaPluginContext) {
  // ...
}

Типизация новых свойств хранилища

При добавлении новых свойств в хранилища, необходимо также расширять интерфейс PiniaCustomProperties.

ts
import 'pinia'
import type { Router } from 'vue-router'

declare module 'pinia' {
  export interface PiniaCustomProperties {
    // используя сеттер, мы можем разрешить использование как строк, так и ref-ссылок
    set hello(value: string | Ref<string>)
    get hello(): string

    // можно определять и более простые значения
    simpleNumber: number

    // типизация роутера, добавленного плагином выше (#adding-new-external-properties)
    router: Router
  }
}

После этого его можно безопасно писать и читать:

ts
pinia.use(({ store }) => {
  store.hello = 'Hola'
  store.hello = ref('Hola')

  store.simpleNumber = Math.random()
  // @ts-expect-error: we haven't typed this correctly
  store.simpleNumber = ref(Math.random())
})

PiniaCustomProperties - это общий тип, позволяющий ссылаться на свойства хранилища. Представьте себе следующий пример, в котором мы копируем исходные опции как $options (это будет работать только для option-хранилищ):

ts
pinia.use(({ options }) => ({ $options: options }))

Мы можем правильно типизировать его, используя 4 дженерика PiniaCustomProperties:

ts
import 'pinia'

declare module 'pinia' {
  export interface PiniaCustomProperties<Id, S, G, A> {
    $options: {
      id: Id
      state?: () => S
      getters?: G
      actions?: A
    }
  }
}

Совет

При расширении типов в дженериках они должны быть названы точно так же, как в исходном коде. Id не может быть назван id или I, а S не может быть назван State. Вот что означает каждая буква:

  • S: Состояние
  • G: Геттеры
  • A: Действия
  • SS: Setup-хранилище / хранилище

Типизация нового состояния

При добавлении новых свойств состояния (как в store, так и в store.$state), вы должны добавить тип в PiniaCustomStateProperties вместо этого. В отличие от PiniaCustomProperties, в него передается только дженерик State:

ts
import 'pinia'

declare module 'pinia' {
  export interface PiniaCustomStateProperties<S> {
    hello: string
  }
}

Типизация новых опций создания

При создании новых опций для defineStore(), вы должны расширять DefineStoreOptionsBase. В отличие от PiniaCustomProperties, в него передаются только два дженерика: State и Store, позволяя вам ограничить то, что можно определить. Например, вы можете использовать названия действий:

ts
import 'pinia'

declare module 'pinia' {
  export interface DefineStoreOptionsBase<S, Store> {
    // позволяет определить тип number для мс для любого из действий
    debounce?: Partial<Record<keyof StoreActions<Store>, number>>
  }
}

Совет

Существует также тип StoreGetters для извлечения геттеров из типа Store. Вы также можете расширить опции setup-хранилищ или option-хранилищ только, расширив типы DefineStoreOptions и DefineSetupStoreOptions соответственно.

Nuxt.js

При использовании pinia вместе с Nuxt необходимо сначала создать Nuxt плагин. Это даст вам доступ к экземпляру pinia:

ts
// 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, поэтому при использовании файла .js необходимо удалить аннотации типов PiniaPluginContext и Plugin, а также их импорт.

Nuxt.js 2

Если вы используете Nuxt.js 2, то типы немного отличаются:

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

Released under the MIT License.