From 0f81918954733be6aaa9a492aac144cd0216107d Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Mon, 12 Jul 2021 16:14:57 +0800 Subject: [PATCH] chore: redux cache controller --- shared/index.tsx | 5 + shared/model/user.ts | 14 ++ shared/package.json | 1 + .../redux}/hooks/useAppSelector.ts | 0 shared/redux/hooks/useReduxCache.ts | 138 ++++++++++++++++++ shared/redux/slices/cache.ts | 32 ++++ shared/redux/slices/index.ts | 3 + .../Main/Content/Personal/Friends/index.tsx | 2 +- web/src/routes/Main/Navbar.tsx | 2 +- yarn.lock | 2 +- 10 files changed, 196 insertions(+), 3 deletions(-) rename {web/src => shared/redux}/hooks/useAppSelector.ts (100%) create mode 100644 shared/redux/hooks/useReduxCache.ts create mode 100644 shared/redux/slices/cache.ts diff --git a/shared/index.tsx b/shared/index.tsx index eb0e1c8a..73d51647 100644 --- a/shared/index.tsx +++ b/shared/index.tsx @@ -46,6 +46,11 @@ export { } from './model/user'; // redux +export { useAppSelector, useAppDispatch } from './redux/hooks/useAppSelector'; +export { + useCachedUserInfo, + useCachedUserInfoList, +} from './redux/hooks/useReduxCache'; export { userActions } from './redux/slices'; export { setupRedux } from './redux/setup'; export { createStore } from './redux/store'; diff --git a/shared/model/user.ts b/shared/model/user.ts index 3ad0b31a..7edaa86c 100644 --- a/shared/model/user.ts +++ b/shared/model/user.ts @@ -72,3 +72,17 @@ export async function searchUserWithUniqueName( return data; } + +/** + * 获取用户基本信息 + * @param userId 用户ID + */ +export async function fetchUserInfo(userId: string): Promise { + const { data } = await request.get('/api/user/getUserInfo', { + params: { + userId, + }, + }); + + return data; +} diff --git a/shared/package.json b/shared/package.json index 3dbd02e7..9e4acd29 100644 --- a/shared/package.json +++ b/shared/package.json @@ -16,6 +16,7 @@ "lodash": "^4.17.21", "react-i18next": "^11.11.0", "react-native-storage": "npm:@trpgengine/react-native-storage@^1.0.1", + "react-redux": "^7.2.4", "redux": "^4.1.0", "str2int": "^1.0.0", "url-regex": "^5.0.0", diff --git a/web/src/hooks/useAppSelector.ts b/shared/redux/hooks/useAppSelector.ts similarity index 100% rename from web/src/hooks/useAppSelector.ts rename to shared/redux/hooks/useAppSelector.ts diff --git a/shared/redux/hooks/useReduxCache.ts b/shared/redux/hooks/useReduxCache.ts new file mode 100644 index 00000000..e43635ba --- /dev/null +++ b/shared/redux/hooks/useReduxCache.ts @@ -0,0 +1,138 @@ +import _get from 'lodash/get'; +import _set from 'lodash/set'; +import _isNil from 'lodash/isNil'; +import { useEffect, useMemo, useRef } from 'react'; +import { fetchUserInfo, UserBaseInfo } from '../../model/user'; +import { cacheActions } from '../slices'; +import type { CacheKey } from '../slices/cache'; +import { useAppDispatch, useAppSelector } from './useAppSelector'; + +// 检查是否需要跳过处理 +const isSkipId = (id: string) => + _isNil(id) || + id === '' || + typeof id !== 'string' || + id.toString().startsWith('_'); + +interface CacheHookOptions { + forceFetch?: boolean; +} + +type GetCacheDataFn = (id: string) => Promise; + +function reduxHookCacheFactory( + cacheScope: CacheKey, + getCacheData: GetCacheDataFn +) { + const isFetchingDataIdQueue: string[] = []; // 正在请求的id列表 + + return function hook(id: string, options?: CacheHookOptions): Partial { + const data = useAppSelector( + (state) => _get(state, ['cache', cacheScope, id]) as any + ); + const dispatch = useAppDispatch(); + const forceFetchRef = useRef(options?.forceFetch ?? false); + + useEffect(() => { + if ((_isNil(data) || forceFetchRef.current === true) && !isSkipId(id)) { + // 如果没有数据或设置了强制重新获取 且 不是内置的UUID + // 从服务端获取缓存信息 + if (isFetchingDataIdQueue.indexOf(id) === -1) { + // 没有正在获取缓存信息 + console.log(`缓存[${cacheScope}: ${id}]不存在, 自动获取`); + + getCacheData(id).then((data) => { + // 从列表中移除 + const index = isFetchingDataIdQueue.indexOf(id); + if (index !== -1) { + isFetchingDataIdQueue.splice(index, 1); + } + forceFetchRef.current = false; // 不论怎么样都置为false 表示已经获取过了 + + dispatch( + cacheActions.setCache({ + scope: cacheScope, + id, + data, + }) + ); + }); + + isFetchingDataIdQueue.push(id); + } + } + }, [data]); + + return data ?? {}; + }; +} + +export const useCachedUserInfo = reduxHookCacheFactory( + 'user', + (userId) => fetchUserInfo(userId) +); + +/** + * redux 的批量获取hooks的构造器 + * 用于列表 + * @param cacheScope 缓存的域 + * @param getCacheDispatch 请求缓存的dispatch + */ +function reduxHookCacheListFactory( + cacheScope: CacheKey, + getCacheData: GetCacheDataFn +) { + const isFetchingDataIdQueue: string[] = []; // 正在请求的UUID列表 + + return function hook = Record>( + idList: string[] + ): R { + const cacheList = useAppSelector( + (state) => _get(state, ['cache', cacheScope]) as any + ); + const dispatch = useAppDispatch(); + + const resMap = useMemo(() => { + const map = {} as R; + for (const id of idList) { + if (_isNil(cacheList[id]) && !isSkipId(id)) { + // 如果没有数据则请求数据 + // 从服务端获取缓存信息 + if (isFetchingDataIdQueue.indexOf(id) === -1) { + // 没有正在获取缓存信息 + console.log(`缓存[${cacheScope}: ${id}]不存在, 自动获取`); + getCacheData(id).then((data) => { + // 从列表中移除 + const index = isFetchingDataIdQueue.indexOf(id); + if (index !== -1) { + isFetchingDataIdQueue.splice(index, 1); + } + + dispatch( + cacheActions.setCache({ + scope: cacheScope, + id, + data, + }) + ); + }); + + isFetchingDataIdQueue.push(id); + } + continue; + } + + // 加入返回的map中 + _set(map, id, cacheList[id]); + } + + return map; + }, [cacheList, idList.join(',')]); + + return resMap; + }; +} +export const useCachedUserInfoList = reduxHookCacheListFactory( + 'user', + (userId) => fetchUserInfo(userId) +); diff --git a/shared/redux/slices/cache.ts b/shared/redux/slices/cache.ts new file mode 100644 index 00000000..3dcd8876 --- /dev/null +++ b/shared/redux/slices/cache.ts @@ -0,0 +1,32 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import type { UserBaseInfo } from '../../model/user'; +import _set from 'lodash/set'; + +interface CacheState { + user: Record; +} + +export type CacheKey = keyof CacheState; + +const initialState: CacheState = { user: {} }; + +const cacheSlice = createSlice({ + name: 'cache', + initialState, + reducers: { + setCache( + state, + action: PayloadAction<{ + scope: CacheKey; + id: string; + data: unknown; + }> + ) { + const { scope, id, data } = action.payload; + _set(state, [scope, id], data); + }, + }, +}); + +export const cacheActions = cacheSlice.actions; +export const cacheReducer = cacheSlice.reducer; diff --git a/shared/redux/slices/index.ts b/shared/redux/slices/index.ts index a6bd1dcc..bba90e14 100644 --- a/shared/redux/slices/index.ts +++ b/shared/redux/slices/index.ts @@ -1,10 +1,13 @@ import { combineReducers } from '@reduxjs/toolkit'; +import { cacheReducer } from './cache'; import { userReducer } from './user'; export const appReducer = combineReducers({ + cache: cacheReducer, user: userReducer, }); export type AppState = ReturnType; +export { cacheActions } from './cache'; export { userActions } from './user'; diff --git a/web/src/routes/Main/Content/Personal/Friends/index.tsx b/web/src/routes/Main/Content/Personal/Friends/index.tsx index 9e9e8cf6..66d40bc2 100644 --- a/web/src/routes/Main/Content/Personal/Friends/index.tsx +++ b/web/src/routes/Main/Content/Personal/Friends/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { useAppSelector } from '@/hooks/useAppSelector'; import { PillTabPane, PillTabs } from '@/components/PillTabs'; import { AddFriend } from './AddFriend'; +import { useAppSelector } from 'pawchat-shared'; /** * 主要内容组件 diff --git a/web/src/routes/Main/Navbar.tsx b/web/src/routes/Main/Navbar.tsx index a14f1d1b..219c2f4a 100644 --- a/web/src/routes/Main/Navbar.tsx +++ b/web/src/routes/Main/Navbar.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useAppSelector } from '../../hooks/useAppSelector'; +import { useAppSelector } from 'pawchat-shared'; import { Icon } from '@iconify/react'; import clsx, { ClassValue } from 'clsx'; import { Avatar } from '../../components/Avatar'; diff --git a/yarn.lock b/yarn.lock index 1f5fea27..6a8ab16c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7535,7 +7535,7 @@ react-is@^17.0.1: react-redux@^7.2.4: version "7.2.4" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225" + resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225" integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA== dependencies: "@babel/runtime" "^7.12.1"