From 26fd3c6cdbd731f2713cc8867e6bb571e44a996d Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Sat, 10 Jul 2021 16:39:10 +0800 Subject: [PATCH] refactor: rebuild project struction --- shared/hooks/useEffectOnce.ts | 7 + shared/hooks/useRafState.ts | 26 ++++ shared/hooks/useUnmount.ts | 13 ++ shared/index.tsx | 2 + shared/utils/environment.ts | 3 + web/package.json | 2 + web/src/hooks/useIsMobile.ts | 10 ++ web/src/hooks/useWindowSize.ts | 33 +++++ web/src/routes/Main/Content.tsx | 52 ------- web/src/routes/Main/Content/PageContent.tsx | 132 ++++++++++++++++++ .../Main/Content/Personal/Friends/index.tsx | 50 +++++++ .../Main/{ => Content/Personal}/Sidebar.tsx | 9 +- .../routes/Main/Content/Personal/index.tsx | 18 +++ web/src/routes/Main/Content/index.tsx | 19 +++ web/src/routes/Main/Provider.tsx | 9 +- web/src/routes/Main/SidebarContext.tsx | 38 +++++ web/src/routes/Main/index.tsx | 7 +- web/tsconfig.json | 8 +- web/webpack.config.ts | 8 ++ yarn.lock | 16 ++- 20 files changed, 396 insertions(+), 66 deletions(-) create mode 100644 shared/hooks/useEffectOnce.ts create mode 100644 shared/hooks/useRafState.ts create mode 100644 shared/hooks/useUnmount.ts create mode 100644 shared/utils/environment.ts create mode 100644 web/src/hooks/useIsMobile.ts create mode 100644 web/src/hooks/useWindowSize.ts delete mode 100644 web/src/routes/Main/Content.tsx create mode 100644 web/src/routes/Main/Content/PageContent.tsx create mode 100644 web/src/routes/Main/Content/Personal/Friends/index.tsx rename web/src/routes/Main/{ => Content/Personal}/Sidebar.tsx (93%) create mode 100644 web/src/routes/Main/Content/Personal/index.tsx create mode 100644 web/src/routes/Main/Content/index.tsx create mode 100644 web/src/routes/Main/SidebarContext.tsx diff --git a/shared/hooks/useEffectOnce.ts b/shared/hooks/useEffectOnce.ts new file mode 100644 index 00000000..3705a7e2 --- /dev/null +++ b/shared/hooks/useEffectOnce.ts @@ -0,0 +1,7 @@ +import { EffectCallback, useEffect } from 'react'; + +// Reference: https://github.com/streamich/react-use/blob/master/src/useEffectOnce.ts + +export const useEffectOnce = (effect: EffectCallback) => { + useEffect(effect, []); +}; diff --git a/shared/hooks/useRafState.ts b/shared/hooks/useRafState.ts new file mode 100644 index 00000000..3ed7a657 --- /dev/null +++ b/shared/hooks/useRafState.ts @@ -0,0 +1,26 @@ +import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react'; + +import { useUnmount } from './useUnmount'; + +// Reference: https://github.com/streamich/react-use/blob/master/src/useRafState.ts + +export const useRafState = ( + initialState: S | (() => S) +): [S, Dispatch>] => { + const frame = useRef(0); + const [state, setState] = useState(initialState); + + const setRafState = useCallback((value: S | ((prevState: S) => S)) => { + cancelAnimationFrame(frame.current); + + frame.current = requestAnimationFrame(() => { + setState(value); + }); + }, []); + + useUnmount(() => { + cancelAnimationFrame(frame.current); + }); + + return [state, setRafState]; +}; diff --git a/shared/hooks/useUnmount.ts b/shared/hooks/useUnmount.ts new file mode 100644 index 00000000..7812c051 --- /dev/null +++ b/shared/hooks/useUnmount.ts @@ -0,0 +1,13 @@ +import { useRef } from 'react'; +import { useEffectOnce } from './useEffectOnce'; + +// Reference: https://github.com/streamich/react-use/blob/master/src/useUnmount.ts + +export const useUnmount = (fn: () => any): void => { + const fnRef = useRef(fn); + + // update the ref each render so if it change the newest callback will be invoked + fnRef.current = fn; + + useEffectOnce(() => () => fnRef.current()); +}; diff --git a/shared/index.tsx b/shared/index.tsx index 1c870c4a..dbd4b9b7 100644 --- a/shared/index.tsx +++ b/shared/index.tsx @@ -22,6 +22,7 @@ export { t, setLanguage, useTranslation } from './i18n'; export { useAsync } from './hooks/useAsync'; export { useAsyncFn } from './hooks/useAsyncFn'; export { useMountedState } from './hooks/useMountedState'; +export { useRafState } from './hooks/useRafState'; // manager export { getStorage, setStorage, useStorage } from './manager/storage'; @@ -39,3 +40,4 @@ export type { AppStore, AppDispatch } from './redux/store'; // utils export { getTextColorHex } from './utils/string-helper'; +export { isBrowser, isNavigator } from './utils/environment'; diff --git a/shared/utils/environment.ts b/shared/utils/environment.ts new file mode 100644 index 00000000..1db842d7 --- /dev/null +++ b/shared/utils/environment.ts @@ -0,0 +1,3 @@ +export const isBrowser = typeof window !== 'undefined'; + +export const isNavigator = typeof navigator !== 'undefined'; diff --git a/web/package.json b/web/package.json index 2720b89b..53287a4f 100644 --- a/web/package.json +++ b/web/package.json @@ -24,6 +24,7 @@ "react-redux": "^7.2.4", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", + "react-use-gesture": "^9.1.3", "socket.io-client": "^4.1.2", "tailwindcss": "^2.2.4", "yup": "^0.32.9" @@ -56,6 +57,7 @@ "style-loader": "^3.0.0", "ts-node": "^10.0.0", "tsconfig-paths": "^3.9.0", + "tsconfig-paths-webpack-plugin": "^3.5.1", "typescript": "^4.3.4", "url-loader": "^4.1.1", "webpack": "^5.41.0", diff --git a/web/src/hooks/useIsMobile.ts b/web/src/hooks/useIsMobile.ts new file mode 100644 index 00000000..0915baa6 --- /dev/null +++ b/web/src/hooks/useIsMobile.ts @@ -0,0 +1,10 @@ +import { useWindowSize } from './useWindowSize'; + +/** + * 判定是否为移动版网页 + */ +export function useIsMobile(): boolean { + const { width } = useWindowSize(); + + return width < 768; +} diff --git a/web/src/hooks/useWindowSize.ts b/web/src/hooks/useWindowSize.ts new file mode 100644 index 00000000..d3873cf0 --- /dev/null +++ b/web/src/hooks/useWindowSize.ts @@ -0,0 +1,33 @@ +import { isBrowser, useRafState } from 'pawchat-shared'; +import { useEffect } from 'react'; + +// Reference: https://github.com/streamich/react-use/blob/master/src/useWindowSize.ts + +export const useWindowSize = ( + initialWidth = Infinity, + initialHeight = Infinity +) => { + const [state, setState] = useRafState<{ width: number; height: number }>({ + width: isBrowser ? window.innerWidth : initialWidth, + height: isBrowser ? window.innerHeight : initialHeight, + }); + + useEffect((): (() => void) | void => { + if (isBrowser) { + const handler = () => { + setState({ + width: window.innerWidth, + height: window.innerHeight, + }); + }; + + window.addEventListener('resize', handler); + + return () => { + window.removeEventListener('resize', handler); + }; + } + }, []); + + return state; +}; diff --git a/web/src/routes/Main/Content.tsx b/web/src/routes/Main/Content.tsx deleted file mode 100644 index 74411047..00000000 --- a/web/src/routes/Main/Content.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import { PillTabs, PillTabPane } from '../../components/PillTabs'; -import { useAppSelector } from '../../hooks/useAppSelector'; - -/** - * 主要内容组件 - */ -export const Content: React.FC = React.memo(() => { - const friends = useAppSelector((state) => state.user.friends); - const friendRequests = useAppSelector((state) => state.user.friendRequests); - const userId = useAppSelector((state) => state.user.info?._id); - - return ( -
- - -
-
好友列表
-
{JSON.stringify(friends)}
-
-
- -
-
发送的好友请求
-
- {JSON.stringify( - friendRequests.filter((item) => item.from === userId) - )} -
-
-
- -
-
接受的好友请求
-
- {JSON.stringify( - friendRequests.filter((item) => item.to === userId) - )} -
-
-
- 添加好友} - key="4" - > - 添加好友 - -
-
- ); -}); -Content.displayName = 'Content'; diff --git a/web/src/routes/Main/Content/PageContent.tsx b/web/src/routes/Main/Content/PageContent.tsx new file mode 100644 index 00000000..5ee46e80 --- /dev/null +++ b/web/src/routes/Main/Content/PageContent.tsx @@ -0,0 +1,132 @@ +import React, { useCallback } from 'react'; +import { useSidebarContext } from '../SidebarContext'; +import _isNil from 'lodash/isNil'; +import { useDrag } from 'react-use-gesture'; +import { useIsMobile } from '@/hooks/useIsMobile'; +import clsx from 'clsx'; + +// const PageContentRoot = styled.div` +// display: flex; +// flex-direction: row; +// flex: 1; +// overflow: hidden; +// `; + +// const ContentDetail = styled.div` +// flex: 1; +// background-color: ${(props) => props.theme.style.contentBackgroundColor}; +// display: flex; +// flex-direction: column; +// position: relative; +// overflow: hidden; + +// @media (max-width: 768px) { +// width: ${(props) => `calc(100vw - ${props.theme.style.navbarWidth})`}; +// min-width: ${(props) => `calc(100vw - ${props.theme.style.navbarWidth})`}; +// } +// `; + +// const ContentDetailMask = styled.div` +// position: absolute; +// top: 0; +// left: 0; +// right: 0; +// bottom: 0; +// z-index: 10; +// `; + +// const SidebarContainer = styled.div<{ +// showSidebar: boolean; +// }>` +// ${(props) => props.theme.mixins.transition('width', 0.2)}; +// width: ${(props) => (props.showSidebar ? props.theme.style.sidebarWidth : 0)}; +// background-color: ${(props) => props.theme.style.sidebarBackgroundColor}; +// overflow: hidden; +// display: flex; +// flex-direction: column; +// flex: none; +// `; + +const PageContentRoot: React.FC = (props) => ( +
{props.children}
+); + +interface PageContentProps { + sidebar?: React.ReactNode; +} + +const PageGestureWrapper: React.FC = React.memo((props) => { + const { setShowSidebar } = useSidebarContext(); + + const bind = useDrag( + (state) => { + const { swipe } = state; + const swipeX = swipe[0]; + if (swipeX > 0) { + setShowSidebar(true); + } else if (swipeX < 0) { + setShowSidebar(false); + } + }, + { + axis: 'x', + swipeDistance: 5, + } + ); + + return {props.children}; +}); +PageGestureWrapper.displayName = 'PageGestureWrapper'; + +/** + * 用于渲染实际页面的组件,即除了导航栏剩余的内容 + */ +export const PageContent: React.FC = React.memo((props) => { + const { sidebar, children } = props; + const { showSidebar, setShowSidebar } = useSidebarContext(); + const isMobile = useIsMobile(); + const handleHideSidebar = useCallback(() => { + setShowSidebar(false); + }, []); + + const sidebarEl = _isNil(sidebar) ? null : ( +
+ {props.sidebar} +
+ ); + + // 是否显示遮罩层 + const showMask = + isMobile === true && showSidebar === true && !_isNil(sidebarEl); + + const contentMaskEl = showMask ? ( +
+ ) : null; + + const contentEl = children; + + const el = ( + <> + {sidebarEl} + +
+ {contentMaskEl} + {contentEl} +
+ + ); + + if (isMobile) { + return {el}; + } else { + return {el}; + } +}); +PageContent.displayName = 'PageContent'; diff --git a/web/src/routes/Main/Content/Personal/Friends/index.tsx b/web/src/routes/Main/Content/Personal/Friends/index.tsx new file mode 100644 index 00000000..7f2f5ce9 --- /dev/null +++ b/web/src/routes/Main/Content/Personal/Friends/index.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { useAppSelector } from '@/hooks/useAppSelector'; +import { PillTabPane, PillTabs } from '@/components/PillTabs'; + +/** + * 主要内容组件 + */ +export const FriendPanel: React.FC = React.memo(() => { + const friends = useAppSelector((state) => state.user.friends); + const friendRequests = useAppSelector((state) => state.user.friendRequests); + const userId = useAppSelector((state) => state.user.info?._id); + + return ( + + +
+
好友列表
+
{JSON.stringify(friends)}
+
+
+ +
+
发送的好友请求
+
+ {JSON.stringify( + friendRequests.filter((item) => item.from === userId) + )} +
+
+
+ +
+
接受的好友请求
+
+ {JSON.stringify( + friendRequests.filter((item) => item.to === userId) + )} +
+
+
+ 添加好友} + key="4" + > + 添加好友 + +
+ ); +}); +FriendPanel.displayName = 'FriendPanel'; diff --git a/web/src/routes/Main/Sidebar.tsx b/web/src/routes/Main/Content/Personal/Sidebar.tsx similarity index 93% rename from web/src/routes/Main/Sidebar.tsx rename to web/src/routes/Main/Content/Personal/Sidebar.tsx index e4ad9653..4a3168a1 100644 --- a/web/src/routes/Main/Sidebar.tsx +++ b/web/src/routes/Main/Content/Personal/Sidebar.tsx @@ -1,7 +1,7 @@ import React from 'react'; import clsx, { ClassValue } from 'clsx'; import { Icon } from '@iconify/react'; -import { Avatar } from '../../components/Avatar'; +import { Avatar } from '@/components/Avatar'; const SidebarItem: React.FC<{ className?: ClassValue; @@ -44,12 +44,11 @@ const SidebarSection: React.FC<{ SidebarSection.displayName = 'SidebarSection'; /** - * 侧边栏组件 + * 个人面板侧边栏组件 */ export const Sidebar: React.FC = React.memo(() => { return ( -
- {/* Sidebar */} + <> }> 好友 @@ -81,7 +80,7 @@ export const Sidebar: React.FC = React.memo(() => { > 用户1 -
+ ); }); Sidebar.displayName = 'Sidebar'; diff --git a/web/src/routes/Main/Content/Personal/index.tsx b/web/src/routes/Main/Content/Personal/index.tsx new file mode 100644 index 00000000..27b7d0e7 --- /dev/null +++ b/web/src/routes/Main/Content/Personal/index.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Redirect, Route, Switch } from 'react-router-dom'; +import { PageContent } from '../PageContent'; +import { FriendPanel } from './Friends'; +import { Sidebar } from './Sidebar'; + +export const Personal: React.FC = React.memo(() => { + return ( + }> + + + + + + + ); +}); +Personal.displayName = 'Personal'; diff --git a/web/src/routes/Main/Content/index.tsx b/web/src/routes/Main/Content/index.tsx new file mode 100644 index 00000000..84233b55 --- /dev/null +++ b/web/src/routes/Main/Content/index.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Personal } from './Personal'; +import { Route, Switch, Redirect } from 'react-router-dom'; + +export const MainContent: React.FC = React.memo(() => { + return ( + + + {/* + + + + + */} + + + ); +}); +MainContent.displayName = 'MainContent'; diff --git a/web/src/routes/Main/Provider.tsx b/web/src/routes/Main/Provider.tsx index 73f2f320..481d1c32 100644 --- a/web/src/routes/Main/Provider.tsx +++ b/web/src/routes/Main/Provider.tsx @@ -5,7 +5,7 @@ import { useAsync, userActions, } from 'pawchat-shared'; -import React, { useMemo } from 'react'; +import React from 'react'; import { LoadingSpinner } from '../../components/LoadingSpinner'; import { Provider as ReduxProvider } from 'react-redux'; import { getGlobalUserLoginInfo } from '../../utils/user-helper'; @@ -13,6 +13,7 @@ import _isNil from 'lodash/isNil'; import { loginWithToken } from 'pawchat-shared/model/user'; import { getUserJWT } from '../../utils/jwt-helper'; import { useHistory } from 'react-router'; +import { SidebarContextProvider } from './SidebarContext'; /** * 应用状态管理hooks @@ -76,6 +77,10 @@ export const MainProvider: React.FC = React.memo((props) => { return
出现异常, Store 创建失败
; } - return {props.children}; + return ( + + {props.children} + + ); }); MainProvider.displayName = 'MainProvider'; diff --git a/web/src/routes/Main/SidebarContext.tsx b/web/src/routes/Main/SidebarContext.tsx new file mode 100644 index 00000000..074cece0 --- /dev/null +++ b/web/src/routes/Main/SidebarContext.tsx @@ -0,0 +1,38 @@ +import React, { useContext, useState, useCallback } from 'react'; +import _noop from 'lodash/noop'; + +interface SidebarContextProps { + showSidebar: boolean; + switchSidebar: () => void; + setShowSidebar: React.Dispatch>; +} +const SidebarContext = React.createContext({ + showSidebar: true, + switchSidebar: _noop, + setShowSidebar: _noop, +}); +SidebarContext.displayName = 'SidebarContext'; + +export const SidebarContextProvider: React.FC = React.memo((props) => { + const [showSidebar, setShowSidebar] = useState(true); + + // 切换 + const switchSidebar = useCallback(() => { + setShowSidebar(!showSidebar); + }, [showSidebar]); + + return ( + + {props.children} + + ); +}); +SidebarContextProvider.displayName = 'SidebarContextProvider'; + +export function useSidebarContext(): SidebarContextProps { + const context = useContext(SidebarContext); + + return context; +} diff --git a/web/src/routes/Main/index.tsx b/web/src/routes/Main/index.tsx index 9bdf7bb2..db661811 100644 --- a/web/src/routes/Main/index.tsx +++ b/web/src/routes/Main/index.tsx @@ -1,8 +1,7 @@ import React from 'react'; -import { Content } from './Content'; +import { MainContent } from './Content'; import { Navbar } from './Navbar'; import { MainProvider } from './Provider'; -import { Sidebar } from './Sidebar'; export const MainRoute: React.FC = React.memo(() => { return ( @@ -10,9 +9,7 @@ export const MainRoute: React.FC = React.memo(() => { - - - +
); diff --git a/web/tsconfig.json b/web/tsconfig.json index 3c43903c..280b38d0 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -1,3 +1,9 @@ { - "extends": "../tsconfig.json" + "extends": "../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } } diff --git a/web/webpack.config.ts b/web/webpack.config.ts index 5f2c16f1..47f0eca0 100644 --- a/web/webpack.config.ts +++ b/web/webpack.config.ts @@ -9,7 +9,9 @@ import path from 'path'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import CopyPlugin from 'copy-webpack-plugin'; +import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; +delete process.env.TS_NODE_PROJECT; // https://github.com/dividab/tsconfig-paths-webpack-plugin/issues/32 require('../build/script/buildPublicTranslation.js'); // 编译前先执行一下构建翻译的脚本 const ROOT_PATH = path.resolve(__dirname, './'); @@ -48,6 +50,7 @@ const config: Configuration = { options: { loader: 'tsx', target: 'es2015', + tsconfigRaw: require('./tsconfig.json'), }, }, { @@ -92,6 +95,11 @@ const config: Configuration = { }, resolve: { extensions: ['.tsx', '.ts', '.js', '.css'], + plugins: [ + new TsconfigPathsPlugin({ + configFile: path.resolve(ROOT_PATH, './tsconfig.json'), + }), + ], }, plugins: [ new DefinePlugin({ diff --git a/yarn.lock b/yarn.lock index b86b2f19..1f5fea27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3303,7 +3303,7 @@ engine.io-parser@~4.0.1: dependencies: base64-arraybuffer "0.1.4" -enhanced-resolve@^5.8.0: +enhanced-resolve@^5.7.0, enhanced-resolve@^5.8.0: version "5.8.2" resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz#15ddc779345cbb73e97c611cd00c01c1e7bf4d8b" integrity sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA== @@ -7574,6 +7574,11 @@ react-router@5.2.0, react-router@^5.2.0: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" +react-use-gesture@^9.1.3: + version "9.1.3" + resolved "https://registry.yarnpkg.com/react-use-gesture/-/react-use-gesture-9.1.3.tgz#92bd143e4f58e69bd424514a5bfccba2a1d62ec0" + integrity sha512-CdqA2SmS/fj3kkS2W8ZU8wjTbVBAIwDWaRprX7OKaj7HlGwBasGEFggmk5qNklknqk9zK/h8D355bEJFTpqEMg== + react@^17.0.2: version "17.0.2" resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" @@ -8940,6 +8945,15 @@ ts-node@^10.0.0: source-map-support "^0.5.17" yn "3.1.1" +tsconfig-paths-webpack-plugin@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.5.1.tgz#e4dbf492a20dca9caab60086ddacb703afc2b726" + integrity sha512-n5CMlUUj+N5pjBhBACLq4jdr9cPTitySCjIosoQm0zwK99gmrcTGAfY9CwxRFT9+9OleNWXPRUcxsKP4AYExxQ== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.7.0" + tsconfig-paths "^3.9.0" + tsconfig-paths@^3.9.0: version "3.9.0" resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b"