《Pinia实战》6.核心概念 插件

欢迎观看《Pinia实战》视频课程

    1. 插件
      1. 插件

由于有了底层 API 的支持,Pinia store 现在完全支持扩展。以下是你可以扩展的内容:

  1. 为 store 添加新的属性
  2. 定义 store 时增加新的选项
  3. 为 store 增加新的方法
  4. 包装现有的方法
  5. 改变甚至取消 action
  6. 实现副作用,如本地存储
  7. 仅应用插件于特定 store

插件是通过 pinia.use() 添加到 pinia 实例的。最简单的例子是通过返回一个对象将一个静态属性添加到所有 store。

import { createPinia } from 'pinia'

// 创建的每个 store 中都会添加一个名为 `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'

这对添加全局对象很有用,如路由器、modal 或 toast 管理器。

      1. 简介

Pinia 插件是一个函数,可以选择性地返回要添加到 store 的属性。它接收一个可选参数,即 context。

export function myPiniaPlugin(context) {

  context.pinia // 用 `createPinia()` 创建的 pinia。

  context.app // 用 `createApp()` 创建的当前应用(仅 Vue 3)。

  context.store // 该插件想扩展的 store

  context.options // 定义传给 `defineStore()` 的 store 的可选对象。

  // ...

}

然后用 pinia.use() 将这个函数传给 pinia:

pinia.use(myPiniaPlugin)

插件只会应用于在 pinia 传递给应用后创建的 store,否则它们不会生效。

      1. 扩展 Store

你可以直接通过在一个插件中返回包含特定属性的对象来为每个 store 都添加上特定属性:

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

你也可以直接在 store 上设置该属性,但可以的话,请使用返回对象的方法,这样它们就能被 devtools 自动追踪到:

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

  store.hello = 'world'

})

任何由插件返回的属性都会被 devtools 自动追踪,所以如果你想在 devtools 中调试 hello 属性,为了使 devtools 能追踪到 hello,请确保在 dev 模式下将其添加到 store._customProperties 中:

// 上文示例

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

  store.hello = 'world'

  // 确保你的构建工具能处理这个问题,webpack 和 vite 在默认情况下应该能处理。

  if (process.env.NODE_ENV === 'development') {

    // 添加你在 store 中设置的键值

    store._customProperties.add('hello')

  }

})

值得注意的是,每个 store 都被 reactive包装过,所以可以自动解包任何它所包含的 Ref(ref()、computed()...)。

const sharedRef = ref('shared')

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

  // 每个 store 都有单独的 `hello` 属性

  store.hello = ref('secret')

  // 它会被自动解包

  store.hello // 'secret'

  // 所有的 store 都在共享 `shared` 属性的值

  store.shared = sharedRef

  store.shared // 'shared'

})

这就是在没有 .value 的情况下你依旧可以访问所有计算属性的原因,也是它们为什么是响应式的原因。

        1. 添加新的 state

如果你想给 store 添加新的 state 属性或者在服务端渲染的激活过程中使用的属性,你必须同时在两个地方添加它。

  1. 在 store 上,然后你才可以用 store.myState 访问它。
  2. 在 store.$state 上,然后你才可以在 devtools 中使用它,并且,在 SSR 时被正确序列化(serialized)。

除此之外,你肯定也会使用 ref()(或其他响应式 API),以便在不同的读取中共享相同的值:

import { toRef, ref } from 'vue'

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

  // 为了正确地处理 SSR,我们需要确保我们没有重写任何一个

  // 现有的值

  if (!store.$state.hasOwnProperty('hasError')) {

    // 在插件中定义 hasError,因此每个 store 都有各自的

    // hasError 状态

    const hasError = ref(false)

    // 在 `$state` 上设置变量,允许它在 SSR 期间被序列化。

    store.$state.hasError = hasError

  }

  // 我们需要将 ref 从 state 转移到 store

  // 这样的话,两种方式:store.hasError 和 store.$state.hasError 都可以访问

  // 并且共享的是同一个变量

  // 查看 https://cn.vuejs.org/api/reactivity-utilities.html#toref

  store.hasError = toRef(store.$state, 'hasError')

  // 在这种情况下,最好不要返回 `hasError`

  // 因为它将被显示在 devtools 的 `state` 部分

  // 如果我们返回它,devtools 将显示两次。

})

需要注意的是,在一个插件中, state 变更或添加(包括调用 store.$patch())都是发生在 store 被激活之前,因此不会触发任何订阅函数。

WARNING

如果你使用的是 Vue 2,Pinia 与 Vue 一样,受限于相同的响应式限制。在创建新的 state 属性时,如 secret 和 hasError,你需要使用 Vue.set() (Vue 2.7) 或者 @vue/composition-api 的 set() (Vue < 2.7)。

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 里设置,这样你就可以访问它了。

    // 两种方式都可以:`store.$state.secret` / `store.secret`。

    set(store, 'secret', secretRef)

    store.secret // 'secret'

  }

})

        1. 重置插件中添加的 state

默认情况下,$reset() 不会重置插件添加的 state,但你可以重写它来重置你添加的 state:

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`) 设置为 store

  const originalReset = store.$reset.bind(store)

  // 覆写其 $reset 函数

  return {

    $reset() {

      originalReset()

      store.hasError = false

    },

  }

})

      1. 添加新的外部属性

当添加外部属性、第三方库的类实例或非响应式的简单值时,你应该先用 markRaw() 来包装一下它,再将它传给 pinia。下面是一个在每个 store 中添加路由器的例子:

import { markRaw } from 'vue'

// 根据你的路由器的位置来调整

import { router } from './router'

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

  store.router = markRaw(router)

})

      1. 在插件中调用 $subscribe

你也可以在插件中使用 store.$subscribe 和 store.$onAction 。

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

  store.$subscribe(() => {

    // 响应 store 变化

  })

  store.$onAction(() => {

    // 响应 store actions

  })

})

      1. 添加新的选项

在定义 store 时,可以创建新的选项,以便在插件中使用它们。例如,你可以创建一个 debounce 选项,允许你让任何 action 实现防抖。

defineStore('search', {

  actions: {

    searchContacts() {

      // ...

    },

  },

  // 这将在后面被一个插件读取

  debounce: {

    // 让 action searchContacts 防抖 300ms

    searchContacts: 300,

  },

})

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

// 使用任意防抖库

import debounce from 'lodash/debounce'

pinia.use(({ options, store }) => {

  if (options.debounce) {

    // 我们正在用新的 action 来覆盖这些 action

    return Object.keys(options.debounce).reduce((debouncedActions, action) => {

      debouncedActions[action] = debounce(

        store[action],

        options.debounce[action]

      )

      return debouncedActions

    }, {})

  }

})

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

defineStore(

  'search',

  () => {

    // ...

  },

  {

    // 这将在后面被一个插件读取

    debounce: {

      // 让 action searchContacts 防抖 300ms

      searchContacts: 300,

    },

  }

)

      1. TypeScript

上述一切功能都有类型支持,所以你永远不需要使用 any 或 @ts-ignore。

        1. 标注插件类型

一个 Pinia 插件可按如下方式实现类型标注:

import { PiniaPluginContext } from 'pinia'

export function myPiniaPlugin(context: PiniaPluginContext) {

  // ...

}

        1. 为新的 store 属性添加类型

当在 store 中添加新的属性时,你也应该扩展 PiniaCustomProperties 接口。

import 'pinia'

declare module 'pinia' {

  export interface PiniaCustomProperties {

    // 通过使用一个 setter,我们可以允许字符串和引用。

    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: we haven't typed this correctly

  store.simpleNumber = ref(Math.random())

})

PiniaCustomProperties 是一个通用类型,允许你引用 store 的属性。思考一下这个例子,如果把初始选项复制成 $options(这只对 option store 有效),如何标注类型:

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

我们可以通过使用 PiniaCustomProperties 的4种通用类型来标注类型:

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。下面是每个字母代表的含义:

  1. S: State
  2. G: Getters
  3. A: Actions
  4. SS: Setup Store / Store
        1. 为新的 state 添加类型

当添加新的 state 属性(包括 store 和 store.$state )时,你需要将类型添加到 PiniaCustomStateProperties 中。与 PiniaCustomProperties 不同的是,它只接收 State 泛型:

import 'pinia'

declare module 'pinia' {

  export interface PiniaCustomStateProperties<S> {

    hello: string

  }

}

        1. 为新的定义选项添加类型

当为 defineStore() 创建新选项时,你应该扩展 DefineStoreOptionsBase。与 PiniaCustomProperties 不同的是,它只暴露了两个泛型:State 和 Store 类型,允许你限制定义选项的可用类型。例如,你可以使用 action 的名称:

import 'pinia'

declare module 'pinia' {

  export interface DefineStoreOptionsBase<S, Store> {

    // 任意 action 都允许定义一个防抖的毫秒数

    debounce?: Partial<Record<keyof StoreActions<Store>, number>>

  }

}

// debouncePlugin.ts

import { PiniaPluginContext } from 'pinia'

import debounce from 'lodash/debounce'

export function debouncePlugin({ options, store }: PiniaPluginContext) {

    if (options.debounce) {

        // 遍历所有 debounce 配置

        Object.entries(options.debounce).forEach(([actionName, delay]) => {

            const originalAction = store[actionName]

            if (typeof originalAction === 'function' && typeof delay === 'number') {

                // 用防抖函数包裹原始 action

                store[actionName] = debounce(originalAction.bind(store), delay)

            }

        })

    }

}

TIP

还有一个可以从一个 store 类型中提取 getter 的 StoreGetters 类型。你也可以且只可以通过扩展 DefineStoreOptions 或 DefineSetupStoreOptions 类型来扩展 setup store 或 option store 的选项。

      1. Nuxt

当在 Nuxt 中使用 pinia 时,你必须先创建一个 Nuxt 插件。这样你才能访问到 pinia 实例:

// plugins/myPiniaPlugin.js

import { PiniaPluginContext } from 'pinia'

import { Plugin } from '@nuxt/types'

function MyPiniaPlugin({ store }: PiniaPluginContext) {

  store.$subscribe((mutation) => {

    // 响应 store 变更

    console.log(`[�� ${mutation.storeId}]: ${mutation.type}.`)

  })

  // 请注意,如果你使用的是 TS,则必须添加类型。

  return { creationTime: new Date() }

}

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

  $pinia.use(MyPiniaPlugin)

}

export default myPlugin

INFO

注意上面的例子使用的是 TypeScript。如果你使用的是 .js 文件,你必须删除类型标注 PiniaPluginContext 和 Plugin 以及它们的导入语句。

        1. Nuxt 2

如果你使用的是 Nuxt 2,其类型会稍有不同:

// plugins/myPiniaPlugin.ts

import { PiniaPluginContext } from 'pinia'

import { Plugin } from '@nuxt/types'

function MyPiniaPlugin({ store }: PiniaPluginContext) {

  store.$subscribe((mutation) => {

    // 响应 store 变更

    console.log(`[�� ${mutation.storeId}]: ${mutation.type}.`)

  })

  // 请注意,如果你使用的是 TS,则必须添加类型。

  return { creationTime: new Date() }

}

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

  $pinia.use(MyPiniaPlugin)

}

export default myPlugin

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值