跳至内容

插件

Pinia Store 可以通过低级 API 完全扩展。以下列出了您可以执行的操作

  • 向 Store 添加新属性
  • 在定义 Store 时添加新选项
  • 向 Store 添加新方法
  • 包装现有方法
  • 拦截 Action 及其结果
  • 实现副作用,例如 本地存储
  • 仅应用于特定 Store

插件使用 pinia.use() 添加到 pinia 实例中。最简单的示例是通过返回一个对象来向所有 Store 添加一个静态属性

js
import { createPinia } from 'pinia'

// add a property named `secret` to every store that is created
// after this plugin is installed this could be in a different file
function SecretPiniaPlugin() {
  return { secret: 'the cake is a lie' }
}

const pinia = createPinia()
// give the plugin to pinia
pinia.use(SecretPiniaPlugin)

// in another file
const store = useStore()
store.secret // 'the cake is a lie'

这对于添加全局对象(如路由器、模态框或吐司管理器)很有用。

介绍

Pinia 插件是一个函数,它可以选择性地返回要添加到 Store 的属性。它接受一个可选参数,即上下文

js
export function myPiniaPlugin(context) {
  context.pinia // the pinia created with `createPinia()`
  context.app // the current app created with `createApp()` (Vue 3 only)
  context.store // the store the plugin is augmenting
  context.options // the options object defining the store passed to `defineStore()`
  // ...
}

然后,此函数使用 pinia.use() 传递给 pinia

js
pinia.use(myPiniaPlugin)

插件仅应用于在插件本身之后以及将 pinia 传递给应用程序之后创建的 Store,否则它们将不会被应用。

增强 Store

您可以通过在插件中简单地返回一个包含这些属性的对象来向每个 Store 添加属性

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

您也可以直接在 store 上设置属性,但如果可能,请使用返回版本,以便它们可以由 devtools 自动跟踪

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

插件返回的任何属性都将由 devtools 自动跟踪,因此为了使 hello 在 devtools 中可见,请确保在仅限开发模式下将其添加到 store._customProperties 中,如果您想在 devtools 中调试它

js
// from the example above
pinia.use(({ store }) => {
  store.hello = 'world'
  // make sure your bundler handle this. webpack and vite should do it by default
  if (process.env.NODE_ENV === 'development') {
    // add any keys you set on the store
    store._customProperties.add('hello')
  }
})

请注意,每个 Store 都使用 reactive 包装,自动解包它包含的任何 Ref(ref()computed()、...)

js
const sharedRef = ref('shared')
pinia.use(({ store }) => {
  // each store has its individual `hello` property
  store.hello = ref('secret')
  // it gets automatically unwrapped
  store.hello // 'secret'

  // all stores are sharing the value `shared` property
  store.shared = sharedRef
  store.shared // 'shared'
})

这就是为什么您可以访问所有计算属性而无需 .value 以及它们为何是响应式的。

添加新状态

如果您想向 Store 添加新的状态属性或在水合过程中要使用的属性,您需要在两个地方添加它

  • store 上,以便您可以使用 store.myState 访问它
  • store.$state 上,以便它可以在 devtools 中使用,并且在 SSR 期间被序列化

最重要的是,您肯定需要使用 ref()(或其他响应式 API)才能在不同的访问中共享值

js
import { toRef, ref } from 'vue'

pinia.use(({ store }) => {
  // to correctly handle SSR, we need to make sure we are not overriding an
  // existing value
  if (!store.$state.hasOwnProperty('hasError')) {
    // hasError is defined within the plugin, so each store has their individual
    // state property
    const hasError = ref(false)
    // setting the variable on `$state`, allows it be serialized during SSR
    store.$state.hasError = hasError
  }
  // we need to transfer the ref from the state to the store, this way
  // both accesses: store.hasError and store.$state.hasError will work
  // and share the same variable
  // See https://vuejs.ac.cn/api/reactivity-utilities.html#toref
  store.hasError = toRef(store.$state, 'hasError')

  // in this case it's better not to return `hasError` since it
  // will be displayed in the `state` section in the devtools
  // anyway and if we return it, devtools will display it twice.
})

请注意,在插件中发生的任何状态更改或添加(包括调用 store.$patch())都发生在 Store 处于活动状态之前,因此不会触发任何订阅

警告

如果您使用的是Vue 2,Pinia 会受到与 Vue 相同的响应式性注意事项。您需要使用 Vue.set()(Vue 2.7)或 set()(来自 @vue/composition-api 用于 Vue <2.7)来创建新的状态属性,例如 secrethasError

js
import { set, toRef } from '@vue/composition-api'
pinia.use(({ store }) => {
  if (!store.$state.hasOwnProperty('secret')) {
    const secretRef = ref('secret')
    // If the data is meant to be used during SSR, you should
    // set it on the `$state` property so it is serialized and
    // picked up during hydration
    set(store.$state, 'secret', secretRef)
  }
  // set it directly on the store too so you can access it
  // both ways: `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 }) => {
  // this is the same code as above for reference
  if (!store.$state.hasOwnProperty('hasError')) {
    const hasError = ref(false)
    store.$state.hasError = hasError
  }
  store.hasError = toRef(store.$state, 'hasError')

  // make sure to set the context (`this`) to the store
  const originalReset = store.$reset.bind(store)

  // override the $reset function
  return {
    $reset() {
      originalReset()
      store.hasError = false
    },
  }
})

添加新的外部属性

在添加外部属性、来自其他库的类实例或只是非响应式内容时,您应该在将对象传递给 pinia 之前使用 markRaw() 包装该对象。以下是在每个 Store 中添加路由器的示例

js
import { markRaw } from 'vue'
// adapt this based on where your router is
import { router } from './router'

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

在插件中调用 $subscribe

您也可以在插件中使用 store.$subscribestore.$onAction

ts
pinia.use(({ store }) => {
  store.$subscribe(() => {
    // react to store changes
  })
  store.$onAction(() => {
    // react to store actions
  })
})

添加新选项

在定义 Store 时创建新选项以供插件稍后使用是可能的。例如,您可以创建一个 debounce 选项,它允许您对任何 Action 进行防抖

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

  // this will be read by a plugin later on
  debounce: {
    // debounce the action searchContacts by 300ms
    searchContacts: 300,
  },
})

然后,插件可以读取该选项以包装 Action 并替换原始 Action

js
// use any debounce library
import debounce from 'lodash/debounce'

pinia.use(({ options, store }) => {
  if (options.debounce) {
    // we are overriding the actions with new ones
    return Object.keys(options.debounce).reduce((debouncedActions, action) => {
      debouncedActions[action] = debounce(
        store[action],
        options.debounce[action]
      )
      return debouncedActions
    }, {})
  }
})

请注意,自定义选项在使用 setup 语法时作为第三个参数传递

js
defineStore(
  'search',
  () => {
    // ...
  },
  {
    // this will be read by a plugin later on
    debounce: {
      // debounce the action searchContacts by 300ms
      searchContacts: 300,
    },
  }
)

TypeScript

上面显示的所有内容都可以使用类型支持来完成,因此您永远不需要使用 any@ts-ignore

键入插件

Pinia 插件可以按如下方式键入

ts
import { PiniaPluginContext } from 'pinia'

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

键入新的 Store 属性

在向 Store 添加新属性时,您还应该扩展 PiniaCustomProperties 接口。

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

declare module 'pinia' {
  export interface PiniaCustomProperties {
    // by using a setter we can allow both strings and refs
    set hello(value: string | Ref<string>)
    get hello(): string

    // you can define simpler values too
    simpleNumber: number

    // type the router added by the plugin above (#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 是一个泛型类型,它允许您引用 Store 的属性。想象一下以下示例,我们将在其中将初始选项复制为 $options(这仅适用于选项 Store)

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

我们可以通过使用 PiniaCustomProperties 的 4 个泛型类型来正确地键入它

ts
import 'pinia'

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

提示

在泛型中扩展类型时,它们必须与源代码中的名称完全相同Id 不能命名为 idIS 不能命名为 State。以下是每个字母代表的内容

  • S: 状态
  • G: Getter
  • A: Action
  • SS: 设置 Store / Store

键入新的状态

在添加新的状态属性(到 storestore.$state)时,您需要将类型添加到 PiniaCustomStateProperties 中。与 PiniaCustomProperties 不同,它只接收 State 泛型

ts
import 'pinia'

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

键入新的创建选项

在为 defineStore() 创建新选项时,您应该扩展 DefineStoreOptionsBase。与 PiniaCustomProperties 不同,它只公开两个泛型:State 和 Store 类型,允许您限制可以定义的内容。例如,您可以使用 Action 的名称

ts
import 'pinia'

declare module 'pinia' {
  export interface DefineStoreOptionsBase<S, Store> {
    // allow defining a number of ms for any of the actions
    debounce?: Partial<Record<keyof StoreActions<Store>, number>>
  }
}

提示

还有一个 StoreGetters 类型用于从 Store 类型中提取Getter。您还可以通过分别扩展类型 DefineStoreOptionsDefineSetupStoreOptions 来扩展设置 Store选项 Store 的选项。

Nuxt.js

将 pinia 与 Nuxt 一起使用时,您需要先创建一个Nuxt 插件。这将使您可以访问 pinia 实例

ts
// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'

function MyPiniaPlugin({ store }: PiniaPluginContext) {
  store.$subscribe((mutation) => {
    // react to store changes
    console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)
  })

  // Note this has to be typed if you are using TS
  return { creationTime: new Date() }
}

export default defineNuxtPlugin(({ $pinia }) => {
  $pinia.use(MyPiniaPlugin)
})

信息

上面的示例使用的是 TypeScript,如果您使用的是 .js 文件,则必须删除类型注释 PiniaPluginContextPlugin 以及它们的导入。

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) => {
    // react to store changes
    console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)
  })

  // Note this has to be typed if you are using TS
  return { creationTime: new Date() }
}

const myPlugin: Plugin = ({ $pinia }) => {
  $pinia.use(MyPiniaPlugin)
}

export default myPlugin