由于有了底層 API 的支持,Pinia store 現(xiàn)在完全支持?jǐn)U展。以下是你可以擴(kuò)展的內(nèi)容:
插件是通過 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ì)生效。
你可以直接通過在一個(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)式的原因。
如果你想給 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í),如 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 (!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'
}
})
:::
當(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)
})
$subscribe
你也可以在插件中使用 store.$subscribe 和 store.$onAction 。
pinia.use(({ store }) => {
store.$subscribe(() => {
// 響應(yīng) store 變化
})
store.$onAction(() => {
// 響應(yīng) store actions
})
})
在定義 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,
},
}
)
上述一切功能都有類型支持,所以你永遠(yuǎn)不需要使用 any
或 @ts-ignore
。
一個(gè) Pinia 插件可按如下方式實(shí)現(xiàn)類型標(biāo)注:
import { PiniaPluginContext } from 'pinia'
export function myPiniaPlugin(context: PiniaPluginContext) {
// ...
}
當(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
不能被命名為 id
或 I
,S
不能被命名為 State
。下面是每個(gè)字母代表的含義:
:::
當(dāng)添加新的 state 屬性(包括 store
和 store.$state
)時(shí),你需要將類型添加到 PiniaCustomStateProperties
中。與 PiniaCustomProperties
不同的是,它只接收 State
泛型:
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomStateProperties<S> {
hello: string
}
}
當(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 類型中提取 getter 的 StoreGetters
類型。你也可以且只可以通過擴(kuò)展 DefineStoreOptions
或 DefineSetupStoreOptions
類型來擴(kuò)展 setup store 或 option store 的選項(xiàng)。
:::
當(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)注 PiniaPluginContext
和 Plugin
以及它們的導(dǎo)入語句。
更多建議: