请注意:解构功能还未完善,解构仍需使用 toRefs 方法(不是 vue 的 toRefs),并且 state 会被包装成 Ref 对象!
问题提出:
pinia的useStore每次导入组件都要调用一次来获取 store,但是又不创建独立的新仓库,非常的不人性化。我希望 pinia 可以像 vuex 一样直接获取到 store 本身使用。 说明一下:pinia 的同一个 store 只有在第一次调用 useStore 时才初始化。
探索过程:
为什么 pinia 要采用这样的方式而不是在定义 store 的时候就直接 export store 本身? 这里我找到三个主要原因:
- 为了懒加载;
- 在 router 中引入 store 本身会导致仓库在 App.use(pinia) 前被加载导致报错;
- 在 pinia 源码发现 useStore 有两个可选参数 pinia 和 hot ,在大项目中可能需要用来 debug;
既然如此那怎么在真正使用时才初始化 store,而在导入时暂时不初始化呢? 然后想到用 Proxy 代理,当代理被引入时先不调用 useStore,在使用到的时候才触发 proxy 的 get 来初始化,就避免了提前调用报错,并且同样是懒加载,而且可以完全实现 useStore 本身的函数调用方式
这里我们简单介绍一下 Proxy: developer.mozilla.org/zh-CN/docs/… Proxy 实例化可以代理一个对象,当我们对这个 Proxy 对象做一些操作时会触发对应的回调函数返回结果。但是我认为将数据储存在这个内部对象中非常不方便(我不太会使用),所以我采用闭包的方式来维护内部数据。 我们以下面的例子来说明一下 Proxy 的构造函数:
// 第一个参数是 (() => {}), 代理一个函数对象,
// 使得我们可以将Proxy对象作为函数调用, 也可以作为普通对象调用其成员。
new Proxy((() => {}) as storeProxyType, {
// 第二个参数是一个对象, 其中可以定义多个操作的代理回调函数
// 这些函数的返回值作为真正的操作后返回的值
// 例如这里'get'代理调用成员的操作
get: function (target, property, receiver) {
if (!storeValue) {
storeValue = useStore() as any
storeValue.value = storeValue
}
return (storeValue as any)[property]
},
// 这里'apply'代理当作函数调用的操作
apply: function (target, thisArg, argumentsList) {
return useStore(...argumentsList)
}
})
解决方案:
最终我们得到一个完美的代理,他具有解决上述所有问题,具有以下功能:
- 完全代替原先导出的 useStore ,我们可以完全将这个新代理当作原先导出的 useStore 使用;
- 也可以将其当作已经初始化了的 store 对象,直接调用他的成员使用,如果要获取 store 对象本身,可以使用无参调用或调用其成员 'value'
以下是我的storeProxy.ts源码:
import type {
StoreDefinition,
_ExtractStateFromSetupStore,
_ExtractGettersFromSetupStore,
_ExtractActionsFromSetupStore,
Pinia,
StoreGeneric,
DefineSetupStoreOptions
} from 'pinia'
import { defineStore } from 'pinia'
import { toRef, type Ref } from 'vue'
// 第一步用 useStoreProxy 代理 ( defineStore 返回的 ) useStore
const useStoreProxy = <Id extends string, SS>(
useStore: StoreDefinition<
Id,
_ExtractStateFromSetupStore<SS>,
_ExtractGettersFromSetupStore<SS>,
_ExtractActionsFromSetupStore<SS>
>,
storeSetup: () => SS
) => {
// ReturnType<typeof useStore> 即真实的 store 实例类型
// 这里交叉 useStore 的函数类型,使其可以当作 useStore 来调用函数,可以传入参数 'pinia' 和 'hot'
// 还交叉一个 'value' 成员,用来返回内部维护的真实 store 对象,( 理念和 vue 的 ref 对象类似 )
type storeProxyType = ReturnType<typeof useStore> & {
(
pinia?: Pinia | null | undefined,
hot?: StoreGeneric
): ReturnType<typeof useStore>
value: ReturnType<typeof useStore>
toRefs: () => {
[key in keyof SS]: SS[key] extends Function ? SS[key] : Ref<SS[key]>
}
}
// 维护的真正的 store 对象
// 注意这里的 store 对象始终是没有使用参数 'pinia' 和 'hot' 定义的普通对象
let store: storeProxyType
return new Proxy((() => {}) as storeProxyType, {
// 直接使用代理调用成员
get(target, prop, receiver) {
// 初始化 store
if (!store) {
store = useStore() as any
store.value = store
// 定义解构辅助方法
store.toRefs = (() => {
const res = {} as any
Object.keys(storeSetup() as object).forEach((key) => {
const val = (store as any)[key]
if (typeof val === 'function') res[key] = val
else res[key] = toRef(store, key as any)
})
return () => res
})()
}
return (store as any)[prop]
},
// 代理 useStore 掉用函数传入参数 'pinia' 和 'hot'
// 如果需要使用这两个参数可以完全将新的代理当作原先导出的 useStore 使用
apply(target, thisArg, args) {
return useStore(...args)
}
})
}
// 二次封装,用 defineStoreProxy 代替 defineStore
// 不完善:defineStore共有三个重载
export const defineStoreProxy = <Id extends string, SS>(
id: Id,
storeSetup: () => SS,
options?: DefineSetupStoreOptions<
Id,
_ExtractStateFromSetupStore<SS>,
_ExtractGettersFromSetupStore<SS>,
_ExtractActionsFromSetupStore<SS>
>
) => useStoreProxy(defineStore(id, storeSetup, options), storeSetup)
在 src/stores/modules/counter.ts 这个 counterStore 的定义文件中,我们这样使用:
import { ref } from 'vue'
// 只需要用 defineStoreProxy 取代原先的 defineStore
// import { defineStore } from 'pinia'
import { defineStoreProxy } from '@/utils/storeProxy'
export const counterStore = defineStoreProxy('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
这是原始 pinia store 的使用:
// 这是原始的使用 definedStore 的方式
import { useCounterStore } from '@/stores'
import { storeToRefs } from 'pinia'
const counterStore = useCounterStore(/*可以传参'pinia'和'hot'*/)
const { count, doubleCount } = storeToRefs(counterStore)
const { increment } = counterStore
console.log(count.value, counterStore.count)
console.log(doubleCount.value, counterStore.doubleCount)
increment(), counterStore.increment()
现在可以使用:
// 这是使用 defineStoreProxy 的方式
import { counterProxy } from '@/stores'
// 解构暂时请使用 toRefs 方法,与 pinia 的 storeToRefs 类似,
// 解构出来的 state 将由 ref 包装,函数则保持原样
const { count, doubleCount, increment } = counterStore.toRefs()
console.log(count.value, counterStore.count)
console.log(doubleCount.value, counterStore.doubleCount)
increment(), counterStore.increment()
当然新代理也可以使用那样的原始方式以使用参数 'pinia' 和 'hot' :
// 这是使用 defineStoreProxy 的方式
import { counterProxy } from '@/stores'
const newCounterStore = counterProxy(/*可以传参'pinia'和'hot'*/)
// 或 const newCounterStore = counterProxy.value 几乎等同于上面的无参调用 (但是没有什么意义)
总结
虽然这样解决方案不具有实用性,而且容易导致代码可读性下降,但是其探索过程仍然带给我很大的收获;
因为网上我搜索不到任何对 pinia 这样使用仓库的疑惑和解答,所以我自己写了一篇这样简单的文章来和大家探讨这个简单又不简单的小问题;
我本身是一个前端初学者,没有任何实际工作经验,如果内容有错误或者不妥当的地方欢迎大家指出,我会尽快修正!
改进
- 2024-09-02:使用 useRef 实现了解构自动保持响应性,现在在日常使用可以完全取代原先的写法
- 2024-09-30:发现2024-09-02的改进是错误的,准备模仿vue defineProps解构保持响应性探索vue编译