├── .babelrc ├── .gitignore ├── README.md ├── dll ├── vendor-manifest.json ├── vendor.dll.js └── vendor.dll.js.LICENSE.txt ├── package.json ├── src ├── App.tsx ├── components │ ├── Iconfont │ │ └── index.tsx │ └── Layout │ │ ├── Breadcrumb.tsx │ │ ├── Content.tsx │ │ ├── Header.tsx │ │ ├── Layout.tsx │ │ ├── Logo.tsx │ │ ├── SlideMenu.tsx │ │ ├── _defaultRoutes.ts │ │ └── sty.less ├── config │ └── index.ts ├── index.ejs ├── index.tsx ├── pages │ ├── 404 │ │ └── index.tsx │ ├── DemoHooks │ │ ├── components │ │ │ └── SmartRender.tsx │ │ └── index.tsx │ ├── DemoPage1 │ │ └── index.tsx │ ├── DemoPage2 │ │ └── index.tsx │ ├── Home │ │ ├── components │ │ │ └── content.tsx │ │ ├── index.tsx │ │ └── store │ │ │ └── index.ts │ └── Login │ │ ├── index.tsx │ │ └── style.less ├── public │ └── global.less ├── routes.ts └── store │ ├── app-store.ts │ ├── index.ts │ └── ui-store.ts ├── tsconfig.json ├── typings └── index.d.ts └── webpack ├── webpack.common.js ├── webpack.dev.js ├── webpack.dll.js └── webpack.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | // "useBuiltIns": "usage", 7 | // "corejs": 3, 8 | // "targets": "defaults", 9 | "modules": false 10 | } 11 | ], 12 | "@babel/preset-typescript", 13 | "@babel/preset-react" 14 | ], 15 | "plugins": [ 16 | "@babel/plugin-transform-runtime", 17 | ["@babel/plugin-proposal-decorators", {"legacy": true}], 18 | ["@babel/plugin-proposal-class-properties", { "loose": true }], 19 | ["@babel/plugin-proposal-private-methods", { "loose": true }], 20 | "@babel/plugin-proposal-object-rest-spread", 21 | "@babel/plugin-proposal-nullish-coalescing-operator", 22 | "@babel/plugin-proposal-optional-chaining", 23 | "@babel/plugin-syntax-dynamic-import", 24 | "react-hot-loader/babel", 25 | ["import", 26 | { 27 | "libraryName": "antd", 28 | "libraryDirectory": "es", 29 | "style": true 30 | } 31 | ] 32 | ] 33 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | npm-debug.log 4 | yarn-error.log 5 | yarn.lock 6 | package-lock.json 7 | 8 | # ide 9 | .idea 10 | 11 | # Mac General 12 | .DS_Store 13 | .AppleDouble 14 | .LSOverride -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 这是一个空的 react + antd 管理后台,只需要开发页面就能让它成为你想要的管理系统 2 | 3 | 线上地址:https://vibing.github.io/react-admin/index.html 4 | 5 | ![](https://tva1.sinaimg.cn/large/008i3skNly1gs9d35qq9dj313u0qvteg.jpg) 6 | ![](https://tva1.sinaimg.cn/large/008i3skNly1gs9d3p3ljhj313t0qt46x.jpg) 7 | 8 | 项目地址:https://github.com/Vibing/react-admin 欢迎 Star 和提供更好的建议 9 | 10 | ## 概述 11 | 12 | - 该管理后台基于 webpack5、 react@17、react-router@5.2、typescript、antd@4.10 13 | - 状态管理使用 [mobx@6.x](https://github.com/mobxjs/mobx),相比 redux 使用更简单,整个项目使用多 store 进行状态管理更容易维护 14 | - 支持页面刷新后菜单和面包屑自动聚焦 15 | - 使用 [dayjs](https://github.com/iamkun/dayjs) 代替 momentjs 16 | - 支持 Code Splitting(代码分割)、Optional Chaining(可选链)、Nullish Coalescing Operator(控制合并运算符) 17 | - 使用 [DLL](https://webpack.docschina.org/plugins/dll-plugin/) 提取公共库进行缓存 提高项目加载速度 18 | - 使用 [Tree Shaking](https://webpack.docschina.org/guides/tree-shaking/#root) 优化项目,打包时删除没用到的代码 19 | - antd 组件库按需引入 20 | 21 | ## 项目结构 22 | 23 | ``` 24 | . 25 | ├── dll // 生成的DLL 26 | ├── node_modules 27 | ├── src 28 | ├── components // 公用组件 29 | ├── pages // 用于存放所有页面 30 | ├── store // 顶级store,项目内任何地方都能访问 31 | ├── public 32 | ├── App.tsx // APP组件 33 | ├── routes.ts // 路由 34 | ├── index.tsx // 整个项目的入口 35 | └── index.ejs // 模板 36 | ├── tsconfig.json // typescript配置 37 | ├── typings // 自定义类型 38 | ├── webpack 39 | ├── package.json 40 | └── yarn.lock 41 | ``` 42 | 43 | ## 使用 44 | 45 | ```shell 46 | - git clone https://github.com/Vibing/react-admin.git 47 | - cd react-admin && yarn 48 | - yarn dll 49 | - yarn dev 50 | ``` 51 | 52 | 执行上面命令后 打开 http://localhost:3000/#/home 即可访问 53 | 54 | ## 描述 55 | 56 | ### 基于 mobx 的多 store 状态管理 57 | 58 | 项目提供两个顶级 store : ui-store 和 app-store 分别用于项目级别的UI状态控制和逻辑状态控制: 59 | 60 | ```tsx 61 | // store/index.ts 62 | import uiStore from './ui-store' 63 | import appStore from './app-store' 64 | 65 | export { uiStore, appStore } 66 | 67 | // index.tsx 68 | class Main extends Component { 69 | render() { 70 | return ( 71 | 76 | 77 | 78 | } 79 | > 80 | ) 81 | } 82 | } 83 | ``` 84 | 85 | 你也可有只使用一个顶级 store ,具体看你项目规划和大小 86 | 87 | 除了顶级 store 用于项目级的状态管理,为了更好的状态维护,我们给每个页面创建一个对应的 store,页面级的 store 里只维护当前页面的状态: 88 | 89 | ```typescript 90 | // pages/Home/store/index.ts 91 | import { action, configure, makeObservable, observable } from 'mobx' 92 | 93 | configure({ 94 | enforceActions: 'observed' 95 | }) 96 | 97 | export default class Store { 98 | constructor() { 99 | makeObservable(this, { 100 | count: observable, 101 | changeCount: action 102 | }) 103 | } 104 | 105 | count: number = 0 106 | 107 | changeCount = (count: number) => { 108 | this.count = count 109 | } 110 | } 111 | ``` 112 | 113 | 114 | 115 | ```tsx 116 | // pages/Home/index.tsx 117 | import React from 'react' 118 | import { Provider } from 'mobx-react' 119 | import Content from './components/content' 120 | import Store from './store' 121 | 122 | export default class Home extends React.Component { 123 | store: Store = new Store() 124 | 125 | render() { 126 | return } /> 127 | } 128 | } 129 | ``` 130 | 131 | 每个页面的 store 在页面被创建时创建,页面组件销毁时自动销毁,不会给内存压力 132 | 133 | 你可以启动项目,在 home 页面中点击按钮改变 store 的值体验一下 134 | 135 | ### 菜单数据 136 | 137 | 目前的菜单数据是模拟的,在 `src/components/Layout/_defaultRoutes.ts`中,实际开发时,这里的数据应该通过接口请求,然后渲染出来,你可以告知服务端小伙伴使用`_defaultRoutes.ts`里的数据格式 138 | 139 | ## 打包 140 | 141 | 在项目中运行`yarn build`就可以将项目打包到根目录的`dist`文件夹中,如果想将打包后的项目上传到阿里云OSS,我推荐你使用我编写的 webpack 插件:[webpack-oss-plugin](https://github.com/Vibing/webpack-oss-plugin) 它可以在打包后将打包产物上传到你配置的 OSS 中 142 | 143 | ## 其他 144 | 145 | ### webpack 5 新特性 146 | 147 | `webpack5` 相较于之前版本,主要以优化为主 148 | 149 | - 使用长期缓存,提升构建速度 150 | - 减少原先 bundle 内自动生成的冗余代码 151 | - NodeJS 的 polyfill 脚本被移除 152 | - 更好的 TreeShaking 153 | - Module Federation 让 Webpack 达到了线上 runtime 的效果,让代码直接在独立应用间利用 CDN 直接共享,可以说为微前端提供了一个新思路 154 | 155 | 关于 `webpack5` 新特性和 `Module Federation`可以看[这篇文章](https://blog.towavephone.com/webpack-v5-new-feature) 156 | 157 | 总之,随着 webpack 不断更新,它身后的团队肯定是不断对 webpack 进行优化的,我们尽量使用新版本(稳定版)来构建项目 158 | 159 | ### DLL 160 | 161 | 将不常改动的库或插件(react、react-dom、axios...)压缩在一个文件里,浏览器访问后缓存下来,以后再访问会快很多。 162 | 163 | ### 关于 @babel/preset-env 164 | 165 | 这里我没有使用`useBuiltIns`和`corejs`,因为我公司项目是内部使用,基本都是现代浏览器,不用考虑浏览器兼容问题,如果你需要考虑浏览器兼容 ES6 新语法,请使用`corejs`来作为 polyfill 166 | 167 | ### typescript 168 | 169 | 不需要使用`ts-loader`, `babel-loader`已经兼容 typescript 的编译,配合 @babel/preset-typescript 就能使用 typescript 开发。 170 | 171 | ### 图片、文字 172 | 173 | `webpack5` 不需要`url-loader` 等 loader 来解析图片和文字文件了,具体配置看代码吧 174 | 175 | ### resolve 176 | 177 | 解析范围尽量缩小 来减少 webpack 搜索范围,比如使用 exclude、include 178 | 179 | ### 多线程编译打包 180 | 181 | nodejs 可以多线程来充分使用 CPU,所以可以使用类似 `thread-loader`的库或插件来进行优化,提高构建效率 182 | 183 | ### 其他 184 | 185 | 有些没说,具体看代码吧,有更好的优化请告知我,谢谢 186 | 187 | 188 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /dll/vendor-manifest.json: -------------------------------------------------------------------------------- 1 | {"name":"vendor_library","content":{"./node_modules/mobx-react/dist/mobxreact.esm.js":{"id":150,"buildMeta":{"exportsType":"namespace"},"exports":["MobXProviderContext","Observer","PropTypes","Provider","disposeOnUnmount","enableStaticRendering","inject","isUsingStaticRendering","observer","observerBatching","useAsObservableSource","useLocalObservable","useLocalStore","useObserver","useStaticRendering"]},"./node_modules/react/index.js":{"id":294,"buildMeta":{"exportsType":"dynamic","defaultObject":"redirect"},"exports":["Children","Component","Fragment","Profiler","PureComponent","StrictMode","Suspense","__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED","cloneElement","createContext","createElement","createFactory","createRef","forwardRef","isValidElement","lazy","memo","useCallback","useContext","useDebugValue","useEffect","useImperativeHandle","useLayoutEffect","useMemo","useReducer","useRef","useState","version"]},"./node_modules/react-router-dom/esm/react-router-dom.js":{"id":427,"buildMeta":{"exportsType":"namespace"},"exports":["BrowserRouter","HashRouter","Link","MemoryRouter","NavLink","Prompt","Redirect","Route","Router","StaticRouter","Switch","generatePath","matchPath","useHistory","useLocation","useParams","useRouteMatch","withRouter"]},"./node_modules/axios/index.js":{"id":669,"buildMeta":{"exportsType":"dynamic","defaultObject":"redirect"}},"./node_modules/react-dom/index.js":{"id":935,"buildMeta":{"exportsType":"dynamic","defaultObject":"redirect"},"exports":["__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED","createPortal","findDOMNode","flushSync","hydrate","render","unmountComponentAtNode","unstable_batchedUpdates","unstable_createPortal","unstable_renderSubtreeIntoContainer","version"]},"./node_modules/mobx/dist/mobx.esm.js":{"id":949,"buildMeta":{"exportsType":"namespace"},"exports":["$mobx","FlowCancellationError","ObservableMap","ObservableSet","Reaction","_allowStateChanges","_allowStateChangesInsideComputed","_allowStateReadsEnd","_allowStateReadsStart","_autoAction","_endAction","_getAdministration","_getGlobalState","_interceptReads","_isComputingDerivation","_resetGlobalState","_startAction","action","autorun","comparer","computed","configure","createAtom","defineProperty","entries","extendObservable","flow","flowResult","get","getAtom","getDebugName","getDependencyTree","getObserverTree","has","intercept","isAction","isBoxedObservable","isComputed","isComputedProp","isFlow","isFlowCancellationError","isObservable","isObservableArray","isObservableMap","isObservableObject","isObservableProp","isObservableSet","keys","makeAutoObservable","makeObservable","observable","observe","onBecomeObserved","onBecomeUnobserved","onReactionError","override","ownKeys","reaction","remove","runInAction","set","spy","toJS","trace","transaction","untracked","values","when"]}}} -------------------------------------------------------------------------------- /dll/vendor.dll.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /** @license React v0.20.2 8 | * scheduler.production.min.js 9 | * 10 | * Copyright (c) Facebook, Inc. and its affiliates. 11 | * 12 | * This source code is licensed under the MIT license found in the 13 | * LICENSE file in the root directory of this source tree. 14 | */ 15 | 16 | /** @license React v16.13.1 17 | * react-is.production.min.js 18 | * 19 | * Copyright (c) Facebook, Inc. and its affiliates. 20 | * 21 | * This source code is licensed under the MIT license found in the 22 | * LICENSE file in the root directory of this source tree. 23 | */ 24 | 25 | /** @license React v17.0.2 26 | * react-dom.production.min.js 27 | * 28 | * Copyright (c) Facebook, Inc. and its affiliates. 29 | * 30 | * This source code is licensed under the MIT license found in the 31 | * LICENSE file in the root directory of this source tree. 32 | */ 33 | 34 | /** @license React v17.0.2 35 | * react.production.min.js 36 | * 37 | * Copyright (c) Facebook, Inc. and its affiliates. 38 | * 39 | * This source code is licensed under the MIT license found in the 40 | * LICENSE file in the root directory of this source tree. 41 | */ 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack5-react-demo", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "sideEffects": [ 6 | "*.css", 7 | "*.less" 8 | ], 9 | "scripts": { 10 | "dev": "webpack serve --config webpack/webpack.dev.js --progress --mode development", 11 | "build": "rm -rf dist && webpack --config webpack/webpack.prod.js --progress --color", 12 | "dll": "rm -rf dll && webpack --config webpack/webpack.dll.js" 13 | }, 14 | "repository": "https://github.com/Vibing/webpack5-react-demo.git", 15 | "author": "chenlong <903245852@qq.com>", 16 | "devDependencies": { 17 | "@babel/core": "^7.12.10", 18 | "@babel/plugin-proposal-class-properties": "^7.12.1", 19 | "@babel/plugin-proposal-decorators": "^7.12.12", 20 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", 21 | "@babel/plugin-proposal-object-rest-spread": "^7.12.1", 22 | "@babel/plugin-proposal-optional-chaining": "^7.12.7", 23 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 24 | "@babel/plugin-transform-runtime": "^7.12.10", 25 | "@babel/preset-env": "^7.12.11", 26 | "@babel/preset-react": "^7.12.10", 27 | "@babel/preset-typescript": "^7.12.7", 28 | "@babel/runtime": "^7.12.5", 29 | "add-asset-html-webpack-plugin": "^3.1.3", 30 | "antd-dayjs-webpack-plugin": "^1.0.6", 31 | "babel-loader": "^8.2.2", 32 | "babel-plugin-import": "^1.13.3", 33 | "clean-webpack-plugin": "^3.0.0", 34 | "core-js": "^3.8.2", 35 | "css-loader": "^5.0.1", 36 | "html-webpack-dll-file-plugin": "^1.0.4", 37 | "html-webpack-plugin": "^5.0.0-beta.5", 38 | "html-webpack-tags-plugin": "^2.0.17", 39 | "less": "^4.1.0", 40 | "less-loader": "^7.2.1", 41 | "less-plugin-clean-css": "^1.5.1", 42 | "mini-css-extract-plugin": "^1.3.4", 43 | "optimize-css-assets-webpack-plugin": "^5.0.4", 44 | "react-hot-loader": "^4.13.0", 45 | "style-loader": "^2.0.0", 46 | "terser-webpack-plugin": "^5.1.1", 47 | "thread-loader": "^3.0.1", 48 | "typescript": "^4.1.3", 49 | "webpack": "^5.14.0", 50 | "webpack-cli": "^4.3.1", 51 | "webpack-dev-server": "^3.11.2", 52 | "webpack-merge": "^5.7.3", 53 | "webpack-oss-upload-plugin": "^1.1.2", 54 | "webpackbar": "^5.0.0-3" 55 | }, 56 | "license": "MIT", 57 | "dependencies": { 58 | "@ant-design/icons": "^4.4.0", 59 | "@loadable/component": "^5.14.1", 60 | "antd": "^4.10.2", 61 | "axios": "^0.21.1", 62 | "dayjs": "^1.10.3", 63 | "mobx": "^6.1.4", 64 | "mobx-react": "^7.1.0", 65 | "nanoid": "^3.1.23", 66 | "react": "^17.0.1", 67 | "react-dom": "^17.0.1", 68 | "react-router-dom": "^5.2.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import './public/global.less' 2 | import loadable from '@loadable/component' 3 | import { HashRouter, Switch, Route } from 'react-router-dom' 4 | import routes from './routes' 5 | 6 | // 组件按需加载(Code Spliting) 7 | const Layout = loadable(() => import('./components/Layout/Layout')) 8 | 9 | const Login = loadable(() => import('./pages/Login')) 10 | 11 | import React, { Component } from 'react' 12 | import { inject, observer } from 'mobx-react' 13 | 14 | @inject('uiStore') 15 | @observer 16 | export default class App extends Component { 17 | render() { 18 | return ( 19 | 20 | 21 | } /> 22 | } 25 | /> 26 | 27 | 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Iconfont/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createFromIconfontCN } from '@ant-design/icons' 3 | import { ICON_FONT_URL } from '@/config' 4 | console.log(ICON_FONT_URL) 5 | 6 | const IconFont = createFromIconfontCN({ 7 | scriptUrl: ICON_FONT_URL 8 | }) 9 | 10 | export default props => 11 | -------------------------------------------------------------------------------- /src/components/Layout/Breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Breadcrumb } from 'antd' 3 | import { Link, withRouter } from 'react-router-dom' 4 | import { nanoid } from 'nanoid' 5 | import menuData from './_defaultRoutes' 6 | 7 | @withRouter 8 | export default class LayoutBreadcrumb extends Component { 9 | unHistoryListener: any 10 | 11 | state = { 12 | breadcrumbs: [] 13 | } 14 | 15 | componentDidMount() { 16 | const { history } = this.props 17 | const breadcrumbs = this.setBreadcrumb() 18 | 19 | this.setState({ 20 | breadcrumbs 21 | }) 22 | this.unHistoryListener = history.listen((i, a) => { 23 | const breadcrumbs = this.setBreadcrumb() 24 | 25 | this.setState({ 26 | breadcrumbs 27 | }) 28 | }) 29 | } 30 | 31 | componentWillUnmount() { 32 | this.unHistoryListener() 33 | } 34 | 35 | render() { 36 | const { breadcrumbs } = this.state 37 | 38 | return ( 39 | 40 | {breadcrumbs.map(b => ( 41 | 42 | {b?.children?.length ? ( 43 | b.name 44 | ) : ( 45 | 46 | {b.name} 47 | 48 | )} 49 | 50 | ))} 51 | 52 | ) 53 | } 54 | 55 | setBreadcrumb = () => { 56 | const { location } = this.props.history 57 | const pathArr = location.pathname.split('/') 58 | const breadcrumbArr = [] 59 | 60 | if (pathArr.length > 1) { 61 | menuData.routes.forEach(route => { 62 | if (location.pathname.startsWith(route.path)) { 63 | breadcrumbArr.push(route) 64 | if (route?.children?.length) { 65 | route.children.forEach(cRoute => { 66 | if (location.pathname.startsWith(cRoute.path)) { 67 | breadcrumbArr.push(cRoute) 68 | } 69 | }) 70 | } 71 | } 72 | }) 73 | } 74 | return breadcrumbArr 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/components/Layout/Content.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Layout } from 'antd' 3 | import { Redirect, Route, Switch } from 'react-router-dom' 4 | import loadable from '@loadable/component' 5 | import routes from '@/routes' 6 | 7 | const { Content } = Layout 8 | 9 | const CannotFind = loadable(() => import('../../pages/404')) 10 | 11 | export default class LayoutContent extends Component { 12 | render() { 13 | return ( 14 | 15 |
19 | 20 | {this.renderRoutes()} 21 | 22 | } /> 23 | 24 |
25 |
26 | ) 27 | } 28 | 29 | renderRoutes = () => { 30 | return (routes || []).map((route, idx) => { 31 | if (route.component) { 32 | return ( 33 | { 39 | const CurrComponent = loadable(route.component) 40 | return 41 | }} 42 | /> 43 | ) 44 | } 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Layout/Header.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Layout, Avatar, Menu, Dropdown } from 'antd' 3 | import { inject, observer } from 'mobx-react' 4 | import { withRouter } from 'react-router-dom' 5 | import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons' 6 | import Iconfont from '@/components/Iconfont' 7 | 8 | const { Header } = Layout 9 | 10 | @withRouter 11 | @inject('uiStore') 12 | @observer 13 | export default class LayoutHeader extends Component { 14 | props: any 15 | 16 | render() { 17 | return ( 18 |
19 | {this.renderCollap()} 20 | 21 |
22 | 26 | Feiliang 27 |
28 |
29 |
30 | ) 31 | } 32 | 33 | toggleCollapsed = () => { 34 | const { uiStore } = this.props 35 | uiStore.changeCollapsed(!uiStore.collapsed) 36 | } 37 | 38 | renderCollap = () => { 39 | const { collapsed } = this.props.uiStore 40 | 41 | return collapsed ? ( 42 | 43 | ) : ( 44 | 45 | ) 46 | } 47 | 48 | overlayContent = () => { 49 | return ( 50 | 55 |

56 | 账户资金: 57 | 87622.22 58 |

59 | } 62 | > 63 | 修改密码 64 | 65 | } 68 | > 69 | 退出登录 70 | 71 |
72 | ) 73 | } 74 | 75 | handleClick = e => { 76 | console.log('click ', e) 77 | if (e.key == 'logout') { 78 | this.props.history.push('/login') 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/components/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import './sty.less' 2 | import React, { Component } from 'react' 3 | import { Layout } from 'antd' 4 | import { withRouter, Route } from 'react-router-dom' 5 | import loadable from '@loadable/component' 6 | import SlideMenu from './SlideMenu' 7 | import LayoutHeader from './Header' 8 | import LayoutContent from './Content' 9 | import Breadcrumb from './Breadcrumb' 10 | 11 | @withRouter 12 | export default class BaseLayout extends Component { 13 | props: { route: any; iconfontUrl: string; children: any; history: any } 14 | state: { pathname: string } 15 | 16 | constructor(props) { 17 | super(props) 18 | this.state = { 19 | pathname: '/home', 20 | } 21 | } 22 | 23 | render() { 24 | const { pathname } = this.state 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ) 36 | } 37 | 38 | renderRoutes = () => { 39 | return (routes || []).map((route, idx) => { 40 | if (route.component) { 41 | return ( 42 | { 48 | const CurrComponent = loadable(route.component) 49 | return 50 | }} 51 | /> 52 | ) 53 | } 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Layout/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { inject, observer } from 'mobx-react' 2 | import React, { Component } from 'react' 3 | 4 | @inject('uiStore') 5 | @observer 6 | export default class Logo extends Component { 7 | render() { 8 | const { collapsed } = this.props.uiStore 9 | 10 | return ( 11 |
12 | 13 | 14 | {!collapsed ? ( 15 |

22 | Ant Design 23 |

24 | ) : null} 25 |
26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Layout/SlideMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import { Layout, Menu } from 'antd' 3 | import { inject, observer } from 'mobx-react' 4 | import { withRouter } from 'react-router-dom' 5 | import Iconfont from '@/components/Iconfont' 6 | import Logo from './Logo' 7 | 8 | import menusData from './_defaultRoutes' 9 | 10 | const { Sider } = Layout 11 | const { SubMenu } = Menu 12 | @withRouter 13 | @inject('uiStore') 14 | @observer 15 | export default class SlideMenu extends Component { 16 | props: any 17 | state: any 18 | 19 | constructor(props) { 20 | super(props) 21 | 22 | this.state = { 23 | openKeys: [] 24 | } 25 | } 26 | 27 | componentDidMount() { 28 | // 设置openKeys 29 | this.handleOpenKeys() 30 | } 31 | 32 | render() { 33 | const { collapsed } = this.props.uiStore 34 | 35 | return ( 36 | 43 | 44 | {this.renderMenus()} 45 | 46 | ) 47 | } 48 | 49 | renderMenus = () => { 50 | if (!menusData.routes || !menusData.routes.length) { 51 | return null 52 | } 53 | 54 | const { openKeys } = this.state 55 | const { history } = this.props 56 | const { routes } = menusData 57 | 58 | return ( 59 | 67 | {this.renderMenuItems(routes)} 68 | 69 | ) 70 | } 71 | 72 | renderMenuItems = routes => { 73 | const renderMenu = ({ path, icon, name, children }) => { 74 | const itemProps = { 75 | key: path, 76 | icon: icon ? : null 77 | } 78 | if (!children?.length) { 79 | return {name} 80 | } 81 | return ( 82 | 83 | {this.renderMenuItems(children)} 84 | 85 | ) 86 | } 87 | 88 | return {routes.map(route => renderMenu(route))} 89 | } 90 | 91 | handleMenuSelect = ({ item, key, keyPath, selectedKeys, domEvent }) => { 92 | const { history } = this.props 93 | key !== history.location.pathname && history.push(key) 94 | } 95 | 96 | handleOpenKeys = () => { 97 | const { location } = this.props.history 98 | const pathArr = location.pathname.split('/') 99 | 100 | switch (pathArr.length) { 101 | case 2: 102 | this.setState({ 103 | openKeys: [] 104 | }) 105 | break 106 | case 3: 107 | this.setState({ 108 | openKeys: [pathArr.slice(0, 2).join('/')] 109 | }) 110 | break 111 | } 112 | } 113 | 114 | onOpenChange = (openKeys: string[]) => { 115 | this.setState({ 116 | openKeys 117 | }) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/components/Layout/_defaultRoutes.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | routes: [ 3 | { 4 | path: '/home', 5 | name: '首页', 6 | icon: 'icon-16', 7 | children: [] 8 | }, 9 | { 10 | path: '/article', 11 | name: '文章管理', 12 | icon: 'icon-lianxi2hebing_shipin', 13 | children: [ 14 | { 15 | path: '/article/demopage1', 16 | name: 'demoPage1', 17 | icon: null 18 | } 19 | ] 20 | }, 21 | { 22 | path: '/set', 23 | name: '视频管理', 24 | icon: 'icon-lianxi2hebing_shipin', 25 | children: [ 26 | { 27 | path: '/set/demopage2', 28 | name: 'demoPage2', 29 | icon: null 30 | }, 31 | { 32 | path: '/set/demohooks', 33 | name: 'demohooks', 34 | icon: null 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Layout/sty.less: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | } 6 | .app-layout { 7 | height: 100%; 8 | } 9 | 10 | .app-container { 11 | margin: 0 16px 0; 12 | overflow: initial; 13 | background: #fff; 14 | } 15 | .app-logo { 16 | display: flex; 17 | height: 32px; 18 | margin: 16px; 19 | justify-content: center; 20 | align-items: center; 21 | img { 22 | width: 40px; 23 | height: 40px; 24 | border-radius: 50%; 25 | } 26 | } 27 | 28 | .app-iconfont { 29 | font-size: 18px; 30 | margin-right: 5px; 31 | } 32 | .app-customer-info { 33 | font-size: 13px; 34 | justify-content: center; 35 | padding: 5px 0; 36 | border-bottom: 1px solid #f0f0f0; 37 | } 38 | 39 | .site-layout .app-header { 40 | display: flex; 41 | position: relative; 42 | align-items: center; 43 | background: #fff; 44 | padding: 0 24px; 45 | box-shadow: 0 1px 4px rgb(0 21 41 / 8%); 46 | .app-trigger { 47 | font-size: 18px; 48 | line-height: 64px; 49 | cursor: pointer; 50 | transition: color 0.3s; 51 | } 52 | .app-trigger:hover { 53 | color: #1890ff; 54 | } 55 | .app-avatar { 56 | cursor: pointer; 57 | position: absolute; 58 | right: 20px; 59 | display: flex; 60 | align-items: center; 61 | height: 46px; 62 | padding: 0 15px 0 3px; 63 | border-radius: 32px; 64 | transition: 0.3s; 65 | &:hover { 66 | background: #f0f2f5; 67 | } 68 | } 69 | } 70 | .app-header-popover { 71 | p { 72 | display: flex; 73 | align-items: center; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | // iconfont remote url 2 | export const ICON_FONT_URL = '//at.alicdn.com/t/font_2654948_4te3l9opof6.js' 3 | -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= title %> 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { hot } from 'react-hot-loader/root' 2 | import React, { Component } from 'react' 3 | import ReactDOM from 'react-dom' 4 | import dayjs from 'dayjs' 5 | import 'dayjs/locale/zh-cn' 6 | import zhCN from 'antd/lib/locale/zh_CN' 7 | import { ConfigProvider } from 'antd' 8 | import loadable from '@loadable/component' 9 | import { Provider } from 'mobx-react' 10 | import { appStore, uiStore } from './store' 11 | 12 | dayjs.locale('zh-cn') 13 | 14 | const BaseLayout = loadable(() => import('./App')) 15 | 16 | class Main extends Component { 17 | render() { 18 | return ( 19 | 24 | 25 | 26 | } 27 | > 28 | ) 29 | } 30 | } 31 | 32 | // const App = hot(Main) 33 | 34 | ReactDOM.render(
, document.querySelector('#root')) 35 | -------------------------------------------------------------------------------- /src/pages/404/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Result, Button } from 'antd' 3 | import { withRouter } from 'react-router-dom' 4 | 5 | @withRouter 6 | export default class FourZeroFour extends Component { 7 | render() { 8 | const { history } = this.props 9 | return ( 10 | history.push('/')}> 16 | 返回首页 17 | 18 | } 19 | /> 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/DemoHooks/components/SmartRender.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, ReactElement } from 'react' 2 | import { observer } from 'mobx-react' 3 | 4 | let num = 0 5 | 6 | export default observer(() => { 7 | num += 1 8 | 9 | console.log('rerender-->', num) 10 | 11 | return <>{num} 12 | }) 13 | -------------------------------------------------------------------------------- /src/pages/DemoHooks/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useCallback } from 'react' 2 | import { observer } from 'mobx-react' 3 | import ChildrenComponent from './components/SmartRender' 4 | 5 | export default observer(() => { 6 | const [count, setCount] = useState(0) 7 | useEffect(() => {}, []) 8 | 9 | const HanldeCount = useCallback(() => { 10 | setCount(count + 1) 11 | }, [count]) 12 | 13 | return ( 14 | <> 15 |

{count}

16 | 17 |
18 | children: 19 |
20 | 21 | ) 22 | }) 23 | -------------------------------------------------------------------------------- /src/pages/DemoPage1/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default () =>
DemoPage1
4 | -------------------------------------------------------------------------------- /src/pages/DemoPage2/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default () =>
DemoPage2
4 | -------------------------------------------------------------------------------- /src/pages/Home/components/content.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { observer, inject } from 'mobx-react' 3 | import { Button } from 'antd' 4 | import Store from '../store' 5 | 6 | @inject('store') 7 | @observer 8 | export default class Content extends Component { 9 | store: Store = this.props.store 10 | 11 | render() { 12 | const { count } = this.store 13 | return ( 14 |
15 | Home Page 16 |
17 | 20 |
21 | ) 22 | } 23 | 24 | handleChange = () => { 25 | const { count } = this.store 26 | this.store.changeCount(count + 1) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Provider } from 'mobx-react' 3 | import Content from './components/content' 4 | import Store from './store' 5 | 6 | export default class Home extends React.Component { 7 | store: Store = new Store() 8 | 9 | render() { 10 | return } /> 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/Home/store/index.ts: -------------------------------------------------------------------------------- 1 | import { action, configure, makeObservable, observable } from 'mobx' 2 | 3 | configure({ 4 | enforceActions: 'observed' 5 | }) 6 | 7 | export default class Store { 8 | constructor() { 9 | makeObservable(this, { 10 | count: observable, 11 | changeCount: action 12 | }) 13 | } 14 | 15 | count: number = 0 16 | 17 | changeCount = (count: number) => { 18 | this.count = count 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/pages/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import './style.less' 2 | import React, { Component } from 'react' 3 | import { Form, Input, Checkbox, Button } from 'antd' 4 | import { withRouter } from 'react-router-dom' 5 | 6 | @withRouter 7 | export default class Login extends Component { 8 | render() { 9 | return ( 10 |
11 |
12 |

Webpack5 + React + antd4 后台管理系统

13 |

14 | 空空如也的后台管理系统,你只需在里面添加页面即可 15 |

16 |
17 | 18 |
19 |
24 | 28 | 29 | 30 | 31 | 35 | 40 | 41 | 42 | 43 | 记住我 44 | 45 | 46 | 47 | 55 | 56 |
57 |
58 |
59 | ) 60 | } 61 | 62 | onFinish = (values: any) => { 63 | console.log('Success:', values) 64 | const { history } = this.props 65 | history.push('/') 66 | } 67 | 68 | onFinishFailed = (errorInfo: any) => { 69 | console.log('Failed:', errorInfo) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/pages/Login/style.less: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | background-color: #f0f2f5; 5 | } 6 | 7 | .login-page { 8 | width: 100%; 9 | min-height: 100%; 10 | background: #f0f2f5 11 | url('http://e-static.oss-cn-shanghai.aliyuncs.com/img/background.svg') 12 | no-repeat 50%; 13 | background-size: 100% 100%; 14 | padding: 110px 0 144px; 15 | position: relative; 16 | top: 110px; 17 | .top { 18 | text-align: center; 19 | .title { 20 | font-size: 30px; 21 | font-weight: 400; 22 | } 23 | .desc { 24 | font-size: 14px; 25 | color: rgba(0, 0, 0, 0.45); 26 | margin-top: 12px; 27 | margin-bottom: 40px; 28 | } 29 | } 30 | 31 | .login-form-wrap { 32 | width: 400px; 33 | margin: 0 auto; 34 | input { 35 | background: #fff; //背景透明 36 | -webkit-tap-highlight-color: #fff; 37 | box-shadow: inset 0 0 0 1000px #fff !important; 38 | } 39 | 40 | .submitbtn { 41 | width: 100%; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/public/global.less: -------------------------------------------------------------------------------- 1 | #root { 2 | .icon { 3 | width: 1em; 4 | height: 1em; 5 | vertical-align: -0.15em; 6 | fill: currentColor; 7 | overflow: hidden; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/routes.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/home', 4 | name: '主页', 5 | exact: true, 6 | component: () => import('./pages/Home') 7 | }, 8 | { 9 | path: '/article/demopage1', 10 | name: 'demoPage1', 11 | exact: true, 12 | component: () => import('./pages/DemoPage1') 13 | }, 14 | { 15 | path: '/set/demopage2', 16 | name: 'demoPage2', 17 | exact: true, 18 | component: () => import('./pages/DemoPage2') 19 | }, 20 | { 21 | path: '/set/demohooks', 22 | name: 'demohooks', 23 | exact: true, 24 | component: () => import('./pages/DemoHooks') 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /src/store/app-store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | observable, 3 | configure, 4 | action, 5 | makeObservable, 6 | runInAction 7 | } from 'mobx' 8 | 9 | configure({ 10 | enforceActions: 'observed' 11 | }) 12 | 13 | class GlobalAPPStore { 14 | constructor() { 15 | makeObservable(this, { 16 | user: observable, 17 | changeUser: action 18 | }) 19 | } 20 | 21 | user = null 22 | 23 | changeUser(user) { 24 | this.user = user 25 | } 26 | } 27 | 28 | export default new GlobalAPPStore() 29 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import uiStore from './ui-store' 2 | import appStore from './app-store' 3 | 4 | export { uiStore, appStore } 5 | -------------------------------------------------------------------------------- /src/store/ui-store.ts: -------------------------------------------------------------------------------- 1 | import { observable, configure, action, makeObservable } from 'mobx' 2 | 3 | configure({ 4 | enforceActions: 'observed' 5 | }) 6 | 7 | class GlobalUIStore { 8 | constructor() { 9 | makeObservable(this, { 10 | collapsed: observable, 11 | changeCollapsed: action 12 | }) 13 | } 14 | 15 | collapsed = false 16 | 17 | changeCollapsed(collapsed: boolean) { 18 | this.collapsed = collapsed 19 | } 20 | } 21 | 22 | export default new GlobalUIStore() 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "react", 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "experimentalDecorators": true, 11 | "outDir": "lib", 12 | "allowSyntheticDefaultImports": true, 13 | "allowJs": true 14 | }, 15 | "include": ["src","typings"] 16 | } 17 | -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | // support NodeJS modules without type definitions 2 | /* eslint-disable */ 3 | declare module '*'; 4 | 5 | declare var ENV: string; 6 | declare var HMR: boolean; 7 | declare var System: SystemJS; 8 | 9 | interface SystemJS { 10 | import: (path?: string) => Promise; 11 | } 12 | 13 | interface GlobalEnvironment { 14 | ENV: string; 15 | HMR: boolean; 16 | SystemJS: SystemJS; 17 | System: SystemJS; 18 | } 19 | 20 | // Extend typings 21 | interface Global extends GlobalEnvironment { } -------------------------------------------------------------------------------- /webpack/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 3 | const WebpackBar = require('webpackbar') 4 | const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin') 5 | const HtmlWebpackPlugin = require('html-webpack-plugin') 6 | const webpack = require('webpack') 7 | 8 | module.exports = { 9 | entry: { 10 | app: path.resolve(__dirname, '../src/index.tsx') 11 | }, 12 | output: { 13 | path: path.resolve(__dirname, '../dist') 14 | }, 15 | resolve: { 16 | enforceExtension: false, 17 | extensions: ['.tsx', '.ts', '.js', '.less'], 18 | symlinks: false, 19 | alias: { 20 | '@': path.resolve(__dirname, '../src') 21 | } 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.(ts|js)x?$/, 27 | exclude: /node_modules/, 28 | use: [ 29 | 'thread-loader', 30 | { 31 | loader: 'babel-loader', 32 | options: { 33 | cacheDirectory: true, 34 | include: path.resolve(__dirname, '../src') 35 | } 36 | } 37 | ] 38 | }, 39 | { 40 | test: /\.(?:ico|gif|png|jpg|jpeg)$/i, 41 | type: 'asset/resource' 42 | }, 43 | { 44 | test: /\.(woff(2)?|eot|ttf|otf|svg|)$/, 45 | type: 'asset/inline' 46 | } 47 | ] 48 | }, 49 | plugins: [ 50 | new CleanWebpackPlugin(), 51 | new AntdDayjsWebpackPlugin(), 52 | new webpack.DllReferencePlugin({ 53 | manifest: require('../dll/vendor-manifest.json'), 54 | context: path.resolve(__dirname, '..') 55 | }), 56 | new HtmlWebpackPlugin({ 57 | template: path.resolve(__dirname, '../src/index.ejs'), 58 | templateParameters: { 59 | title: 'react-admin' 60 | } 61 | }), 62 | new WebpackBar() 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | const { merge } = require('webpack-merge') 4 | const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin') 5 | 6 | const commonConfig = require('./webpack.common') 7 | 8 | module.exports = merge(commonConfig, { 9 | mode: 'development', 10 | devtool: 'eval-cheap-module-source-map', 11 | output: { 12 | filename: '[name].js', 13 | pathinfo: false, 14 | publicPath: '/' 15 | }, 16 | cache: { 17 | type: 'memory' 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.css$/, 23 | use: ['style-loader', 'css-loader'] 24 | }, 25 | { 26 | test: /\.less$/, 27 | use: [ 28 | { 29 | loader: 'style-loader' 30 | }, 31 | { 32 | loader: 'css-loader' 33 | }, 34 | { 35 | loader: 'less-loader', 36 | options: { 37 | lessOptions: { 38 | paths: [ 39 | path.resolve(__dirname, '../src'), 40 | path.resolve(__dirname, '../node_modules/antd') 41 | ], 42 | javascriptEnabled: true 43 | } 44 | } 45 | } 46 | ] 47 | } 48 | ] 49 | }, 50 | optimization: { 51 | runtimeChunk: true, 52 | removeAvailableModules: false, 53 | removeEmptyChunks: false, 54 | splitChunks: false, 55 | usedExports: true 56 | }, 57 | 58 | plugins: [ 59 | new webpack.HotModuleReplacementPlugin(), 60 | new AddAssetHtmlPlugin([ 61 | { 62 | filepath: require.resolve('../dll/vendor.dll.js'), 63 | includeRelatedFiles: false, 64 | publicPath: '/' 65 | } 66 | ]) 67 | ], 68 | 69 | devServer: { 70 | historyApiFallback: true, 71 | contentBase: path.resolve(__dirname, '../dist'), 72 | open: false, 73 | hot: true, 74 | quiet: true, 75 | port: 3000 76 | } 77 | }) 78 | -------------------------------------------------------------------------------- /webpack/webpack.dll.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | module.exports = { 5 | mode: 'production', 6 | entry: { 7 | vendor: [ 8 | 'react', 9 | 'react-dom', 10 | 'react-router-dom', 11 | 'mobx', 12 | 'mobx-react', 13 | 'axios' 14 | ] 15 | }, 16 | output: { 17 | path: path.resolve(__dirname, '../dll'), 18 | filename: '[name].dll.js', 19 | library: '[name]_library' 20 | }, 21 | 22 | plugins: [ 23 | new webpack.DllPlugin({ 24 | context: path.resolve(__dirname, '..'), 25 | path: path.join(__dirname, '../dll', '[name]-manifest.json'), 26 | name: '[name]_library' 27 | }) 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /webpack/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 4 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin') 5 | const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin') 6 | const TerserPlugin = require('terser-webpack-plugin') 7 | const CleanCSSPlugin = require('less-plugin-clean-css') 8 | const { merge } = require('webpack-merge') 9 | const commonConfig = require('./webpack.common') 10 | 11 | module.exports = merge(commonConfig, { 12 | mode: 'production', 13 | output: { 14 | filename: '[name].[contenthash].js', 15 | publicPath: '' 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.css$/, 21 | use: [MiniCssExtractPlugin.loader, 'css-loader'] 22 | }, 23 | { 24 | test: /\.less$/, 25 | use: [ 26 | { 27 | loader: MiniCssExtractPlugin.loader 28 | }, 29 | { 30 | loader: 'css-loader' 31 | }, 32 | { 33 | loader: 'less-loader', 34 | options: { 35 | lessOptions: { 36 | paths: [ 37 | path.resolve(__dirname, '../src'), 38 | path.resolve(__dirname, '../node_modules/antd') 39 | ], 40 | plugins: [new CleanCSSPlugin({ advanced: true })], 41 | javascriptEnabled: true 42 | } 43 | } 44 | } 45 | ] 46 | } 47 | ] 48 | }, 49 | optimization: { 50 | minimize: true, 51 | minimizer: [ 52 | new TerserPlugin({ 53 | exclude: /node_modules/, 54 | parallel: true, 55 | extractComments: true 56 | }) 57 | ], 58 | splitChunks: { 59 | cacheGroups: { 60 | styles: { 61 | name: 'styles', 62 | test: /\.css$/, 63 | chunks: 'all', 64 | enforce: true 65 | } 66 | } 67 | } 68 | }, 69 | plugins: [ 70 | new CleanWebpackPlugin(), 71 | new MiniCssExtractPlugin({ 72 | filename: '[name].[contenthash].css', 73 | chunkFilename: '[id].[contenthash].css', 74 | ignoreOrder: false 75 | }), 76 | new OptimizeCssAssetsPlugin({ 77 | cssProcessor: require('cssnano'), 78 | cssProcessorPluginOptions: { 79 | preset: [ 80 | 'default', 81 | { 82 | discardComments: { 83 | removeAll: true 84 | } 85 | } 86 | ] 87 | }, 88 | canPrint: true 89 | }), 90 | new AddAssetHtmlPlugin([ 91 | { 92 | filepath: require.resolve('../dll/vendor.dll.js'), 93 | includeRelatedFiles: false, 94 | publicPath: '' 95 | } 96 | ]) 97 | ] 98 | }) 99 | --------------------------------------------------------------------------------