Pinia(Vue 的专属状态管理库)
Vue全家桶 - pinia 的理解和学习1(Pinia 核心概念的 Store、State、Getter、Action) https://blog.csdn.net/weixin_54092687/article/details/140520675
插件
// 通过 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'
export function myPiniaPlugin(context) {
context.pinia // 用 `createPinia()` 创建的 pinia。
context.app // 用 `createApp()` 创建的当前应用(仅 Vue 3)。
context.store // 该插件想扩展的 store
context.options // 定义传给 `defineStore()` 的 store 的可选对象。
// ...
}
pinia.use(myPiniaPlugin)
扩展 Store
// 方式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
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
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
}
}
})
添加新的外部属性
import { markRaw } from 'vue'
import { router } from './router'
pinia.use(({ store }) => { store.router = markRaw(router) })
在插件中调用 $subscribe
pinia.use(({ store }) => {
store.$subscribe(() => { }) // 响应 store 变化
store.$onAction(() => { }) // 响应 store actions
})
添加新的选项
// 创建一个 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
标注插件类型
// 一个 Pinia 插件可按如下方式实现类型标注:
import { PiniaPluginContext } from 'pinia'
export function myPiniaPlugin(context: PiniaPluginContext) { }
为新的 store 属性添加类型
// 当在 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())
})
// 如果把初始选项复制成 $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 添加类型
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomStateProperties<S> {
hello: string
}
}
为新的定义选项添加类型
import 'pinia'
declare module 'pinia' {
export interface DefineStoreOptionsBase<S, Store> {
// 任意 action 都允许定义一个防抖的毫秒数
debounce?: Partial<Record<keyof StoreActions<Store>, number>>
}
}
在组件外使用 store
单页面应用
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()
// 在 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'
})
服务端渲染应用
<script setup>
// 这是可行的,
// 因为 pinia 知道在 `setup` 中运行的是什么程序。
const main = useMainStore()
</script>
在 setup() 外部使用 store
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'
})
export default {
serverPrefetch() {
const store = useStore(this.$pinia)
},
}
State 激活
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)
// 在 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
}
}
)
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
// 必须由用户设置
if (isClient) {
pinia.state.value = JSON.parse(window.__pinia)
}