Pinia(Vue 的专属状态管理库)
Vue全家桶 - pinia 的理解和学习1(Pinia 核心概念的 Store、State、Getter、Action) https://blog.csdn.net/weixin_54092687/article/details/140520675
插件
由于有了底层 API 的支持,Pinia store 现在完全支持扩展。以下是你可以扩展的内容:
为 store 添加新的属性
定义 store 时增加新的选项
为 store 增加新的方法
包装现有的方法
改变甚至取消 action
实现副作用,如本地存储
仅应用插件于特定 store
javascript
// 通过 pinia.use() 添加插件到 pinia 实例的。
import { createPinia } from 'pinia'
function SecretPiniaPlugin() { return { secret: 'the cake is a lie' } }
const pinia = createPinia()
pinia.use(SecretPiniaPlugin)
// 在组件中使用
const store = useStore()
store.secret // 'the cake is a lie'
Pinia 插件是一个函数,可以选择性地返回要添加到 store 的属性。它接收一个可选参数 context。
javascript
export function myPiniaPlugin(context) {
context.pinia // 用 `createPinia()` 创建的 pinia。
context.app // 用 `createApp()` 创建的当前应用(仅 Vue 3)。
context.store // 该插件想扩展的 store
context.options // 定义传给 `defineStore()` 的 store 的可选对象。
// ...
}
pinia.use(myPiniaPlugin)
扩展 Store
javascript
// 方式1 -- 可以直接通过在一个插件中返回包含特定属性的对象来为每个 store 都添加上特定属性。
pinia.use(() => ({ hello: 'world' }))
// 方式2 -- 可以直接在 store 上设置该属性,最好使用返回对象的方法。
pinia.use(({ store }) => { store.hello = 'world' })
// 想在 devtools 中调试 hello 属性,为了使 devtools 能追踪到 hello。
pinia.use(({ store }) => {
store.hello = 'world'
if (process.env.NODE_ENV === 'development') {
store._customProperties.add('hello') // 添加你在 store 中设置的键值
}
})
// 每个 store 都被 reactive 包装过,所以可以自动解包任何它所包含的 Ref(ref()、computed()...)。
const sharedRef = ref('shared')
pinia.use(({ store }) => {
store.hello = ref('secret') // 每个 store 都有单独的 `hello` 属性,它会被自动解包
store.hello // 'secret'
store.shared = sharedRef // 所有的 store 都在共享 `shared` 属性的值
store.shared // 'shared'
})
添加新的 state
给 store 添加新的 state 属性或者在服务端渲染的激活过程中使用的属性,必须同时在两个地方添加它。
在 store 上,然后才可以用 store.myState 访问它。
在 store.$state 上,然后才可以在 devtools 中使用它,并且,在 SSR 时被正确序列化(serialized)。
除此之外,你肯定也会使用
ref()(或其他响应式 API)
,以便在不同的读取中共享相同的值。
javascript
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 被激活之前,因此不会触发任何订阅函数。
重置插件中添加的 state
默认情况下,$reset() 不会重置插件添加的 state,但你可以重写它来重置你添加的 state。
javascript
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
}
}
})
添加新的外部属性
当添加外部属性、第三方库的类实例或非响应式的简单值时,你应该先用 markRaw() 来包装一下它,再将它传给 pinia。
javascript
import { markRaw } from 'vue'
import { router } from './router'
pinia.use(({ store }) => { store.router = markRaw(router) })
在插件中调用 $subscribe
javascript
pinia.use(({ store }) => {
store.$subscribe(() => { }) // 响应 store 变化
store.$onAction(() => { }) // 响应 store actions
})
添加新的选项
在定义 store 时,可以创建新的选项,以便在插件中使用它们。
javascript
// 创建一个 debounce 选项,允许让任何 action 实现防抖。
defineStore('search', {
actions: {
searchContacts() { }
},
// 这将在后面被一个插件读取
debounce: {
searchContacts: 300 // 让 action searchContacts 防抖 300ms
}
})
// 然后,该插件可以读取该选项来包装 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: { searchContacts: 300 }
}
)
TypeScript
上述一切功能都有类型支持,所以你永远不需要使用 any 或 @ts-ignore。
标注插件类型
javascript
// 一个 Pinia 插件可按如下方式实现类型标注:
import { PiniaPluginContext } from 'pinia'
export function myPiniaPlugin(context: PiniaPluginContext) { }
为新的 store 属性添加类型
javascript
// 当在 store 中添加新的属性时,你也应该扩展 PiniaCustomProperties 接口。
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomProperties {
set hello(value: string | Ref<string>) // 通过使用一个 setter,我们可以允许字符串和引用。
get hello(): string
simpleNumber: number // 你也可以定义更简单的值
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 的属性。
当在泛型中扩展类型时,它们的名字必须与源代码中完全一样。
S: State
|G: Getters
|A: Actions
|SS: Setup Store / Store
javascript
// 如果把初始选项复制成 $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
}
}
}
为新的 state 添加类型
当添加新的 state 属性(包括 store 和 store.$state )时,你需要将类型添加到 PiniaCustomStateProperties 中。与 PiniaCustomProperties 不同的是,它只接收 State 泛型。
javascript
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomStateProperties<S> {
hello: string
}
}
为新的定义选项添加类型
当为 defineStore() 创建新选项时,你应该扩展 DefineStoreOptionsBase。与 PiniaCustomProperties 不同的是,它只暴露了两个泛型:State 和 Store 类型,允许你限制定义选项的可用类型。
javascript
import 'pinia'
declare module 'pinia' {
export interface DefineStoreOptionsBase<S, Store> {
// 任意 action 都允许定义一个防抖的毫秒数
debounce?: Partial<Record<keyof StoreActions<Store>, number>>
}
}
在组件外使用 store
Pinia store 依靠 pinia 实例在所有调用中共享同一个 store 实例。大多数时候,只需调用你定义的 useStore() 函数,完全开箱即用。例如,在 setup() 中,你不需要再做任何事情。但在组件之外,情况就有点不同了。 实际上,useStore() 给你的 app 自动注入了 pinia 实例。这意味着,如果 pinia 实例不能自动注入,你必须手动提供给 useStore() 函数。 你可以根据不同的应用,以不同的方式解决这个问题。
单页面应用
如果你不做任何 SSR(服务器端渲染),在用 app.use(pinia) 安装 pinia 插件后,对 useStore() 的任何调用都会正常执行:
javascript
import { useUserStore } from '@/stores/user'
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import App from './App.vue'
// ❌ 失败,因为它是在创建 pinia 之前被调用的
const userStore = useUserStore()
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
// ✅ 成功,因为 pinia 实例现在激活了
const userStore = useUserStore()
为确保 pinia 实例被激活,最简单的方法就是将 useStore() 的调用放在 pinia 安装后才会执行的函数中。
javascript
// 在 Vue Router 的导航守卫中使用 store 的例子。
import { createRouter } from 'vue-router'
const router = createRouter({
// ...
})
// ❌ 由于引入顺序的问题,这将失败
const store = useStore()
router.beforeEach((to, from, next) => {
// 我们想要在这里使用 store
if (store.isLoggedIn) next()
else next('/login')
})
router.beforeEach((to) => {
// ✅ 这样做是可行的,因为路由器是在其被安装之后开始导航的,
// 而此时 Pinia 也已经被安装。
const store = useStore()
if (to.meta.requiresAuth && !store.isLoggedIn) return '/login'
})
服务端渲染应用
当处理服务端渲染时,你将必须把 pinia 实例传递给 useStore()。这可以防止 pinia 在不同的应用实例之间共享全局状态。
只要你只在 setup 函数、getter 和 action 的顶部调用你定义的 useStore() 函数,那么使用 Pinia 创建 store 对于 SSR 来说应该是开箱即用的:
javascript
<script setup>
// 这是可行的,
// 因为 pinia 知道在 `setup` 中运行的是什么程序。
const main = useMainStore()
</script>
在 setup() 外部使用 store
如果你需要在其他地方使用 store,你需要将原本被传递给应用 的 pinia 实例传递给 useStore() 函数:
javascript
const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(pinia)
router.beforeEach((to) => {
// ✅这会正常工作,因为它确保了正确的 store 被用于
// 当前正在运行的应用
const main = useMainStore(pinia)
if (to.meta.requiresAuth && !main.isLoggedIn) return '/login'
})
Pinia 会将自己作为 $pinia 添加到你的应用中,所以你可以在 serverPrefetch() 等函数中使用它。
javascript
export default {
serverPrefetch() {
const store = useStore(this.$pinia)
},
}
State 激活
为了激活初始 state,你需要确保 rootState 包含在 HTML 中的某个地方,以便 Pinia 稍后能够接收到它。根据你服务端所渲染的内容,为了安全你应该转义 state。
javascript
import devalue from '@nuxt/devalue'
import { createPinia } from 'pinia'
// 检索服务端的 rootState
const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(pinia)
// 渲染页面后,rootState 被建立,
// 可以直接在 `pinia.state.value`上读取。
// 序列化,转义(如果 state 的内容可以被用户改变,这点就非常重要,几乎都是这样的)
// 并将其放置在页面的某处
// 例如,作为一个全局变量。
devalue(pinia.state.value)
根据你服务端所渲染的内容,你将设置一个初始状态变量,该变量将在 HTML 中被序列化。你还应该保护自己免受 XSS 攻击。
javascript
// 在 vite-ssr中你可以使用transformState 选项 以及 @nuxt/devalue:
import devalue from '@nuxt/devalue'
export default viteSSR(
App,
{
routes,
transformState(state) {
return import.meta.env.SSR ? devalue(state) : state
},
},
({ initialState }) => {
// ...
if (import.meta.env.SSR) {
// 序列化并设置为 window.__INITIAL_STATE__
initialState.pinia = pinia.state.value
} else {
// 在客户端,我们恢复 state
pinia.state.value = initialState.pinia
}
}
)
你可以根据你的需要使用 @nuxt/devalue 的其他替代品,例如,你也可以用 JSON.stringify()/JSON.parse() 来序列化和解析你的 state,这样你可以把性能提高很多。
也可以根据你的环境调整这个策略。但确保在客户端调用任何 useStore() 函数之前,激活 pinia 的 state。例如,如果我们将 state 序列化为一个
<script>
标签,并在客户端通过 window.__pinia 全局访问它,我们可以这样写:
javascript
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
// 必须由用户设置
if (isClient) {
pinia.state.value = JSON.parse(window.__pinia)
}