Pinia 插件

2023-09-28 15:00 更新

由于有了底層 API 的支持,Pinia store 現(xiàn)在完全支持?jǐn)U展。以下是你可以擴(kuò)展的內(nèi)容:

  • 為 store 添加新的屬性
  • 定義 store 時(shí)增加新的選項(xiàng)
  • 為 store 增加新的方法
  • 包裝現(xiàn)有的方法
  • 改變甚至取消 action
  • 實(shí)現(xiàn)副作用,如本地存儲(chǔ)
  • 應(yīng)用插件于特定 store

插件是通過 pinia.use() 添加到 pinia 實(shí)例的。最簡單的例子是通過返回一個(gè)對(duì)象將一個(gè)靜態(tài)屬性添加到所有 store。

import { createPinia } from 'pinia'


// 創(chuàng)建的每個(gè) store 中都會(huì)添加一個(gè)名為 `secret` 的屬性。
// 在安裝此插件后,插件可以保存在不同的文件中
function SecretPiniaPlugin() {
  return { secret: 'the cake is a lie' }
}


const pinia = createPinia()
// 將該插件交給 Pinia
pinia.use(SecretPiniaPlugin)


// 在另一個(gè)文件中
const store = useStore()
store.secret // 'the cake is a lie'

這對(duì)添加全局對(duì)象很有用,如路由器、modal 或 toast 管理器。

簡介

Pinia 插件是一個(gè)函數(shù),可以選擇性地返回要添加到 store 的屬性。它接收一個(gè)可選參數(shù),即 context。

export function myPiniaPlugin(context) {
  context.pinia // 用 `createPinia()` 創(chuàng)建的 pinia。 
  context.app // 用 `createApp()` 創(chuàng)建的當(dāng)前應(yīng)用(僅 Vue 3)。
  context.store // 該插件想擴(kuò)展的 store
  context.options // 定義傳給 `defineStore()` 的 store 的可選對(duì)象。
  // ...
}

然后用 pinia.use() 將這個(gè)函數(shù)傳給 pinia

pinia.use(myPiniaPlugin)

插件只會(huì)應(yīng)用于pinia 傳遞給應(yīng)用后創(chuàng)建的 store,否則它們不會(huì)生效。

擴(kuò)展 Store

你可以直接通過在一個(gè)插件中返回包含特定屬性的對(duì)象來為每個(gè) store 都添加上特定屬性:

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

你也可以直接在 store 上設(shè)置該屬性,但可以的話,請(qǐng)使用返回對(duì)象的方法,這樣它們就能被 devtools 自動(dòng)追蹤到

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

任何由插件返回的屬性都會(huì)被 devtools 自動(dòng)追蹤,所以如果你想在 devtools 中調(diào)試 hello 屬性,為了使 devtools 能追蹤到 hello,請(qǐng)確保在 dev 模式下將其添加到 store._customProperties 中:

// 上文示例
pinia.use(({ store }) => {
  store.hello = 'world'
  // 確保你的構(gòu)建工具能處理這個(gè)問題,webpack 和 vite 在默認(rèn)情況下應(yīng)該能處理。
  if (process.env.NODE_ENV === 'development') {
    // 添加你在 store 中設(shè)置的鍵值
    store._customProperties.add('hello')
  }
})

值得注意的是,每個(gè) store 都被 reactive包裝過,所以可以自動(dòng)解包任何它所包含的 Ref(ref()、computed()...)。

const sharedRef = ref('shared')
pinia.use(({ store }) => {
  // 每個(gè) store 都有單獨(dú)的 `hello` 屬性
  store.hello = ref('secret')
  // 它會(huì)被自動(dòng)解包
  store.hello // 'secret'


  // 所有的 store 都在共享 `shared` 屬性的值
  store.shared = sharedRef
  store.shared // 'shared'
})

這就是在沒有 .value 的情況下你依舊可以訪問所有計(jì)算屬性的原因,也是它們?yōu)槭裁词琼憫?yīng)式的原因。

添加新的 state

如果你想給 store 添加新的 state 屬性或者在服務(wù)端渲染的激活過程中使用的屬性,你必須同時(shí)在兩個(gè)地方添加它。。

  • store 上,然后你才可以用 store.myState 訪問它。
  • store.$state 上,然后你才可以在 devtools 中使用它,并且,在 SSR 時(shí)被正確序列化(serialized)

除此之外,你肯定也會(huì)使用 ref()(或其他響應(yīng)式 API),以便在不同的讀取中共享相同的值:

import { toRef, ref } from 'vue'


pinia.use(({ store }) => {
  // 為了正確地處理 SSR,我們需要確保我們沒有重寫任何一個(gè) 
  // 現(xiàn)有的值
  if (!Object.prototype.hasOwnProperty(store.$state, 'hasError')) {
    // 在插件中定義 hasError,因此每個(gè) store 都有各自的
    // hasError 狀態(tài)
    const hasError = ref(false)
    // 在 `$state` 上設(shè)置變量,允許它在 SSR 期間被序列化。
    store.$state.hasError = hasError
  }
  // 我們需要將 ref 從 state 轉(zhuǎn)移到 store
  // 這樣的話,兩種方式:store.hasError 和 store.$state.hasError 都可以訪問
  // 并且共享的是同一個(gè)變量
  // 查看 https://cn.vuejs.org/api/reactivity-utilities.html#toref
  store.hasError = toRef(store.$state, 'hasError')


  // 在這種情況下,最好不要返回 `hasError`
  // 因?yàn)樗鼘⒈伙@示在 devtools 的 `state` 部分
  // 如果我們返回它,devtools 將顯示兩次。
})

需要注意的是,在一個(gè)插件中, state 變更或添加(包括調(diào)用 store.$patch())都是發(fā)生在 store 被激活之前,因此不會(huì)觸發(fā)任何訂閱函數(shù)。

:::warning 如果你使用的是 Vue 2,Pinia 與 Vue 一樣,受限于相同的響應(yīng)式限制。在創(chuàng)建新的 state 屬性時(shí),如 secrethasError,你需要使用 Vue.set() (Vue 2.7) 或者 @vue/composition-apiset() (Vue < 2.7)。

import { set, toRef } from '@vue/composition-api'
pinia.use(({ store }) => {
  if (!Object.prototype.hasOwnProperty(store.$state, 'hello')) {
    const secretRef = ref('secret')
    // 如果這些數(shù)據(jù)是要在 SSR 過程中使用的
    // 你應(yīng)該將其設(shè)置在 `$state' 屬性上
    // 這樣它就會(huì)被序列化并在激活過程中被接收
    set(store.$state, 'secret', secretRef)
    // 直接在 store 里設(shè)置,這樣你就可以訪問它了。
    // 兩種方式都可以:`store.$state.secret` / `store.secret`。
    set(store, 'secret', secretRef)
    store.secret // 'secret'
  }
})

:::

添加新的外部屬性 %{#adding-new-external-properties}%

當(dāng)添加外部屬性、第三方庫的類實(shí)例或非響應(yīng)式的簡單值時(shí),你應(yīng)該先用 markRaw() 來包裝一下它,再將它傳給 pinia。下面是一個(gè)在每個(gè) store 中添加路由器的例子:

import { markRaw } from 'vue'
// 根據(jù)你的路由器的位置來調(diào)整
import { router } from './router'


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

在插件中調(diào)用 $subscribe

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

pinia.use(({ store }) => {
  store.$subscribe(() => {
    // 響應(yīng) store 變化
  })
  store.$onAction(() => {
    // 響應(yīng) store actions
  })
})

添加新的選項(xiàng)

在定義 store 時(shí),可以創(chuàng)建新的選項(xiàng),以便在插件中使用它們。例如,你可以創(chuàng)建一個(gè) debounce 選項(xiàng),允許你讓任何 action 實(shí)現(xiàn)防抖。

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


  // 這將在后面被一個(gè)插件讀取
  debounce: {
    // 讓 action searchContacts 防抖 300ms
    searchContacts: 300,
  },
})

然后,該插件可以讀取該選項(xiàng)來包裝 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 語法時(shí),自定義選項(xiàng)作為第 3 個(gè)參數(shù)傳遞:

defineStore(
  'search',
  () => {
    // ...
  },
  {
    // 這將在后面被一個(gè)插件讀取
    debounce: {
      // 讓 action searchContacts 防抖 300ms
      searchContacts: 300,
    },
  }
)

TypeScript

上述一切功能都有類型支持,所以你永遠(yuǎn)不需要使用 any@ts-ignore。

標(biāo)注插件類型

一個(gè) Pinia 插件可按如下方式實(shí)現(xiàn)類型標(biāo)注:

import { PiniaPluginContext } from 'pinia'


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

為新的 store 屬性添加類型

當(dāng)在 store 中添加新的屬性時(shí),你也應(yīng)該擴(kuò)展 PiniaCustomProperties 接口。

import 'pinia'


declare module 'pinia' {
  export interface PiniaCustomProperties {
    // 通過使用一個(gè) 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 是一個(gè)通用類型,允許你引用 store 的屬性。思考一下這個(gè)例子,如果把初始選項(xiàng)復(fù)制成 $options(這只對(duì) option store 有效),如何標(biāo)注類型:

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

我們可以通過使用 PiniaCustomProperties 的4種通用類型來標(biāo)注類型:

import 'pinia'


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

當(dāng)在泛型中擴(kuò)展類型時(shí),它們的名字必須與源代碼中完全一樣。Id 不能被命名為 idI ,S 不能被命名為 State。下面是每個(gè)字母代表的含義:

  • S: State
  • G: Getters
  • A: Actions
  • SS: Setup Store / Store

:::

為新的 state 添加類型

當(dāng)添加新的 state 屬性(包括 storestore.$state )時(shí),你需要將類型添加到 PiniaCustomStateProperties 中。與 PiniaCustomProperties 不同的是,它只接收 State 泛型:

import 'pinia'


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

為新的定義選項(xiàng)添加類型

當(dāng)為 defineStore() 創(chuàng)建新選項(xiàng)時(shí),你應(yīng)該擴(kuò)展 DefineStoreOptionsBase。與 PiniaCustomProperties 不同的是,它只暴露了兩個(gè)泛型:State 和 Store 類型,允許你限制定義選項(xiàng)的可用類型。例如,你可以使用 action 的名稱:

import 'pinia'


declare module 'pinia' {
  export interface DefineStoreOptionsBase<S, Store> {
    // 任意 action 都允許定義一個(gè)防抖的毫秒數(shù)
    debounce?: Partial<Record<keyof StoreActions<Store>, number>>
  }
}

:::tip 還有一個(gè)可以從一個(gè) store 類型中提取 getterStoreGetters 類型。你也可以且只可以通過擴(kuò)展 DefineStoreOptionsDefineSetupStoreOptions 類型來擴(kuò)展 setup storeoption store 的選項(xiàng)。 :::

Nuxt.js

當(dāng)在 Nuxt 中使用 pinia 時(shí),你必須先創(chuàng)建一個(gè) Nuxt 插件。這樣你才能訪問到 pinia 實(shí)例:

// plugins/myPiniaPlugin.js
import { PiniaPluginContext } from 'pinia'
import { Plugin } from '@nuxt/types'


function MyPiniaPlugin({ store }: PiniaPluginContext) {
  store.$subscribe((mutation) => {
    // 響應(yīng) store 變更
    console.log(`[???? ${mutation.storeId}]: ${mutation.type}.`)
  })


  // 請(qǐng)注意,如果你使用的是 TS,則必須添加類型。
  return { creationTime: new Date() }
}


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


export default myPlugin

注意上面的例子使用的是 TypeScript。如果你使用的是 .js 文件,你必須刪除類型標(biāo)注 PiniaPluginContextPlugin 以及它們的導(dǎo)入語句。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)