Pinia 插件

2023-09-28 15:00 更新

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

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

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

  1. import { createPinia } from 'pinia'
  2. // 創(chuàng)建的每個 store 中都會添加一個名為 `secret` 的屬性。
  3. // 在安裝此插件后,插件可以保存在不同的文件中
  4. function SecretPiniaPlugin() {
  5. return { secret: 'the cake is a lie' }
  6. }
  7. const pinia = createPinia()
  8. // 將該插件交給 Pinia
  9. pinia.use(SecretPiniaPlugin)
  10. // 在另一個文件中
  11. const store = useStore()
  12. store.secret // 'the cake is a lie'

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

簡介

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

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

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

  1. pinia.use(myPiniaPlugin)

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

擴展 Store

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

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

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

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

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

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

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

  1. const sharedRef = ref('shared')
  2. pinia.use(({ store }) => {
  3. // 每個 store 都有單獨的 `hello` 屬性
  4. store.hello = ref('secret')
  5. // 它會被自動解包
  6. store.hello // 'secret'
  7. // 所有的 store 都在共享 `shared` 屬性的值
  8. store.shared = sharedRef
  9. store.shared // 'shared'
  10. })

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

添加新的 state

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

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

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

  1. import { toRef, ref } from 'vue'
  2. pinia.use(({ store }) => {
  3. // 為了正確地處理 SSR,我們需要確保我們沒有重寫任何一個
  4. // 現(xiàn)有的值
  5. if (!Object.prototype.hasOwnProperty(store.$state, 'hasError')) {
  6. // 在插件中定義 hasError,因此每個 store 都有各自的
  7. // hasError 狀態(tài)
  8. const hasError = ref(false)
  9. // 在 `$state` 上設(shè)置變量,允許它在 SSR 期間被序列化。
  10. store.$state.hasError = hasError
  11. }
  12. // 我們需要將 ref 從 state 轉(zhuǎn)移到 store
  13. // 這樣的話,兩種方式:store.hasError 和 store.$state.hasError 都可以訪問
  14. // 并且共享的是同一個變量
  15. // 查看 https://cn.vuejs.org/api/reactivity-utilities.html#toref
  16. store.hasError = toRef(store.$state, 'hasError')
  17. // 在這種情況下,最好不要返回 `hasError`
  18. // 因為它將被顯示在 devtools 的 `state` 部分
  19. // 如果我們返回它,devtools 將顯示兩次。
  20. })

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

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

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

:::

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

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

  1. import { markRaw } from 'vue'
  2. // 根據(jù)你的路由器的位置來調(diào)整
  3. import { router } from './router'
  4. pinia.use(({ store }) => {
  5. store.router = markRaw(router)
  6. })

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

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

  1. pinia.use(({ store }) => {
  2. store.$subscribe(() => {
  3. // 響應(yīng) store 變化
  4. })
  5. store.$onAction(() => {
  6. // 響應(yīng) store actions
  7. })
  8. })

添加新的選項

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

  1. defineStore('search', {
  2. actions: {
  3. searchContacts() {
  4. // ...
  5. },
  6. },
  7. // 這將在后面被一個插件讀取
  8. debounce: {
  9. // 讓 action searchContacts 防抖 300ms
  10. searchContacts: 300,
  11. },
  12. })

然后,該插件可以讀取該選項來包裝 action,并替換原始 action:

  1. // 使用任意防抖庫
  2. import debounce from 'lodash/debounce'
  3. pinia.use(({ options, store }) => {
  4. if (options.debounce) {
  5. // 我們正在用新的 action 來覆蓋這些 action
  6. return Object.keys(options.debounce).reduce((debouncedActions, action) => {
  7. debouncedActions[action] = debounce(
  8. store[action],
  9. options.debounce[action]
  10. )
  11. return debouncedActions
  12. }, {})
  13. }
  14. })

注意,在使用 setup 語法時,自定義選項作為第 3 個參數(shù)傳遞:

  1. defineStore(
  2. 'search',
  3. () => {
  4. // ...
  5. },
  6. {
  7. // 這將在后面被一個插件讀取
  8. debounce: {
  9. // 讓 action searchContacts 防抖 300ms
  10. searchContacts: 300,
  11. },
  12. }
  13. )

TypeScript

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

標注插件類型

一個 Pinia 插件可按如下方式實現(xiàn)類型標注:

  1. import { PiniaPluginContext } from 'pinia'
  2. export function myPiniaPlugin(context: PiniaPluginContext) {
  3. // ...
  4. }

為新的 store 屬性添加類型

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

  1. import 'pinia'
  2. declare module 'pinia' {
  3. export interface PiniaCustomProperties {
  4. // 通過使用一個 setter,我們可以允許字符串和引用。
  5. set hello(value: string | Ref<string>)
  6. get hello(): string
  7. // 你也可以定義更簡單的值
  8. simpleNumber: number
  9. // 添加路由(#adding-new-external-properties)
  10. router: Router
  11. }
  12. }

然后,就可以安全地寫入和讀取它了:

  1. pinia.use(({ store }) => {
  2. store.hello = 'Hola'
  3. store.hello = ref('Hola')
  4. store.simpleNumber = Math.random()
  5. // @ts-expect-error: we haven't typed this correctly
  6. store.simpleNumber = ref(Math.random())
  7. })

PiniaCustomProperties 是一個通用類型,允許你引用 store 的屬性。思考一下這個例子,如果把初始選項復制成 $options(這只對 option store 有效),如何標注類型:

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

我們可以通過使用 PiniaCustomProperties 的4種通用類型來標注類型:

  1. import 'pinia'
  2. declare module 'pinia' {
  3. export interface PiniaCustomProperties<Id, S, G, A> {
  4. $options: {
  5. id: Id
  6. state?: () => S
  7. getters?: G
  8. actions?: A
  9. }
  10. }
  11. }

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

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

:::

為新的 state 添加類型

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

  1. import 'pinia'
  2. declare module 'pinia' {
  3. export interface PiniaCustomStateProperties<S> {
  4. hello: string
  5. }
  6. }

為新的定義選項添加類型

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

  1. import 'pinia'
  2. declare module 'pinia' {
  3. export interface DefineStoreOptionsBase<S, Store> {
  4. // 任意 action 都允許定義一個防抖的毫秒數(shù)
  5. debounce?: Partial<Record<keyof StoreActions<Store>, number>>
  6. }
  7. }

:::tip 還有一個可以從一個 store 類型中提取 getterStoreGetters 類型。你也可以且只可以通過擴展 DefineStoreOptionsDefineSetupStoreOptions 類型來擴展 setup storeoption store 的選項。 :::

Nuxt.js

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

  1. // plugins/myPiniaPlugin.js
  2. import { PiniaPluginContext } from 'pinia'
  3. import { Plugin } from '@nuxt/types'
  4. function MyPiniaPlugin({ store }: PiniaPluginContext) {
  5. store.$subscribe((mutation) => {
  6. // 響應(yīng) store 變更
  7. console.log(`[???? ${mutation.storeId}]: ${mutation.type}.`)
  8. })
  9. // 請注意,如果你使用的是 TS,則必須添加類型。
  10. return { creationTime: new Date() }
  11. }
  12. const myPlugin: Plugin = ({ $pinia }) => {
  13. $pinia.use(MyPiniaPlugin)
  14. }
  15. export default myPlugin

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號