From 66e158d86d5e15a8cd79a0e948e3ff8fc1b6220e Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Sat, 3 Jul 2021 19:48:38 +0800 Subject: [PATCH] =?UTF-8?q?refactor(web):=20=E4=B8=BB=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E7=8A=B6=E6=80=81=E7=9A=84=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/index.tsx | 1 + shared/model/user.ts | 14 ++++- shared/redux/slices/index.ts | 2 + shared/redux/slices/user.ts | 16 +++++- tsconfig.json | 2 + web/src/components/MainProvider.tsx | 28 --------- web/src/hooks/useAppSelector.ts | 11 ++++ web/src/hooks/useEnsureSocket.ts | 25 --------- web/src/hooks/useSetupRedux.ts | 13 ----- web/src/routes/Entry/LoginView.tsx | 2 + web/src/routes/Entry/RegisterView.tsx | 2 + web/src/routes/Main/Navbar.tsx | 12 ++++ web/src/routes/Main/Provider.tsx | 81 +++++++++++++++++++++++++++ web/src/routes/Main/index.tsx | 6 +- web/src/utils/user-helper.ts | 11 ++++ 15 files changed, 152 insertions(+), 74 deletions(-) delete mode 100644 web/src/components/MainProvider.tsx create mode 100644 web/src/hooks/useAppSelector.ts delete mode 100644 web/src/hooks/useEnsureSocket.ts delete mode 100644 web/src/hooks/useSetupRedux.ts create mode 100644 web/src/routes/Main/Navbar.tsx create mode 100644 web/src/routes/Main/Provider.tsx create mode 100644 web/src/utils/user-helper.ts diff --git a/shared/index.tsx b/shared/index.tsx index c14fdcb6..5a5919e8 100644 --- a/shared/index.tsx +++ b/shared/index.tsx @@ -28,6 +28,7 @@ export { setTokenGetter } from './manager/request'; export { loginWithEmail, registerWithEmail } from './model/user'; // redux +export { userActions } from './redux/slices'; export { setupRedux } from './redux/setup'; export { createStore } from './redux/store'; export type { AppStore, AppDispatch } from './redux/store'; diff --git a/shared/model/user.ts b/shared/model/user.ts index 0b84601b..7abcbd0a 100644 --- a/shared/model/user.ts +++ b/shared/model/user.ts @@ -1,6 +1,6 @@ import { request } from '../api/request'; -interface UserLoginInfo { +export interface UserLoginInfo { _id: string; email: string; password: string; @@ -26,6 +26,18 @@ export async function loginWithEmail( return data; } +/** + * 使用 Token 登录 + * @param token JWT令牌 + */ +export async function loginWithToken(token: string): Promise { + const { data } = await request.post('/api/user/resolveToken', { + token, + }); + + return data; +} + /** * 邮箱注册账号 * @param email 邮箱 diff --git a/shared/redux/slices/index.ts b/shared/redux/slices/index.ts index b1496079..a6bd1dcc 100644 --- a/shared/redux/slices/index.ts +++ b/shared/redux/slices/index.ts @@ -6,3 +6,5 @@ export const appReducer = combineReducers({ }); export type AppState = ReturnType; + +export { userActions } from './user'; diff --git a/shared/redux/slices/user.ts b/shared/redux/slices/user.ts index 22c7e129..ec301bf0 100644 --- a/shared/redux/slices/user.ts +++ b/shared/redux/slices/user.ts @@ -1,11 +1,21 @@ -import { createSlice } from '@reduxjs/toolkit'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import type { UserLoginInfo } from '../../model/user'; -const initialState = {}; +interface UserState { + info: UserLoginInfo | null; +} + +const initialState: UserState = { info: null }; const userSlice = createSlice({ name: 'user', initialState, - reducers: {}, + reducers: { + setUserInfo(state, action: PayloadAction) { + state.info = action.payload; + }, + }, }); +export const userActions = userSlice.actions; export const userReducer = userSlice.reducer; diff --git a/tsconfig.json b/tsconfig.json index d41cf869..ee574a85 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,8 @@ "isolatedModules": true, "module": "ESNext", "moduleResolution": "node", + "strict": true, + "importsNotUsedAsValues": "error", "typeRoots": ["./node_modules/@types", "../node_modules/@types", "./types"] } } diff --git a/web/src/components/MainProvider.tsx b/web/src/components/MainProvider.tsx deleted file mode 100644 index ffcefd92..00000000 --- a/web/src/components/MainProvider.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { createStore } from 'pawchat-shared'; -import React, { useMemo } from 'react'; -import { useEnsureSocket } from '../hooks/useEnsureSocket'; -import { LoadingSpinner } from './LoadingSpinner'; -import { Provider as ReduxProvider } from 'react-redux'; -import { useSetupRedux } from '../hooks/useSetupRedux'; - -/** - * 主页面核心数据Provider - * 在主页存在 - */ -export const MainProvider: React.FC = React.memo((props) => { - const store = useMemo(() => createStore(), []); - const { socket, loading } = useEnsureSocket(); - - useSetupRedux(socket, store); - - if (loading) { - return ( -
- -
- ); - } - - return {props.children}; -}); -MainProvider.displayName = 'MainProvider'; diff --git a/web/src/hooks/useAppSelector.ts b/web/src/hooks/useAppSelector.ts new file mode 100644 index 00000000..c4598bca --- /dev/null +++ b/web/src/hooks/useAppSelector.ts @@ -0,0 +1,11 @@ +import type { AppState } from 'pawchat-shared/redux/slices'; +import { useSelector, useDispatch } from 'react-redux'; + +export function useAppSelector( + selector: (state: AppState) => T, + equalityFn?: (left: T, right: T) => boolean +) { + return useSelector(selector, equalityFn); +} + +export const useAppDispatch = useDispatch; diff --git a/web/src/hooks/useEnsureSocket.ts b/web/src/hooks/useEnsureSocket.ts deleted file mode 100644 index 72b697cd..00000000 --- a/web/src/hooks/useEnsureSocket.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createSocket, useAsync } from 'pawchat-shared'; -import { getUserJWT } from '../utils/jwt-helper'; - -/** - * 创建全局Socket - */ -export function useEnsureSocket() { - const { - value: socket, - loading, - error, - } = useAsync(async () => { - const token = await getUserJWT(); - if (typeof token !== 'string') { - throw new Error('Token不合法'); - } - - const socket = await createSocket(token); - console.log('当前socket连接成功'); - - return socket; - }, []); - - return { loading, error, socket }; -} diff --git a/web/src/hooks/useSetupRedux.ts b/web/src/hooks/useSetupRedux.ts deleted file mode 100644 index 89deb0f9..00000000 --- a/web/src/hooks/useSetupRedux.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { AppSocket, AppStore, setupRedux } from 'pawchat-shared'; -import { useEffect } from 'react'; - -/** - * 初始化 全局上下文 只执行一次 - */ -export function useSetupRedux(socket: AppSocket, store: AppStore) { - useEffect(() => { - if (socket !== undefined && store !== undefined) { - setupRedux(socket, store); - } - }, [socket, store]); -} diff --git a/web/src/routes/Entry/LoginView.tsx b/web/src/routes/Entry/LoginView.tsx index 968d8825..b3d47fe1 100644 --- a/web/src/routes/Entry/LoginView.tsx +++ b/web/src/routes/Entry/LoginView.tsx @@ -6,6 +6,7 @@ import { Spinner } from '../../components/Spinner'; import { string } from 'yup'; import { useHistory } from 'react-router'; import { setUserJWT } from '../../utils/jwt-helper'; +import { setGlobalUserLoginInfo } from '../../utils/user-helper'; /** * TODO: @@ -45,6 +46,7 @@ export const LoginView: React.FC = React.memo(() => { const data = await loginWithEmail(email, password); + setGlobalUserLoginInfo(data); await setUserJWT(data.token); history.push('/main'); }, [email, password, history]); diff --git a/web/src/routes/Entry/RegisterView.tsx b/web/src/routes/Entry/RegisterView.tsx index 19e10a39..b8ac7651 100644 --- a/web/src/routes/Entry/RegisterView.tsx +++ b/web/src/routes/Entry/RegisterView.tsx @@ -5,6 +5,7 @@ import { string } from 'yup'; import { Icon } from '@iconify/react'; import { useHistory } from 'react-router'; import { setUserJWT } from '../../utils/jwt-helper'; +import { setGlobalUserLoginInfo } from '../../utils/user-helper'; /** * 注册视图 @@ -27,6 +28,7 @@ export const RegisterView: React.FC = React.memo(() => { const data = await registerWithEmail(email, password); + setGlobalUserLoginInfo(data); await setUserJWT(data.token); history.push('/main'); }, [email, password]); diff --git a/web/src/routes/Main/Navbar.tsx b/web/src/routes/Main/Navbar.tsx new file mode 100644 index 00000000..6ea10a81 --- /dev/null +++ b/web/src/routes/Main/Navbar.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { useAppSelector } from '../../hooks/useAppSelector'; + +/** + * 导航栏组件 + */ +const Navbar: React.FC = React.memo(() => { + const userInfo = useAppSelector((state) => state.user.info); + + return
; +}); +Navbar.displayName = 'Navbar'; diff --git a/web/src/routes/Main/Provider.tsx b/web/src/routes/Main/Provider.tsx new file mode 100644 index 00000000..73f2f320 --- /dev/null +++ b/web/src/routes/Main/Provider.tsx @@ -0,0 +1,81 @@ +import { + createSocket, + createStore, + setupRedux, + useAsync, + userActions, +} from 'pawchat-shared'; +import React, { useMemo } from 'react'; +import { LoadingSpinner } from '../../components/LoadingSpinner'; +import { Provider as ReduxProvider } from 'react-redux'; +import { getGlobalUserLoginInfo } from '../../utils/user-helper'; +import _isNil from 'lodash/isNil'; +import { loginWithToken } from 'pawchat-shared/model/user'; +import { getUserJWT } from '../../utils/jwt-helper'; +import { useHistory } from 'react-router'; + +/** + * 应用状态管理hooks + */ +function useAppState() { + const history = useHistory(); + + const { value, loading } = useAsync(async () => { + let userLoginInfo = getGlobalUserLoginInfo(); + if (_isNil(userLoginInfo)) { + // 如果没有全局缓存的数据, 则尝试自动登录 + try { + const token = await getUserJWT(); + if (typeof token !== 'string') { + throw new Error('Token 不合法'); + } + userLoginInfo = await loginWithToken(token); + } catch (e) { + // 当前 Token 不存在或已过期 + history.replace('/entry/login'); + return; + } + } + + // 到这里 userLoginInfo 必定存在 + // 创建Redux store + const store = createStore(); + store.dispatch(userActions.setUserInfo(userLoginInfo)); + + // 创建 websocket 连接 + const socket = await createSocket(userLoginInfo.token); + + // 初始化Redux + setupRedux(socket, store); + + return { store, socket }; + }, [history]); + + const store = value?.store; + const socket = value?.socket; + + return { loading, store, socket }; +} + +/** + * 主页面核心数据Provider + * 在主页存在 + */ +export const MainProvider: React.FC = React.memo((props) => { + const { loading, store } = useAppState(); + + if (loading) { + return ( +
+ +
+ ); + } + + if (_isNil(store)) { + return
出现异常, Store 创建失败
; + } + + return {props.children}; +}); +MainProvider.displayName = 'MainProvider'; diff --git a/web/src/routes/Main/index.tsx b/web/src/routes/Main/index.tsx index 4298ea43..0980df7a 100644 --- a/web/src/routes/Main/index.tsx +++ b/web/src/routes/Main/index.tsx @@ -1,9 +1,7 @@ import { Icon } from '@iconify/react'; import clsx, { ClassValue } from 'clsx'; -import React, { useLayoutEffect } from 'react'; -import { LoadingSpinner } from '../components/LoadingSpinner'; -import { MainProvider } from '../components/MainProvider'; -import { useEnsureSocket } from '../hooks/useEnsureSocket'; +import React from 'react'; +import { MainProvider } from './Provider'; const NavbarNavItem: React.FC<{ className?: ClassValue; diff --git a/web/src/utils/user-helper.ts b/web/src/utils/user-helper.ts new file mode 100644 index 00000000..e8f9d07b --- /dev/null +++ b/web/src/utils/user-helper.ts @@ -0,0 +1,11 @@ +import { UserLoginInfo } from 'pawchat-shared/model/user'; + +let _userLoginInfo: UserLoginInfo; + +export function setGlobalUserLoginInfo(loginInfo: UserLoginInfo) { + _userLoginInfo = loginInfo; +} + +export function getGlobalUserLoginInfo() { + return _userLoginInfo; +}