├── src ├── views │ ├── NotFound │ │ ├── index.less │ │ └── index.tsx │ ├── SubPages1 │ │ ├── Page2 │ │ │ └── index.tsx │ │ └── Page1 │ │ │ └── index.tsx │ ├── SubPages2 │ │ ├── Page1 │ │ │ └── index.tsx │ │ └── Page2 │ │ │ └── index.tsx │ ├── SubPages3 │ │ ├── Page1 │ │ │ └── index.tsx │ │ └── Page2 │ │ │ └── index.tsx │ ├── Index │ │ └── index.tsx │ ├── Home │ │ ├── index.less │ │ └── index.tsx │ └── Login │ │ ├── index.less │ │ └── index.tsx ├── react-app-env.d.ts ├── assets │ └── styles │ │ ├── global.less │ │ └── common.less ├── layout │ ├── Header │ │ ├── index.less │ │ └── index.tsx │ ├── Menu │ │ ├── index.less │ │ └── index.tsx │ └── BreadCrumb │ │ └── index.tsx ├── setupTests.ts ├── App.test.tsx ├── index.tsx ├── components │ └── Button │ │ └── index.tsx ├── utils │ ├── hasPermission.ts │ ├── axios.ts │ └── fn.ts ├── api │ └── testApi.ts ├── interfaces │ ├── menu.ts │ └── routes.ts ├── App.tsx ├── stores │ ├── config.ts │ ├── index.tsx │ ├── test │ │ └── index.ts │ ├── breadCrumb │ │ └── index.ts │ └── login │ │ └── index.ts ├── hooks │ └── useSetState.ts ├── router │ ├── index.tsx │ └── routes.ts ├── logo.svg └── serviceWorker.ts ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── .env.production ├── .env.development ├── .gitignore ├── tsconfig.json ├── tsconfig.paths.json ├── postcss.config.js ├── package.json ├── README.md └── craco.config.js /src/views/NotFound/index.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benzic/React-typescript-umihooks-mobx/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benzic/React-typescript-umihooks-mobx/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benzic/React-typescript-umihooks-mobx/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/assets/styles/global.less: -------------------------------------------------------------------------------- 1 | @global-text-color: #499AF2; // 公共字体颜色 2 | @primary-color : @global-text-color; // 全局主色 -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 生产环境 2 | # 自定义变量 必须以 REACT_APP_ 开头 3 | 4 | NODE_ENV= production 5 | REACT_APP_ENV = production 6 | REACT_APP_BASE_API = "" -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 方便打包不同接口环境 2 | # 开发环境 3 | # 自定义变量 必须以 REACT_APP_ 开头 4 | 5 | PORT = 3000 6 | NODE_ENV= development 7 | REACT_APP_ENV = development 8 | REACT_APP_BASE_API = "" 9 | -------------------------------------------------------------------------------- /src/views/SubPages1/Page2/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | interface IProps { 3 | } 4 | const SubPage: React.FC = ((): JSX.Element => { 5 | return 6 | 这是SubPages-2 7 | 8 | }) 9 | export default SubPage -------------------------------------------------------------------------------- /src/views/SubPages2/Page1/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | interface IProps { 3 | } 4 | const SubPage: React.FC = ((): JSX.Element => { 5 | return 6 | 这是SubPages2-1 7 | 8 | }) 9 | export default SubPage -------------------------------------------------------------------------------- /src/views/SubPages2/Page2/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | interface IProps { 3 | } 4 | const SubPage: React.FC = ((): JSX.Element => { 5 | return 6 | 这是SubPages2-2 7 | 8 | }) 9 | export default SubPage -------------------------------------------------------------------------------- /src/views/SubPages3/Page1/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | interface IProps { 3 | } 4 | const SubPage: React.FC = ((): JSX.Element => { 5 | return 6 | 这是SubPages3-1 7 | 8 | }) 9 | export default SubPage -------------------------------------------------------------------------------- /src/views/SubPages3/Page2/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | interface IProps { 3 | } 4 | const SubPage: React.FC = ((): JSX.Element => { 5 | return 6 | 这是SubPages3-2 7 | 8 | }) 9 | export default SubPage -------------------------------------------------------------------------------- /src/layout/Header/index.less: -------------------------------------------------------------------------------- 1 | .header { 2 | &-content { 3 | display : flex; 4 | justify-content: space-between; 5 | } 6 | 7 | &-title { 8 | color : white; 9 | font-size: 20px; 10 | } 11 | } -------------------------------------------------------------------------------- /src/views/NotFound/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './index.less' 3 | interface IProps { 4 | } 5 | const NotFound: React.FC = ((): JSX.Element => { 6 | return 7 | 这是NotFound 8 | 9 | }) 10 | export default NotFound -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/views/Index/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Redirect } from 'react-router-dom' 3 | 4 | export default class Home extends React.Component { 5 | render() { 6 | return ( 7 | 8 | ) 9 | } 10 | } -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import '@/assets/styles/common.less' 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | serviceWorker.unregister(); 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, message } from 'antd' 3 | import { usePersistFn } from 'ahooks' 4 | const ButtonCom: React.FC = ((): JSX.Element => { 5 | const onClickBtn = usePersistFn((): void => { 6 | message.info("点击了按钮") 7 | }) 8 | return 9 | 这是 Button组件按钮 10 | 11 | }) 12 | export default ButtonCom -------------------------------------------------------------------------------- /src/utils/hasPermission.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: your name 3 | * @Date: 2020-09-16 17:48:03 4 | * @LastEditTime: 2020-09-16 17:48:20 5 | * @LastEditors: your name 6 | * @Description: In User Settings Edit 7 | * @FilePath: \react-cli\src\utils\hasPermission.ts 8 | */ 9 | export const hasPermission = (roles: string[], userRole: string[]): boolean => { 10 | if (!userRole) return false 11 | if (!roles) return true 12 | return userRole.some((role: string) => roles.includes(role)) 13 | } -------------------------------------------------------------------------------- /src/api/testApi.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: your name 3 | * @Date: 2020-08-10 17:12:31 4 | * @LastEditTime: 2020-09-16 19:22:34 5 | * @LastEditors: Please set LastEditors 6 | * @Description: In User Settings Edit 7 | * @FilePath: \BigDataAP\src\api\api-aoi.ts 8 | */ 9 | import axios from '@/utils/axios'; 10 | 11 | // 公共变量 12 | const config = '/test'; 13 | export function getTestApi(params?: any): Promise { 14 | return axios({ 15 | url: `${config}/api`, 16 | params 17 | }); 18 | } -------------------------------------------------------------------------------- /src/assets/styles/common.less: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | width : 100%; 5 | height: 100%; 6 | } 7 | 8 | #ajax_loading { 9 | position: absolute; 10 | width : 100%; 11 | height : 100%; 12 | left : 0; 13 | top : 0; 14 | display : none; 15 | 16 | .ajax_loading_wrapper { 17 | display : flex; 18 | align-items : center; 19 | justify-content: center; 20 | width : 100%; 21 | height : 100%; 22 | } 23 | } -------------------------------------------------------------------------------- /src/interfaces/menu.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: your name 3 | * @Date: 2020-09-17 14:44:26 4 | * @LastEditTime: 2020-09-17 14:47:07 5 | * @LastEditors: Please set LastEditors 6 | * @Description: In User Settings Edit 7 | * @FilePath: \react-cli\src\interfaces\menu.ts 8 | */ 9 | export interface stateProps { 10 | menuMode: "inline" | "horizontal" | "vertical" | "vertical-left" | "vertical-right" | undefined, 11 | list: testObj 12 | } 13 | 14 | type testObj = { 15 | test: string, 16 | otherKey: string 17 | } -------------------------------------------------------------------------------- /src/layout/Menu/index.less: -------------------------------------------------------------------------------- 1 | .menu-wrapper { 2 | height : 100%; 3 | box-sizing: border-box; 4 | 5 | .ant-layout-sider { 6 | height: 100%; 7 | } 8 | 9 | .logo { 10 | height : 60px; 11 | width : 100%; 12 | background : #001529; 13 | color : white; 14 | font-size : 24px; 15 | text-align : center; 16 | line-height: 60px; 17 | } 18 | 19 | .ant-layout-sider-trigger { 20 | background: #001529; 21 | color : white; 22 | } 23 | } -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Routes from '@/router/index'; 3 | import { Spin } from "antd" 4 | import { StoreProvider } from '@stores/index'; 5 | // 日期组件,antd依赖 6 | import moment from 'moment'; 7 | import 'moment/locale/zh-cn'; 8 | moment.locale('zh-cn') 9 | 10 | const App = () => 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | > 21 | 22 | export default App; -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/layout/BreadCrumb/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Breadcrumb } from 'antd' 3 | import { useStore } from '@/stores' 4 | import { routeTypes } from '@/interfaces/routes' 5 | const BreadCrumb: React.FC = () => { 6 | const { breadcrumbStore } = useStore() 7 | const { breadData } = breadcrumbStore 8 | return 9 | { 10 | breadData.map((item: routeTypes, index: number) => { 11 | return {item.name} 12 | }) 13 | } 14 | 15 | } 16 | export default BreadCrumb -------------------------------------------------------------------------------- /src/stores/config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: your name 3 | * @Date: 2020-09-16 18:53:26 4 | * @LastEditTime: 2020-09-17 11:10:55 5 | * @LastEditors: Please set LastEditors 6 | * @Description: In User Settings Edit 7 | * @FilePath: \react-cli\src\stores\config.ts 8 | */ 9 | import counterStore from './test/index'; 10 | import loginStore from './login/index'; 11 | import breadcrumbStore from './breadCrumb/index' 12 | export function createStore() { 13 | return { 14 | counterStore, 15 | loginStore, 16 | breadcrumbStore 17 | } 18 | } 19 | 20 | export const store = createStore(); 21 | 22 | export type TStore = ReturnType; 23 | -------------------------------------------------------------------------------- /src/stores/index.tsx: -------------------------------------------------------------------------------- 1 | import { useLocalStore } from 'mobx-react-lite'; 2 | import * as React from 'react'; 3 | import { createStore, TStore } from './config'; 4 | 5 | const storeContext = React.createContext(null); 6 | 7 | export const StoreProvider = ({ children }: any) => { 8 | const store = useLocalStore(createStore); 9 | return {children}; 10 | }; 11 | 12 | export const useStore = () => { 13 | const store = React.useContext(storeContext); 14 | if (!store) { 15 | throw new Error('You have forgot to use StoreProvider.'); 16 | } 17 | return store; 18 | }; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.paths.json", 3 | "compilerOptions": { 4 | "experimentalDecorators": true, 5 | "target": "es5", 6 | "lib": [ 7 | "dom", 8 | "dom.iterable", 9 | "esnext" 10 | ], 11 | "allowJs": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | "strict": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | "jsx": "react", 23 | "baseUrl": "." 24 | }, 25 | "include": [ 26 | "src" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/views/Home/index.less: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width : 100%; 3 | height: 100%; 4 | 5 | .layout { 6 | width : 100%; 7 | height : 100%; 8 | display : flex; 9 | flex-direction: row; 10 | 11 | &_right { 12 | .wrapper_box { 13 | background: #f0f2f5; 14 | width : 100%; 15 | height : 100%; 16 | padding : 16px; 17 | box-sizing: border-box; 18 | } 19 | 20 | .wrapper_content { 21 | padding : 16px; 22 | border-radius: 6px; 23 | background : white; 24 | width : 100%; 25 | height : 100%; 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/stores/test/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: your name 3 | * @Date: 2020-09-16 18:53:08 4 | * @LastEditTime: 2020-09-17 15:28:21 5 | * @LastEditors: Please set LastEditors 6 | * @Description: In User Settings Edit 7 | * @FilePath: \react-cli\src\stores\test\index.ts 8 | */ 9 | import { observable, action } from 'mobx' 10 | export type CountStoreType = { 11 | counter: number, 12 | onIncrement: () => void, 13 | onDecrement: () => void 14 | }; 15 | // 观察者方式 16 | class counterStoreClass { 17 | @observable counter: number = 0 18 | @action.bound 19 | onIncrement = (): void => { 20 | this.counter++; 21 | } 22 | onDecrement = (): void => { 23 | this.counter--; 24 | } 25 | } 26 | const counterStore: CountStoreType = new counterStoreClass(); 27 | export default counterStore; -------------------------------------------------------------------------------- /src/stores/breadCrumb/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: your name 3 | * @Date: 2020-09-17 11:06:35 4 | * @LastEditTime: 2020-09-17 14:55:21 5 | * @LastEditors: Please set LastEditors 6 | * @Description: In User Settings Edit 7 | * @FilePath: \react-cli\src\stores\breadCrumb\index.ts 8 | */ 9 | 10 | import { observable, action } from 'mobx' 11 | import { routeTypes } from '@/interfaces/routes' 12 | export type breadcrumbStoreType = { 13 | breadData: routeTypes[], 14 | onChange: (val: routeTypes[]) => void 15 | }; 16 | class breadcrumbStoreClass { 17 | @observable breadData: routeTypes[] = [] 18 | 19 | @action.bound 20 | onChange(breadData: routeTypes[]): void { 21 | this.breadData = breadData 22 | } 23 | } 24 | const breadcrumbStore: breadcrumbStoreType = new breadcrumbStoreClass() 25 | export default breadcrumbStore; -------------------------------------------------------------------------------- /src/interfaces/routes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: your name 3 | * @Date: 2020-09-16 19:30:24 4 | * @LastEditTime: 2020-09-17 14:42:58 5 | * @LastEditors: Please set LastEditors 6 | * @Description: In User Settings Edit 7 | * @FilePath: \react-cli\src\interfaces\routes.ts 8 | */ 9 | import { ComponentClass, FunctionComponent } from 'react' //route在React-router中有专门的RouteProps 我这里只是来举个例子 10 | import { RouteComponentProps } from 'react-router-dom' 11 | export interface routeTypes { 12 | path: string, 13 | component?: React.ComponentType> | React.ComponentType, 14 | requiresAuth?: boolean, 15 | icon?: ComponentClass | FunctionComponent, 16 | name?: string, 17 | meta?: metaType, 18 | exact?: boolean, 19 | children?: routeTypes[] 20 | } 21 | 22 | export interface metaType { 23 | roles?: string[] 24 | } -------------------------------------------------------------------------------- /tsconfig.paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": [ 6 | "src/*" 7 | ], 8 | "@api/*": [ 9 | "src/api/*" 10 | ], 11 | "@assets/*": [ 12 | "src/assets/*" 13 | ], 14 | "@components/*": [ 15 | "src/components/*" 16 | ], 17 | "@hooks/*": [ 18 | "src/hooks/*" 19 | ], 20 | "@interfaces/*": [ 21 | "src/interfaces/*" 22 | ], 23 | "@layout/*": [ 24 | "src/layout/*" 25 | ], 26 | "@router/*": [ 27 | "src/router/*" 28 | ], 29 | "@stores/*": [ 30 | "src/stores/*" 31 | ], 32 | "@utils/*": [ 33 | "src/utils/*" 34 | ], 35 | "@views/*": [ 36 | "src/views/*" 37 | ] 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/layout/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Layout } from 'antd' 3 | import { withRouter, RouteComponentProps } from 'react-router-dom' 4 | import { fn } from '@/utils/fn' 5 | import { usePersistFn } from 'ahooks' 6 | import BreadCrumb from '@layout/BreadCrumb' 7 | import './index.less' 8 | const { Header } = Layout; 9 | const TopHeader = (props: RouteComponentProps) => { 10 | const onLogout = usePersistFn((): void => { 11 | fn.removeLocalStorage("login") 12 | props.history.replace("/login") 13 | }) 14 | return ( 15 | 16 | 17 | React后台管理系统 18 | 19 | 20 | 退出登录 21 | 22 | 23 | 24 | ) 25 | } 26 | 27 | export default withRouter(TopHeader) -------------------------------------------------------------------------------- /src/views/Login/index.less: -------------------------------------------------------------------------------- 1 | .login__form--warpper { 2 | display : flex; 3 | align-items : center; 4 | justify-content: center; 5 | height : 100%; 6 | width : 100%; 7 | 8 | .login__form--contain { 9 | background : white; 10 | border-radius: 6px; 11 | padding : 32px 48px; 12 | border : 1px solid #888888; 13 | 14 | .login__form--title { 15 | font-weight : bold; 16 | margin-bottom: 24px; 17 | text-align : center; 18 | } 19 | 20 | .login__form--item { 21 | display : flex; 22 | align-items : center; 23 | margin-bottom: 32px; 24 | 25 | .login__form--item--icon { 26 | width : 36px; 27 | background : #888888; 28 | height : 36px; 29 | margin-right: 16px; 30 | } 31 | 32 | .login__form--item--input { 33 | width: 300px; 34 | } 35 | } 36 | 37 | .login__form--submit { 38 | margin-left: 52px; 39 | width : 300px; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: your name 3 | * @Date: 2020-09-16 10:31:17 4 | * @LastEditTime: 2020-09-16 20:30:20 5 | * @LastEditors: Please set LastEditors 6 | * @Description: In User Settings Edit 7 | * @FilePath: \vue-cli\.postcssrc.js 8 | */ 9 | module.exports = { 10 | "plugins": { 11 | // 官方说明文档 https://github.com/evrone/postcss-px-to-viewport/blob/HEAD/README_CN.md 12 | "postcss-px-to-viewport": { 13 | unitToConvert: 'px', //需要转换的单位 14 | viewportWidth: 1920, // 设计稿的视口宽度 15 | unitPrecision: 5, // 单位转换后保留的精度 16 | propList: ['*'], // 能转行为vw的属性列表 17 | viewportUnit: 'vw', // 希望使用的视口单位 18 | fontViewportUnit: 'vw', // 字体使用的视口单位 19 | selectorBlackList: [], // 需要忽略的CSS选择器,不会转为视口单位,使用原有的px等单位。 20 | minPixelValue: 1, // 设置最小的转换数值,如果为1的话,只有大于1的值会被转换 21 | mediaQuery: false, // 媒体查询里的单位是否需要转换单位 22 | replace: true, // 是否直接更换属性值,而不添加备用属性 23 | exclude: [], // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件 24 | landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape) 25 | landscapeUnit: 'vw', // 横屏时使用的单 26 | landscapeWidth: 568 //横屏时使用的视口宽度 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/stores/login/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: your name 3 | * @Date: 2020-09-16 18:53:08 4 | * @LastEditTime: 2020-09-17 15:07:47 5 | * @LastEditors: Please set LastEditors 6 | * @Description: In User Settings Edit 7 | * @FilePath: \react-cli\src\stores\test\index.ts 8 | */ 9 | import { observable, action } from 'mobx' 10 | import { fn } from '@/utils/fn' 11 | export type loginStoreType = { 12 | userInfo: userInfoType | null, 13 | login: (loginInfo: userInfoType) => Promise, 14 | loginout: () => void 15 | }; 16 | interface userInfoType { 17 | username: string, password: string 18 | } 19 | class loginClass { 20 | @observable userInfo: userInfoType | null = null 21 | @action.bound 22 | login(loginInfo: userInfoType): Promise { 23 | return new Promise((resolve, reject) => { 24 | fn.setLocalStorage("login", true) 25 | fn.setLocalStorage("userInfo", loginInfo) 26 | this.userInfo = loginInfo 27 | resolve(loginInfo) 28 | }) 29 | } 30 | loginout = (): void => { 31 | fn.removeLocalStorage("login") 32 | fn.removeLocalStorage("userInfo") 33 | this.userInfo = null 34 | } 35 | } 36 | const loginStore: loginStoreType = new loginClass(); 37 | export default loginStore; -------------------------------------------------------------------------------- /src/hooks/useSetState.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: your name 3 | * @Date: 2020-08-12 16:54:40 4 | * @LastEditTime: 2020-09-21 11:29:45 5 | * @LastEditors: Please set LastEditors 6 | * @Description: In User Settings Edit 7 | * @FilePath: \BigDataAP\src\hooks\useImmer.ts 8 | */ 9 | 10 | import { useState, useEffect, useRef, useCallback } from 'react' 11 | export const useSetState = ( 12 | initialState: T = {} as T, 13 | ): [T, (patch: Partial | ((prevState: T) => Partial), cb?: Function) => void] => { 14 | const [state, setState] = useState(initialState); 15 | const callBack = useRef(null) 16 | const setMergeState = useCallback( 17 | (patch, cb) => { 18 | callBack.current = cb; 19 | setState((prevState) => { 20 | if (Object.prototype.toString.call(patch).slice(8, -1) === 'Object') { 21 | return Object.assign({}, prevState, patch) 22 | } else { 23 | return patch 24 | } 25 | }); 26 | }, 27 | [setState], 28 | ); 29 | useEffect(() => { 30 | callBack.current && callBack.current(state) 31 | }, [state]) 32 | return [state, setMergeState]; 33 | }; 34 | 35 | export default useSetState -------------------------------------------------------------------------------- /src/views/SubPages1/Page1/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useDebounceEffect, useMount, useUnmount, useUpdateEffect } from 'ahooks' 3 | import { Button } from 'antd' 4 | import { observer } from 'mobx-react-lite' 5 | import { useStore } from '@/stores'; 6 | import { getTestApi } from '@/api/testApi' 7 | import { useSetState } from '@/hooks/useSetState' 8 | import ButtonCom from '@/components/Button' 9 | interface IProps { } 10 | const SubPage: React.FC = ((): JSX.Element => { 11 | const { counterStore } = useStore(); 12 | const { counter, onIncrement, onDecrement } = counterStore 13 | 14 | const [state, setState] = useSetState(12) 15 | useMount(() => { 16 | console.log("执行了页面加载") 17 | }) 18 | useUnmount(() => { 19 | console.log("执行了页面卸载") 20 | }) 21 | useUpdateEffect(() => { 22 | console.log("counter change:" + counter) 23 | setState(333, (newState: any) => { 24 | console.log("setState的回调:", newState) 25 | }) 26 | console.log("修改完毕后的当前数值:", state) 27 | }, [counter]) 28 | useEffect(() => { 29 | console.log('useEffect监听数值变化:', state) 30 | }, [state]) 31 | useDebounceEffect(() => { 32 | console.log("counter debounce:" + counter) 33 | }, [counter]) 34 | return 35 | 这是SubPages-1 36 | { 37 | getTestApi() 38 | onIncrement() 39 | }}>增加 40 | { 41 | onDecrement() 42 | }}>减少 43 | count:{counter} 44 | 45 | 46 | }) 47 | export default observer(SubPage) -------------------------------------------------------------------------------- /src/utils/axios.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: your name 3 | * @Date: 2020-09-16 19:21:01 4 | * @LastEditTime: 2020-09-17 15:12:12 5 | * @LastEditors: Please set LastEditors 6 | * @Description: In User Settings Edit 7 | * @FilePath: \react-cli\src\utils\axios.ts 8 | */ 9 | import axios from 'axios' 10 | let loading: HTMLElement | null = null 11 | //创建axios实例 12 | const service = axios.create({ 13 | baseURL: process.env.REACT_APP_BASE_API, // api的base_url 14 | timeout: 200000, // 请求超时时间 15 | withCredentials: true // 选项表明了是否是跨域请求 16 | }) 17 | service.interceptors.request.use(config => { 18 | const flag = true 19 | if (flag) { 20 | loading = document.getElementById('ajax_loading') 21 | loading && (loading.style.display = 'block') 22 | } 23 | return config; 24 | }, err => { 25 | console.log('请求失败') 26 | return Promise.reject(err) 27 | }) 28 | 29 | 30 | 31 | //拦截响应 32 | service.interceptors.response.use(config => { 33 | if (loading) { 34 | loading = document.getElementById('ajax_loading') 35 | loading && (loading.style.display = 'none') 36 | loading = null 37 | } 38 | return config; 39 | }, err => { 40 | if (loading) { 41 | loading = document.getElementById('ajax_loading') 42 | loading && (loading.style.display = 'none') 43 | loading = null 44 | } 45 | console.log('响应失败') 46 | return Promise.reject(err) 47 | }) 48 | 49 | 50 | 51 | // respone拦截器 52 | service.interceptors.response.use( 53 | response => { 54 | const res = response.data 55 | if (res.status !== 1) { 56 | return Promise.reject('error') 57 | } else { 58 | return response.data 59 | } 60 | }, 61 | error => { 62 | return Promise.reject(error) 63 | } 64 | ) 65 | export default service 66 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | You need to enable JavaScript to run this app. 31 | 32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-cli", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@craco/craco": "^5.6.4", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.3.2", 9 | "@testing-library/user-event": "^7.1.2", 10 | "@types/jest": "^24.0.0", 11 | "@types/node": "^12.0.0", 12 | "@types/react": "^16.9.0", 13 | "@types/react-dom": "^16.9.0", 14 | "@types/react-router-dom": "^5.1.5", 15 | "ahooks": "^2.6.0", 16 | "antd": "^4.6.4", 17 | "axios": "^0.20.0", 18 | "craco-alias": "^2.1.1", 19 | "craco-antd": "^1.18.1", 20 | "mobx": "^5.15.6", 21 | "mobx-react-lite": "^2.2.2", 22 | "postcss": "^8.0.3", 23 | "dotenv-cli": "^3.1.0", 24 | "react": "^16.13.1", 25 | "react-dom": "^16.13.1", 26 | "react-router-dom": "^5.2.0", 27 | "react-scripts": "3.4.3", 28 | "typescript": "~3.7.2", 29 | "use-immer": "^0.4.1" 30 | }, 31 | "devDependencies": { 32 | "@types/terser-webpack-plugin": "^2.2.0", 33 | "postcss-px-to-viewport": "^1.1.1", 34 | "babel-loader": "^8.1.0", 35 | "compression-webpack-plugin": "^4.0.0", 36 | "speed-measure-webpack-plugin": "^1.3.3", 37 | "terser-webpack-plugin": "^3.0.1", 38 | "thread-loader": "^2.1.3", 39 | "webpack-bundle-analyzer": "^3.8.0", 40 | "webpackbar": "^4.0.0" 41 | }, 42 | "scripts": { 43 | "start": "dotenv -e .env.development craco start", 44 | "build": "detenv -e .env.production craco build", 45 | "test": "craco test", 46 | "eject": "craco eject" 47 | }, 48 | "eslintConfig": { 49 | "extends": "react-app" 50 | }, 51 | "browserslist": { 52 | "production": [ 53 | ">0.2%", 54 | "not dead", 55 | "not op_mini all" 56 | ], 57 | "development": [ 58 | "last 1 chrome version", 59 | "last 1 firefox version", 60 | "last 1 safari version" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode. 10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits. 13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode. 18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder. 23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes. 26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /src/views/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Input, Button, message } from 'antd' 3 | import { withRouter, RouteComponentProps } from 'react-router-dom' 4 | import { observer } from 'mobx-react-lite' 5 | import { useStore } from '@/stores'; 6 | import { usePersistFn } from 'ahooks'; 7 | import './index.less' 8 | import { useImmer } from 'use-immer'; 9 | interface userInfoType { 10 | username: string, password: string 11 | } 12 | const LoginIndex = (props: RouteComponentProps) => { 13 | const { loginStore } = useStore() 14 | const { login } = loginStore 15 | const [loginInfo, setLoginInfo] = useImmer({ 16 | username: "", 17 | password: "" 18 | }) 19 | const onSubmit = usePersistFn(async () => { 20 | try { 21 | if (!loginInfo.username || !loginInfo.password) { 22 | message.error("请输入用户名和密码") 23 | return 24 | } 25 | const res: userInfoType = await login(loginInfo) 26 | console.log(res) 27 | props.history.replace("/pages") 28 | } catch (error) { 29 | 30 | } 31 | }) 32 | return ( 33 | 34 | 登录 35 | 36 | 37 | { 38 | e.persist(); 39 | setLoginInfo(state => { 40 | state.username = e?.target?.value 41 | }) 42 | }}> 43 | 44 | 45 | 46 | { 47 | e.persist(); 48 | setLoginInfo(state => { 49 | state.password = e?.target?.value 50 | }) 51 | }}> 52 | 53 | 登录 54 | 55 | ) 56 | } 57 | 58 | export default withRouter(observer(LoginIndex)) -------------------------------------------------------------------------------- /src/router/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HashRouter as Router, Switch, Route, Redirect, RouteComponentProps } from 'react-router-dom'; 3 | import { fn, each } from '@/utils/fn'; 4 | import { useStore } from '@/stores' 5 | import { usePersistFn } from 'ahooks'; 6 | import { routeTypes } from '@/interfaces/routes' 7 | import routesMap from '@router/routes' 8 | const Routes: React.FC = () => { 9 | const { breadcrumbStore } = useStore() 10 | const { onChange } = breadcrumbStore 11 | const getRoutePath = usePersistFn((routes: routeTypes[], path: string, rootPath: routeTypes[]) => { 12 | each(routes, (item: routeTypes) => { 13 | if (path) { 14 | if (item.path === path) { 15 | rootPath.push(item) 16 | return false 17 | } else if (path.indexOf(item.path) !== -1) { 18 | rootPath.push(item) 19 | if (item.children) { 20 | rootPath = getRoutePath(item.children, path, rootPath) 21 | } 22 | } 23 | } else { 24 | return false 25 | } 26 | }) 27 | return rootPath 28 | }) 29 | return ( 30 | 31 | 32 | { 33 | routesMap.map((item: routeTypes, index: number) => { 34 | return { 39 | onChange(getRoutePath(routesMap, props.location.pathname, [])) 40 | let Component: any = item.component 41 | //如果是不需要权限 或者 已登录 或者 访问路径是/login,则直接返回当前组件 42 | if (!item.requiresAuth || fn.getLocalStorage('login') || item.path === "/login") { 43 | return 44 | } 45 | //否则重定向到/login 46 | return 47 | }} 48 | /> 49 | }) 50 | } 51 | 52 | 53 | ) 54 | } 55 | 56 | export default Routes 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/views/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react' 2 | import { usePersistFn } from 'ahooks' 3 | import { Route, Switch, withRouter, RouteComponentProps } from 'react-router-dom' 4 | import { Layout } from 'antd'; 5 | import { hasPermission } from '@/utils/hasPermission' 6 | import { routeTypes } from '@/interfaces/routes' 7 | import Menu from '@/layout/Menu'; 8 | import Header from '@/layout/Header' 9 | import Routes from '@/router/routes' 10 | import NotFound from '@/views/NotFound' 11 | import './index.less' 12 | const { Content } = Layout; 13 | const Home: React.FC = ((props: RouteComponentProps): JSX.Element => { 14 | const loadFirstPage = useRef(false) 15 | const getPermissionRoutes = usePersistFn((Routes: routeTypes[]): React.ReactNode => { 16 | const userRole: string[] = ['admin'] 17 | return Routes.map((item: routeTypes, index: number) => { 18 | if (item.children && item.children.length > 0) { 19 | return getPermissionRoutes(item.children) 20 | } else { 21 | if (item?.meta?.roles) { 22 | if (hasPermission(item?.meta?.roles, userRole)) { 23 | if (!loadFirstPage.current) { 24 | props.history.replace(item.path) 25 | loadFirstPage.current = true 26 | } 27 | return 28 | } else { 29 | return null 30 | } 31 | } else { 32 | return 33 | } 34 | } 35 | }) 36 | }) 37 | return 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {getPermissionRoutes(Routes[1].children as routeTypes[])} 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | }) 56 | export default withRouter(Home) -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/layout/Menu/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { Menu, Layout } from 'antd' 4 | import { usePersistFn } from 'ahooks' 5 | import { useImmer } from 'use-immer' 6 | import { hasPermission } from '@/utils/hasPermission' 7 | import { routeTypes } from '@/interfaces/routes' 8 | import { stateProps } from '@/interfaces/menu' 9 | import Routes from '@/router/routes' 10 | import './index.less' 11 | const { Sider } = Layout 12 | const MenuDom: React.FC = ((): JSX.Element => { 13 | const [state, setState] = useImmer({ 14 | menuMode: "inline", 15 | list: { 16 | test: "test", 17 | otherKey: "otherKey" 18 | } 19 | }) 20 | const onChangeCollapse = usePersistFn((val: boolean): void => { 21 | setState(state => { 22 | state.menuMode = !val ? 'inline' : 'horizontal'; 23 | state.list.test = 'test update' 24 | }) 25 | }) 26 | const getMenuItem = usePersistFn((item: routeTypes): React.ReactNode => { 27 | return 28 | 29 | 30 | {item.icon && React.createElement(item.icon)} 31 | {item.name} 32 | 33 | 34 | 35 | }) 36 | const getMenuList = usePersistFn((routes: routeTypes[]): React.ReactNode => { 37 | const userRole: string[] = ['admin'] 38 | return 39 | {routes.map((item: routeTypes) => { 40 | if (item.children && item.children.length > 0) { 41 | return ( 44 | {item.icon && React.createElement(item.icon)} 45 | {item.name} 46 | } > 47 | {getMenuList(item.children)} 48 | ) 49 | } else { 50 | if (item?.meta) { 51 | if (hasPermission(item.meta.roles as string[], userRole)) { 52 | return getMenuItem(item) 53 | } else { 54 | return null 55 | } 56 | } else { 57 | return getMenuItem(item) 58 | } 59 | } 60 | })} 61 | 62 | }) 63 | return 64 | 65 | LOGO 66 | {getMenuList(Routes[1].children as routeTypes[])} 67 | 68 | 69 | }) 70 | export default MenuDom -------------------------------------------------------------------------------- /src/router/routes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: your name 3 | * @Date: 2020-09-16 14:02:59 4 | * @LastEditTime: 2020-09-17 13:54:34 5 | * @LastEditors: Please set LastEditors 6 | * @Description: In User Settings Edit 7 | * @FilePath: \react-cli\src\router\routes.ts 8 | */ 9 | import LoginIndex from '@/views/Login' 10 | import Index from '@/views/Index' 11 | import HomeIndex from '@/views/Home' 12 | import SubPages11 from '@/views/SubPages1/Page1' 13 | import SubPages12 from '@/views/SubPages1/Page2' 14 | import SubPages21 from '@/views/SubPages2/Page1' 15 | import SubPages22 from '@/views/SubPages2/Page2' 16 | import SubPages31 from '@/views/SubPages3/Page1' 17 | import SubPages32 from '@/views/SubPages3/Page2' 18 | import NotFound from '@/views/NotFound' 19 | import { AndroidOutlined, AppleOutlined, DingdingOutlined, IeOutlined, ChromeOutlined, GithubOutlined, AlipayCircleOutlined, ZhihuOutlined } from '@ant-design/icons' 20 | import { routeTypes } from '@/interfaces/routes' 21 | const routes: routeTypes[] = [ 22 | { 23 | path: '/', 24 | exact: true, 25 | component: Index, 26 | requiresAuth: false, 27 | }, 28 | { 29 | path: '/pages', 30 | component: HomeIndex, 31 | requiresAuth: true, 32 | children: [{ 33 | path: '/pages/sub1', 34 | name: 'SubPages1', 35 | icon: AndroidOutlined, 36 | children: [{ 37 | path: "/pages/sub1/page1", 38 | component: SubPages11, 39 | name: 'SubPage1', 40 | icon: AppleOutlined, 41 | meta: { 42 | roles: ['admin'] 43 | } 44 | }, 45 | { 46 | path: "/pages/sub1/page2", 47 | component: SubPages12, 48 | name: 'SubPage2', 49 | icon: DingdingOutlined, 50 | meta: { 51 | roles: ['ss'] 52 | } 53 | }] 54 | }, { 55 | path: '/pages/sub2', 56 | name: 'SubPages2', 57 | icon: IeOutlined, 58 | children: [{ 59 | path: "/pages/sub2/page1", 60 | component: SubPages21, 61 | name: 'SubPage1', 62 | icon: ChromeOutlined, 63 | meta: { 64 | roles: ['admin'] 65 | } 66 | }, 67 | { 68 | path: "/pages/sub2/page2", 69 | component: SubPages22, 70 | name: 'SubPage2', 71 | icon: AppleOutlined, 72 | meta: { 73 | roles: ['admin'] 74 | } 75 | }] 76 | }, { 77 | path: '/pages/sub3', 78 | name: 'SubPages3', 79 | icon: GithubOutlined, 80 | children: [{ 81 | path: "/pages/sub3/page1", 82 | component: SubPages31, 83 | name: 'SubPage1', 84 | icon: AlipayCircleOutlined, 85 | meta: { 86 | roles: ['admin'] 87 | } 88 | }, 89 | { 90 | path: "/pages/sub3/page2", 91 | component: SubPages32, 92 | name: 'SubPage2', 93 | icon: ZhihuOutlined, 94 | meta: { 95 | roles: ['admin'] 96 | } 97 | }] 98 | },] 99 | }, 100 | { 101 | path: '/login', 102 | component: LoginIndex, 103 | requiresAuth: false, 104 | }, 105 | { 106 | path: '*', 107 | exact: true, 108 | component: NotFound, 109 | requiresAuth: false, 110 | } 111 | ] 112 | 113 | export default routes -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | 2 | const { POSTCSS_MODES, whenProd } = require("@craco/craco"); 3 | const CracoAliasPlugin = require("craco-alias"); 4 | const CracoAntDesignPlugin = require("craco-antd"); 5 | // 打包信息配置 6 | const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); 7 | // webpack 进度条 8 | const WebpackBar = require('webpackbar'); 9 | // 开启gzip 10 | const CompressionWebpackPlugin = require('compression-webpack-plugin'); 11 | // 压缩js 12 | const TerserPlugin = require('terser-webpack-plugin'); 13 | // 分析打包时间 14 | const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); 15 | const smp = new SpeedMeasurePlugin(); 16 | 17 | const threadLoader = require('thread-loader'); 18 | 19 | const path = require("path"); 20 | const resolve = dir => path.join(__dirname, '..', dir); 21 | 22 | const jsWorkerPool = { 23 | workers: 2, 24 | poolTimeout: 2000 25 | }; 26 | 27 | threadLoader.warmup(jsWorkerPool, ['babel-loader']); 28 | 29 | // 打包取消sourceMap 30 | process.env.GENERATE_SOURCEMAP = "false"; 31 | 32 | // 覆盖默认配置 33 | module.exports = { 34 | webpack: smp.wrap({ 35 | configure: { 36 | /*在这里添加任何webpack配置选项: https://webpack.js.org/configuration */ 37 | module: { 38 | rules: [ 39 | { 40 | test: /\.js$/, 41 | exclude: /node_modules/, 42 | use: [ 43 | { 44 | loader: 'thread-loader', 45 | options: jsWorkerPool 46 | }, 47 | 'babel-loader?cacheDirectory' 48 | ] 49 | }] 50 | }, 51 | resolve: { 52 | modules: [ // 指定以下目录寻找第三方模块,避免webpack往父级目录递归搜索 53 | resolve('src'), 54 | resolve('node_modules'), 55 | ], 56 | alias: { 57 | "@": resolve("src") // 缓存src目录为@符号,避免重复寻址 58 | } 59 | }, 60 | optimization: { 61 | // 开发环境不压缩 62 | minimize: process.env.REACT_APP_ENV !== 'development' ? true : false, 63 | splitChunks: { 64 | chunks: 'all', // initial、async和all 65 | minSize: 30000, // 形成一个新代码块最小的体积 66 | maxAsyncRequests: 5, // 按需加载时候最大的并行请求数 67 | maxInitialRequests: 3, // 最大初始化请求数 68 | automaticNameDelimiter: '~', // 打包分割符 69 | name: true, 70 | cacheGroups: { 71 | vendors: { // 基本框架 72 | chunks: 'all', 73 | test: /(react|react-dom|react-dom-router|babel-polyfill|mobx)/, 74 | priority: 100, 75 | name: 'vendors', 76 | }, 77 | 'async-commons': { // 其余异步加载包 78 | chunks: 'async', 79 | minChunks: 2, 80 | name: 'async-commons', 81 | priority: 90, 82 | }, 83 | commons: { // 其余同步加载包 84 | chunks: 'all', 85 | minChunks: 2, 86 | name: 'commons', 87 | priority: 80, 88 | } 89 | } 90 | } 91 | }, 92 | }, 93 | plugins: [ 94 | // webpack进度条 95 | new WebpackBar({ color: 'green', profile: true }), 96 | // 打包时,启动插件 97 | ...whenProd(() => [ 98 | // 压缩js 同时删除console debug等 99 | new TerserPlugin({ 100 | parallel: true, // 多线程 101 | terserOptions: { 102 | ie8: true, 103 | // 删除注释 104 | output: { 105 | comments: false 106 | }, 107 | //删除console 和 debugger 删除警告 108 | compress: { 109 | drop_debugger: true, 110 | drop_console: true 111 | } 112 | } 113 | }), 114 | // 开启gzip 115 | new CompressionWebpackPlugin({ 116 | // 是否删除源文件,默认: false 117 | deleteOriginalAssets: false 118 | }), 119 | // 打包分析 120 | new BundleAnalyzerPlugin() 121 | ], []) 122 | ] 123 | }), 124 | style: { 125 | // 自适应方案 126 | postcss: { 127 | mode: POSTCSS_MODES.file 128 | } 129 | }, 130 | plugins: [ 131 | // antd 按需加载 less等配置 132 | { 133 | plugin: CracoAntDesignPlugin, 134 | options: { 135 | // 自定义主题 136 | customizeThemeLessPath: path.join(__dirname, "src/assets/styles/global.less") 137 | } 138 | }, 139 | // 插件方式,设置别名 140 | { 141 | plugin: CracoAliasPlugin, 142 | options: { 143 | source: "tsconfig", 144 | tsConfigPath: "tsconfig.paths.json" 145 | } 146 | }, 147 | ], 148 | devServer: { 149 | proxy: { 150 | '/': { 151 | target: 'www.test.com', // 开发路由代理 152 | ws: false, // websocket 153 | changeOrigin: true, //是否跨域 154 | secure: false, // 如果是https接口,需要配置这个参数 155 | pathRewrite: {} 156 | } 157 | } 158 | } 159 | }; -------------------------------------------------------------------------------- /src/utils/fn.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: your name 3 | * @Date: 2020-09-16 14:52:59 4 | * @LastEditTime: 2020-09-17 15:12:34 5 | * @LastEditors: Please set LastEditors 6 | * @Description: In User Settings Edit 7 | * @FilePath: \react-cli\src\utils\fn.ts 8 | */ 9 | export const fn = { 10 | setLocalStorage(key: string, data: any) { 11 | try { 12 | localStorage.setItem(key, JSON.stringify(data)) 13 | } catch (error) { 14 | console.log(error); 15 | } 16 | }, 17 | getLocalStorage(key: string) { 18 | try { 19 | let data: any = localStorage.getItem(key) 20 | return JSON.parse(data) 21 | } catch (error) { 22 | return null 23 | } 24 | }, 25 | removeAllLocalStorage() { 26 | try { 27 | localStorage.clear() 28 | } catch (error) { 29 | console.log(error); 30 | } 31 | }, 32 | removeLocalStorage(key: string) { 33 | try { 34 | localStorage.removeItem(key) 35 | } catch (error) { 36 | console.log(error); 37 | } 38 | }, 39 | saveSessionStorage(key: string, data: any) { 40 | try { 41 | sessionStorage.setItem(key, JSON.stringify(data)) 42 | } catch (error) { 43 | console.log(error); 44 | } 45 | }, 46 | getSessionStorage(key: string) { 47 | try { 48 | let data: any = sessionStorage.getItem(key) 49 | return JSON.parse(data) 50 | } catch (error) { 51 | console.log(error); 52 | } 53 | }, 54 | removeAllSessionStorage() { 55 | try { 56 | sessionStorage.clear() 57 | } catch (error) { 58 | console.log(error); 59 | } 60 | }, 61 | removeSessionStorage(key: any) { 62 | try { 63 | sessionStorage.removeItem(key) 64 | } catch (error) { 65 | console.log(error); 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * 判断是否是类数组 72 | * @param {any} target 需要判断对象 73 | * @return {boolean} 是否类数组 74 | */ 75 | export function isarraylike(target: any) { 76 | let len = target && target.length; 77 | return len && 'number' === typeof len && 0 <= len && len <= Math.pow(2, 53) - 1; 78 | } 79 | /** 80 | * 判断对象是否包含某直接属性(非原型) 81 | * @param {any} obj 需要判断对象 82 | * @param {string} key 需要判断对象 83 | * @return {boolean} 返回是否存在key值 84 | */ 85 | export function haskey(obj: any, key: string) { 86 | return obj !== null && obj.prototype.hasOwnProperty(key) 87 | } 88 | /** 89 | * 判断是否普通对象 90 | * @param {any} target 需要判断对象 91 | * @return {boolean} 返回是否对象 92 | */ 93 | export function isobject(target: any) { 94 | return typeof (target) === 'object'; 95 | } 96 | /** 97 | * 获取对象自有属性名(不包含原型属性) 98 | * @param {any} obj 对象 99 | * @return {Array} 返回对象key数组 100 | */ 101 | export function getkeys(obj: any) { 102 | if (!isobject(obj)) { 103 | return []; 104 | } 105 | if (Object.keys) { 106 | return Object.keys(obj); 107 | } 108 | let keys = [], 109 | key; 110 | for (key in obj) { 111 | if (haskey(obj, key)) { 112 | keys.push(key); 113 | } 114 | } 115 | return keys; 116 | } 117 | /** 118 | * 遍历工具,dataset可以是数组和对象 119 | * 回调函数 handler( item, index|key, dataset) 120 | * break----用return false 121 | * continue --用return ture 122 | * @param {Object | Array} dataset 对象或者数组 123 | * @param {Function} handler 回调函数 124 | * @param {Context} context 上下文this 125 | * @return {Array} 返回对象或数组 126 | */ 127 | export function each(dataset: any, handler: Function, context?: any) { 128 | let callback = 'undefined' === typeof context ? handler : function (value: any, index?: number, collection?: any) { 129 | return handler.call(context, value, index, collection); 130 | }; 131 | let i, len, res; 132 | if (isarraylike(dataset)) { //类数组 133 | i = 0; 134 | len = dataset.length; 135 | for (; i < len; i++) { 136 | res = callback(dataset[i], i, dataset); 137 | if (false === res) { 138 | break; 139 | } else if (true === res) { 140 | continue; 141 | } 142 | } 143 | } else { //键值对象 144 | let keys = getkeys(dataset); 145 | i = 0; 146 | len = keys.length; 147 | for (; i < len; i++) { 148 | res = callback(dataset[keys[i]], keys[i], dataset); 149 | if (false === res) { 150 | break; 151 | } else if (true === res) { 152 | continue; 153 | } 154 | } 155 | } 156 | return dataset; 157 | } 158 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready 142 | .then(registration => { 143 | registration.unregister(); 144 | }) 145 | .catch(error => { 146 | console.error(error.message); 147 | }); 148 | } 149 | } 150 | --------------------------------------------------------------------------------