在需要重复使用大量相同数据的场景下(如展示用户信息、商品信息、购物车列表等),重复调用 api 获取不仅会增加后端服务器的负担,也显得代码不太简洁。
现在写一个全局缓存,将获取数据的逻辑也写入其中,这样外部只需要调用 get() 方法而无需关心数据的来源,提高代码的可维护性。
基本实现
下面以用户信息(User)为例,首先定义一个要保存的数据类型:
export interface User {
id: string,
username: string,
nickname: string,
email: string,
avatar: string,
registeredTime: string,
modifiedTime: string,
activated: boolean
}
再定义一个中间类型用于保存状态:
export interface UserState {
loading: boolean
data: User | null
error: string | null
}
定义一个 Store:
export const useUserProfileStore = defineStore('user', {
state: () => ({
userProfileMap: new Map<string, UserState>(),
}),
actions: {
},
getters: {
}
})
添加一个 getter 用于从 map 中获取用户信息:
getProfile: (state) => {
return (userId: string) => state.userProfileMap.get(userId) || {
loading: false,
data: null,
error: null
}
}
接下来需要写一个从 api 获取用户信息的方法:
async fetchUserProfiles(ids: string[]) {
const idsToFetch = ids.filter(id => {
const existing = this.userProfileMap.get(id)
return !existing?.data && !existing?.loading
})
idsToFetch.forEach(id => {
this.userProfileMap = new Map(this.userProfileMap.set(id, {
loading: true,
data: null,
error: null
}))
})
await Promise.all(idsToFetch.map(async id => {
try {
const data = await getUserProfile(id)
this.userProfileMap = new Map(this.userProfileMap.set(id, {
loading: false,
data,
error: null
}))
} catch (error) {
this.userProfileMap = new Map(this.userProfileMap.set(id, {
loading: false,
data: null,
error: error instanceof Error ? error.message : 'Unknown error'
}))
}
}))
}
这里面的 await getUserProfile(id)
改成对应的获取数据的逻辑,我这里的 api 是 async 函数所以使用了 Promise.all,如果不是直接写即可。
最后我们还要监听 id 列表的变化,自动获取需要的用户信息,因此需要维护一个 ref<string[]> 来保存需要缓存的用户 id。
把部分也封装成一个函数:
export function useUserProfileCache(userIds: Ref<string[]>) {
const store = useUserProfileStore()
watchEffect(async () => {
if (userIds.value.length > 0) {
await store.fetchUserProfiles(userIds.value)
}
})
function getState(userId: string): UserState {
return store.getProfile(userId)
}
return { getUserState: getState }
}
最后,整体的代码如下:
import {defineStore} from 'pinia'
import {getChatCharacterDetails} from "@/net/api/chat-character-controller.ts";
import type {Ref} from "vue";
import {watchEffect} from "vue";
export interface User {
id: string,
username: string,
nickname: string,
email: string,
avatar: string,
registeredTime: string,
modifiedTime: string,
activated: boolean
}
export interface UserState {
loading: boolean
data: User | null
error: string | null
}
export function useUserProfileCache(userIds: Ref<string[]>) {
const store = useUserProfileStore()
watchEffect(async () => {
if (userIds.value.length > 0) {
await store.fetchUserProfiles(userIds.value)
}
})
function getState(userId: string): UserState {
return store.getProfile(userId)
}
return { getUserState: getState }
}
export const useUserProfileStore = defineStore('user', {
state: () => ({
userProfileMap: new Map<string, UserState>(),
}),
actions: {
async fetchUserProfiles(ids: string[]) {
const idsToFetch = ids.filter(id => {
const existing = this.userProfileMap.get(id)
return !existing?.data && !existing?.loading
})
idsToFetch.forEach(id => {
this.userProfileMap = new Map(this.userProfileMap.set(id, {
loading: true,
data: null,
error: null
}))
})
await Promise.all(idsToFetch.map(async id => {
try {
const data = await getUserProfile(id)
this.userProfileMap = new Map(this.userProfileMap.set(id, {
loading: false,
data,
error: null
}))
} catch (error) {
this.userProfileMap = new Map(this.userProfileMap.set(id, {
loading: false,
data: null,
error: error instanceof Error ? error.message : 'Unknown error'
}))
}
}))
}
},
getters: {
getProfile: (state) => {
return (userId: string) => state.userProfileMap.get(userId) || {
loading: false,
data: null,
error: null
}
}
}
})
具体使用方法如下:
const userProfileCacheIds = ref<string[]>([])
const { getUserState } = useUserProfileCache(userProfileCacheIds)
// ......
<p>{{ getUserState(item.id).data?.nickname ?? '' }}</p>
泛化
如果每个 store 都这样写还是显得不够优雅,下面把它封装成一个通用的类。
首先还是定义一个状态接口:
export interface StoreDataState<T> {
loading: boolean
data: T | null
error: string | null
}
接下来定义一个空白的类来代替上面写的 store:
export class KeyValueStore<K = string, V = any> {
private readonly storeName: string
private readonly getDataIfNotExist: (key: K) => Promise<V | null>
constructor(storeName: string, getDataIfNotExist: (key: K) => Promise<V | null>) {
this.storeName = storeName
this.getDataIfNotExist = getDataIfNotExist
}
public use() {}
public useCache<F extends string>(funcName: F, ids: Ref<K[]>): { [Key in F]: (id: K) => StoreDataState<V> } {}
}
接下来就和上面一样,实现一下 use() 和 useCache() 就行了,下面直接放完整的代码:
import {defineStore} from "pinia";
import type {Ref} from "vue";
import {watchEffect} from "vue";
export interface StoreDataState<T> {
loading: boolean
data: T | null
error: string | null
}
export class KeyValueStore<K = string, V = any> {
private readonly storeName: string
private readonly getDataIfNotExist: (key: K) => Promise<V | null>
constructor(storeName: string, getDataIfNotExist: (key: K) => Promise<V | null>) {
this.storeName = storeName
this.getDataIfNotExist = getDataIfNotExist
}
public use() {
const getDataIfNotExist = (id: K) => {
return this.getDataIfNotExist(id)
}
return defineStore(this.storeName, {
state: () => ({
dataMap: new Map<K, StoreDataState<V>>(),
}),
actions: {
async fetchData(ids: K[]) {
const idsToFetch = ids.filter(id => {
const existing = this.dataMap.get(id)
return !existing?.data && !existing?.loading
})
idsToFetch.forEach(id => {
this.dataMap = new Map(this.dataMap.set(id, {
loading: true,
data: null,
error: null
}))
})
await Promise.all(idsToFetch.map(async id => {
const onError = (error: any | null, message: string = '') => {
this.dataMap = new Map(this.dataMap.set(id, {
loading: false,
data: null,
error: message != '' ? message : error instanceof Error ? error.message : 'Unknown Error'
}))
}
try {
const data: V | null = await getDataIfNotExist(id)
if (data != null) {
this.dataMap = new Map(this.dataMap.set(id, {
loading: false,
// @ts-ignore
data: data,
error: null
}))
} else {
onError(null, 'Empty Response')
}
} catch (error) {
onError(error)
}
}))
}
},
getters: {
getData: (state) => {
return (id: K) => (state.dataMap.get(id) || {
loading: false,
data: null,
error: null
}) as StoreDataState<V>
}
}
})()
}
public useCache<F extends string>(funcName: F, ids: Ref<K[]>): { [Key in F]: (id: K) => StoreDataState<V> } {
const store = this.use()
watchEffect(async () => {
if (ids.value.length > 0) {
await store.fetchData(ids.value)
}
})
function getState(id: K): StoreDataState<V> {
return store.getData(id)
}
return { [funcName]: getState } as { [Key in F]: (id: K) => StoreDataState<V> }
}
}
使用方法也很简单,直接定义一个常量保存这个类即可:
export const userProfileStore = new KeyValueStore<string, User>(
'user',
async (key: string) => {
return await getUserProfile(key)
}
)
在需要使用的地方,也是和上面的一样:
/**
* Store
*/
const userProfileCacheIds = ref<string[]>([])
const { getUserState } = userProfileStore.useCache('getUserState', userProfileCacheIds)
只不过这里我支持了动态函数名,useCache 函数的第一个参数填写自定义的函数名,而解构出来的函数名就是这个参数。