Vue3+Pinia 实现全局键值对缓存

在需要重复使用大量相同数据的场景下(如展示用户信息、商品信息、购物车列表等),重复调用 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 函数的第一个参数填写自定义的函数名,而解构出来的函数名就是这个参数。

未经允许禁止转载本站内容,经允许转载后请严格遵守CC-BY-NC-ND知识共享协议4.0,代码部分则采用GPL v3.0协议
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇