diff --git a/shared/api/socket.ts b/shared/api/socket.ts index b3a2a65b..c19866fd 100644 --- a/shared/api/socket.ts +++ b/shared/api/socket.ts @@ -4,17 +4,57 @@ import _isNil from 'lodash/isNil'; let socket: Socket; +class SocketEventError extends Error { + name: 'SocketEventError'; +} + +type SocketEventRespones = + | { + result: true; + data: T; + } + | { + result: false; + message: string; + }; + +/** + * 封装后的 Socket + */ +export class AppSocket { + constructor(private socket: Socket) {} + + get connected(): boolean { + return socket.connected; + } + + async request( + eventName: string, + eventData: unknown = {} + ): Promise { + return new Promise((resolve, reject) => { + this.socket.emit(eventName, eventData, (resp: SocketEventRespones) => { + if (resp.result === true) { + resolve(resp.data); + } else if (resp.result === false) { + reject(new SocketEventError(resp.message)); + } + }); + }); + } +} + /** * 创建Socket连接 * 如果已经有Socket连接则关闭上一个 * @param token Token */ -export function createSocket(token: string) { +export function createSocket(token: string): Promise { if (!_isNil(socket)) { socket.close(); } - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { socket = io(config.serverUrl, { transports: ['websocket'], auth: { @@ -23,7 +63,7 @@ export function createSocket(token: string) { }); socket.once('connect', () => { // 连接成功 - resolve(); + resolve(new AppSocket(socket)); }); socket.once('error', () => { reject(); diff --git a/shared/index.tsx b/shared/index.tsx index 9d5a9ada..4a6bb5d4 100644 --- a/shared/index.tsx +++ b/shared/index.tsx @@ -1,6 +1,7 @@ // api export { buildStorage } from './api/buildStorage'; export { createSocket } from './api/socket'; +export type { AppSocket } from './api/socket'; // components export { FastForm } from './components/FastForm/index'; @@ -25,3 +26,8 @@ export { setTokenGetter } from './manager/request'; // model export { loginWithEmail, registerWithEmail } from './model/user'; + +// redux +export { setupRedux } from './redux/setup'; +export { createStore } from './redux/store'; +export type { AppStore, AppDispatch } from './redux/store'; diff --git a/shared/package.json b/shared/package.json index abdf5238..6b18c6c7 100644 --- a/shared/package.json +++ b/shared/package.json @@ -7,10 +7,12 @@ "license": "GPLv3", "private": true, "dependencies": { + "@reduxjs/toolkit": "^1.6.0", "axios": "^0.21.1", "formik": "^2.2.9", "lodash": "^4.17.21", "react-native-storage": "npm:@trpgengine/react-native-storage@^1.0.1", + "redux": "^4.1.0", "yup": "^0.32.9" }, "devDependencies": { diff --git a/shared/redux/setup.ts b/shared/redux/setup.ts new file mode 100644 index 00000000..8a984427 --- /dev/null +++ b/shared/redux/setup.ts @@ -0,0 +1,12 @@ +import type { AppStore } from './store'; +import type { AppSocket } from '../api/socket'; + +/** + * 初始化Redux 上下文 + */ +export function setupRedux(socket: AppSocket, store: AppStore) { + socket.request('friend.getAllFriends').then((resp) => { + // TODO + console.log('好友列表', resp); + }); +} diff --git a/shared/redux/slices/index.ts b/shared/redux/slices/index.ts new file mode 100644 index 00000000..b1496079 --- /dev/null +++ b/shared/redux/slices/index.ts @@ -0,0 +1,8 @@ +import { combineReducers } from '@reduxjs/toolkit'; +import { userReducer } from './user'; + +export const appReducer = combineReducers({ + user: userReducer, +}); + +export type AppState = ReturnType; diff --git a/shared/redux/slices/user.ts b/shared/redux/slices/user.ts new file mode 100644 index 00000000..22c7e129 --- /dev/null +++ b/shared/redux/slices/user.ts @@ -0,0 +1,11 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = {}; + +const userSlice = createSlice({ + name: 'user', + initialState, + reducers: {}, +}); + +export const userReducer = userSlice.reducer; diff --git a/shared/redux/store.ts b/shared/redux/store.ts new file mode 100644 index 00000000..c73bd058 --- /dev/null +++ b/shared/redux/store.ts @@ -0,0 +1,15 @@ +import { configureStore } from '@reduxjs/toolkit'; +import { appReducer } from './slices'; + +export function createStore() { + const store = configureStore({ + reducer: appReducer, + middleware: (getDefaultMiddleware) => getDefaultMiddleware(), + devTools: process.env.NODE_ENV !== 'production', + }); + + return store; +} + +export type AppStore = ReturnType; +export type AppDispatch = AppStore['dispatch']; diff --git a/web/package.json b/web/package.json index a7718be4..506ac014 100644 --- a/web/package.json +++ b/web/package.json @@ -21,6 +21,7 @@ "pawchat-shared": "*", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-redux": "^7.2.4", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "socket.io-client": "^4.1.2", diff --git a/web/src/components/MainProvider.tsx b/web/src/components/MainProvider.tsx new file mode 100644 index 00000000..ffcefd92 --- /dev/null +++ b/web/src/components/MainProvider.tsx @@ -0,0 +1,28 @@ +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/useEnsureSocket.ts b/web/src/hooks/useEnsureSocket.ts index 0833a78c..72b697cd 100644 --- a/web/src/hooks/useEnsureSocket.ts +++ b/web/src/hooks/useEnsureSocket.ts @@ -5,13 +5,21 @@ import { getUserJWT } from '../utils/jwt-helper'; * 创建全局Socket */ export function useEnsureSocket() { - const { loading, error } = useAsync(async () => { + const { + value: socket, + loading, + error, + } = useAsync(async () => { const token = await getUserJWT(); - if (typeof token === 'string') { - await createSocket(token); - console.log('当前socket连接成功'); // TODO + if (typeof token !== 'string') { + throw new Error('Token不合法'); } + + const socket = await createSocket(token); + console.log('当前socket连接成功'); + + return socket; }, []); - return { loading, error }; + return { loading, error, socket }; } diff --git a/web/src/hooks/useSetupRedux.ts b/web/src/hooks/useSetupRedux.ts new file mode 100644 index 00000000..89deb0f9 --- /dev/null +++ b/web/src/hooks/useSetupRedux.ts @@ -0,0 +1,13 @@ +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/Main.tsx b/web/src/routes/Main.tsx index 52b46fad..4298ea43 100644 --- a/web/src/routes/Main.tsx +++ b/web/src/routes/Main.tsx @@ -2,6 +2,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'; const NavbarNavItem: React.FC<{ @@ -20,47 +21,39 @@ const NavbarNavItem: React.FC<{ }); export const MainRoute: React.FC = React.memo(() => { - const { loading } = useEnsureSocket(); - - if (loading) { - return ( -
- -
- ); - } - return (
-
- {/* Navbar */} -
- -
-
- - + +
+ {/* Navbar */} +
- - - - +
+
+ + + + + + + +
+
+
+
-
- -
-
-
- {/* Sidebar */} -
- 目标 +
+ {/* Sidebar */} +
+ 目标 +
-
-
{/* Main Content */}
+
{/* Main Content */}
+
); }); diff --git a/yarn.lock b/yarn.lock index 3175274a..9326327c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -606,6 +606,16 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@reduxjs/toolkit@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.0.tgz#0a17c6941c57341f8b31e982352b495ab69d5add" + integrity sha512-eGL50G+Vj5AG5uD0lineb6rRtbs96M8+hxbcwkHpZ8LQcmt0Bm33WyBSnj5AweLkjQ7ZP+KFRDHiLMznljRQ3A== + dependencies: + immer "^9.0.1" + redux "^4.1.0" + redux-thunk "^2.3.0" + reselect "^4.0.0" + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -819,6 +829,14 @@ resolved "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/html-minifier-terser@^5.0.0": version "5.1.1" resolved "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#3c9ee980f1a10d6021ae6632ca3e79ca2ec4fb50" @@ -929,6 +947,16 @@ dependencies: "@types/react" "*" +"@types/react-redux@^7.1.16": + version "7.1.16" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21" + integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + "@types/react-router-dom@^5.1.7": version "5.1.7" resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.7.tgz#a126d9ea76079ffbbdb0d9225073eb5797ab7271" @@ -3352,7 +3380,7 @@ history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -3543,6 +3571,11 @@ image-size@~0.5.0: resolved "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= +immer@^9.0.1: + version "9.0.3" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.3.tgz#146e2ba8b84d4b1b15378143c2345559915097f4" + integrity sha512-mONgeNSMuyjIe0lkQPa9YhdmTv8P19IeHV0biYhcXhbd5dhdB9HSK93zBpyKjp6wersSUgT5QyU0skmejUVP2A== + import-cwd@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz#20845547718015126ea9b3676b7592fb8bd4cf92" @@ -5581,7 +5614,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.6.2: +prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -6051,7 +6084,7 @@ react-fast-compare@^2.0.1: resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== -react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: +react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -6069,6 +6102,18 @@ react-is@^17.0.1: opencollective "^1.0.3" opencollective-postinstall "^2.0.2" +react-redux@^7.2.4: + version "7.2.4" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225" + integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA== + dependencies: + "@babel/runtime" "^7.12.1" + "@types/react-redux" "^7.1.16" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.13.1" + react-router-dom@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" @@ -6167,6 +6212,18 @@ reduce-css-calc@^2.1.8: css-unit-converter "^1.1.1" postcss-value-parser "^3.3.0" +redux-thunk@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" + integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== + +redux@^4.0.0, redux@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4" + integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g== + dependencies: + "@babel/runtime" "^7.9.2" + regenerator-runtime@^0.10.0: version "0.10.5" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" @@ -6244,6 +6301,11 @@ requires-port@^1.0.0: resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reselect@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" + integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== + resize-observer-polyfill@^1.5.0, resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"