本篇文章讲述从零搭建React中后台项目框架模板,方便快速进行具体项目开发。包括Webpack4.0配置及打包优化、React全家桶使用(React + React-router + Axios + Mobx + Antd)、ESLint等项目开发规范等。javascript
涉及的技术栈均采用当前最新版本的语法:css
create-react-app
、umi
等脚手架);项目页面截图:
html
通常React开发,可使用Facebook提供的 create-react-app
来建立。create-react-app
足够简单易用,从学习 React 的角度来看很是合适。但严格说来,若是要开发一款大型的应用,须要作到更精细、更灵活的配置,只用 create-react-app
并不合适,有规模点的公司都会考虑搭建本身公司级的脚手架工具和框架模板。而基础就是基于webpack从零精细化构建。java
企业级React开发也能够采用蚂蚁金服的Umi(一个可插拔的企业级 react 应用框架),可使用相关的全套全家桶连贯快捷开发,优势还在于大厂出品,经历众多大项目的考验,稳定性和可维护性获得极大的保障。可是增长了很多学习成本,项目的粒度可控性不高,也比较受限。
在构建公司级全套项目架构上能够从Umi、[Ant Design Pro](https://pro.ant.design/)
等上获取很多极有价值的参考。node
本项目从零搭建React应用模板,一来方便本身快速构建实际应用;二来重点在于梳理各技术栈最新知识点。但愿也对看到的人有所帮助。react
本项目为React中后台项目框架模板,方便快速进行具体项目开发。包括Webpack4.0配置及打包优化、React全家桶使用(React + React-router + Axios + Mobx + Antd)、ESLint等项目开发规范等。webpack
项目Git地址:github.com/now1then/re…;
文章连接-语雀:www.yuque.com/nowthen/lon…;
在线演示地址:ios
目录结构:git
├── build // webpack配置
│ ├── webpack.common.js // webpack通用配置
│ ├── webpack.dev.js // webpack开发环境配置
│ └── webpack.prod.js // webpack生产环境配置
├── dist // 打包输出目录
├── public // 项目公开目录
├── src // src开发目录
│ ├── assets // 静态资源
│ ├── components // 公共组件
│ ├── layouts // 页面布局组件
│ ├── modules // 公共业务模块
│ ├── pages // 具体业务页面
│ ├── routers // 项目路由配置
│ ├── services // axios服务等相关
│ ├── stores // 全局公共 mobx store
│ ├── styles // 存放公共样式
│ ├── utils // 工具库/通用函数
│ ├── index.html // 入口html页面
│ └── main.js // 项目入口文件
├── .babelrc // babel配置
├── .editorconfig // 项目格式配置
├── .eslintrc.js // ESLint配置
├── .gitignore // git 忽略配置
├── .postcssrc.js // postcss配置
├── package.json // 依赖包配置
└── README.md // 项目说明
复制代码
文章中使用 Yarn 管理安装包,若未安装Yarn,替换成 Npm 对应命令便可。github
yarn init
复制代码
yarn add -D webpack webpack-cli webpack-merge
复制代码
项目中使用的Webpack版本是^4.41.2
,Webpack4.0 打包构建作了不少默认的优化配置,很多配置项无需配置或更改。
好比:针对开发模式的加快打包速度,合并chunk; 针对生产模式的代码压缩,减小打包体积等。
// 一部分默认配置
optimization: {
removeAvailableModules: true, // 删除已解决的chunk (默认 true)
removeEmptyChunks: true, // 删除空的chunks (默认 true)
mergeDuplicateChunks: true // 合并重复的chunk (默认 true)
}
// 针对生产环境默认配置
optimization: {
sideEffects:true, //配合tree shaking
splitChunks: {...}, //拆包
namedModules: false, // namedChunks:false 不启用chunk命名,默认自增id
minimize: true, // 代码压缩
}
复制代码
根据开发环境/生产环境 区分webpack配置很是有必要,能够加快开发环境的打包速度,有时候遇到开发环境打包过慢,能够排查下是否配置有误(好比开发环境开启了代码压缩等)。
项目中配合webpack-merge
根据开发环境/生产环境进行拆分配置:
Webpack4.0发布已经很长时间了,相信基本上项目都已迁移至4.0,在这里就很少赘述了。
安装:
yarn add -D html-webpack-plugin
复制代码
配置:
const srcDir = path.join(__dirname, "../src");
plugins: [
new HtmlWebpackPlugin({
template: `${srcDir}/index.html`
})
]
复制代码
安装:
yarn add -D webpack-dev-server clean-webpack-plugin
复制代码
开发环境利用webpack-dev-server
搭建本地 web server,并启用模块热更新(HMR)。
为方便开发调试,转发代理请求(本例中配合axios封装 转发接口到easy-mock在线平台)
配置:
mode: "development", // 开发模式
devServer: { // 本地服务配置
port: 9000,
hot: true,
open: false,
historyApiFallback: true,
compress: true,
proxy: { // 代理
"/testapi": {
target:
"https://www.easy-mock.com/mock/5dff0acd5b188e66c6e07329/react-template",
changeOrigin: true,
secure: false,
pathRewrite: { "^/testapi": "" }
}
}
},
plugins: [
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin()
],
复制代码
安装:
yarn add -D babel-loader @babel/core @babel/plugin-transform-runtime
@babel/preset-env @babel/preset-react babel-plugin-import
@babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators
复制代码
Webpack中Babel配置,是比较重要的一环。关系着ES6语法、React jsx、Mobx等语法通过打包后可否正常运行。
其中:
@babel/preset-react
转换React jsx语法;@babel/plugin-proposal-class-properties
转换 Class语法;@babel/plugin-proposal-decorators
转换 Mobx 等更高级的语法;babel-plugin-import
配合实现React组件的按需加载;这里须要注意Babel7.0 相较于Babel6.0的区别。
配置:
module: {
rules: [
{
test: /\.(js|jsx)$/,
include: [srcDir],
use: ["babel-loader?cacheDirectory=true"]
},
]
}
复制代码
.babelrc
文件配置{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
"@babel/transform-runtime",
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
["@babel/plugin-proposal-class-properties", { "loose": true }],
[
"import",
{
"libraryName": "antd",
"libraryDirectory": "es",
"style": "css" // `style: true` 会加载 less 文件
}
]
]
}
复制代码
安装:
yarn add -D less less-loader style-loader css-loader url-loader
mini-css-extract-plugin postcss-loader autoprefixer
复制代码
其中:
less-loader、style-loader、css-loader
处理加载less、css文件;postcss-loader、autoprefixer
处理css样式浏览器前缀兼容;url-loader
处理图片、字体文件等资源;mini-css-extract-plugin
分离css成单独的文件;配置:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
...
module: {
rules: [
{
test: /\.less$/,
use: [
devMode ? "style-loader" : MiniCssExtractPlugin.loader,
"css-loader",
"postcss-loader",
"less-loader"
]
},
{
test: /\.css$/,
use: [
devMode ? "style-loader" : MiniCssExtractPlugin.loader,
"css-loader",
"postcss-loader"
]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: ["url-loader"],
include: [srcDir]
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
use: ["url-loader"],
include: [srcDir]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: ["url-loader"],
include: [srcDir]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[contenthash:8].css",
chunkFilename: "chunk/[id].[contenthash:8].css"
}),
],
复制代码
配置postcss .postcssrc.js
文件
// .postcssrc.js
module.exports = {
plugins: {
autoprefixer: {}
}
};
// package.json中配置兼容浏览器
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
]
复制代码
安装:
yarn add -D happypack
复制代码
配置:
const os = require("os");
const HappyPack = require("happypack");
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
module: {
rules: [
{
test: /\.(js|jsx)$/,
include: [srcDir],
exclude: /(node_modules|bower_components)/,
use: ["happypack/loader?id=happybabel"]
},
]
},
plugins: [
//开启 happypack 的线程池
new HappyPack({
id: "happybabel",
loaders: ["babel-loader?cacheDirectory=true"],
threadPool: happyThreadPool,
cache: true,
verbose: true
}),
]
复制代码
根据实际项目状况拆分模块,配合异步加载,防止单个文件过大。
optimization: {
runtimeChunk: {
name: "manifest"
},
splitChunks: {
chunks: "all", //默认只做用于异步模块,为`all`时对全部模块生效,`initial`对同步模块有效
cacheGroups: {
dll: {
test: /[\\/]node_modules[\\/](react|react-dom|react-dom-router|babel-polyfill|mobx|mobx-react|mobx-react-dom|antd|@ant-design)/,
minChunks: 1,
priority: 2,
name: "dll"
},
codeMirror: {
test: /[\\/]node_modules[\\/](react-codemirror|codemirror)/,
minChunks: 1,
priority: 2,
name: "codemirror"
},
vendors: {
test: /[\\/]node_modules[\\/]/,
minChunks: 1,
priority: 1,
name: "vendors"
}
}
}
}
复制代码
引入 ESLint 与 Prettier 配合,规范化团队项目代码开发,统一代码风格。
yarn add -D prettier babel-eslint eslint eslint-loader eslint-config-airbnb
eslint-config-prettier eslint-plugin-babel eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
复制代码
(细节待补充)
具体配置详见**/build目录**
下 项目代码。
package.json
文件
{
...
"scripts": {
"start": "webpack-dev-server --color --inline --progress --config build/webpack.dev.js", //
"build": "NODE_ENV=production webpack --progress --config ./build/webpack.prod.js",
"build:report": "NODE_ENV=production webpack --progress --config ./build/webpack.prod.js",
"build:watch": "NODE_ENV=production webpack --progress --config ./build/webpack.prod.js"
},
...
}
复制代码
命令行运行:
// 命令行执行
// 运行开发环境;
yarn start
// 生产环境打包压缩;
yarn build
// 图形化分析打包文件大小;
yarn build:report
// 方便排查生产环境打包后文件的错误信息(文件source map);
yarn build:watch
复制代码
其中build:report、build:watch
可以实现功能,是在build/webpack.prod.js
中有以下代码:
// 方便排查生产环境打包后文件的错误信息(文件source map)
if (process.env.npm_lifecycle_event == "build:watch") {
config = merge(config, {
devtool: "cheap-source-map"
});
}
// 图形化分析打包文件大小
if (process.env.npm_lifecycle_event === "build:report") {
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
.BundleAnalyzerPlugin;
config.plugins.push(new BundleAnalyzerPlugin());
}
复制代码
实际开发用到的依赖包安装:
yarn add react react-dom react-router-dom mobx mobx-react mobx-react-router
axios antd moment
复制代码
咱们在写具体代码以前要作的第一个决定就是,目录结构怎么构建?要把这些组件放在哪里?
根据我的习惯及经验,项目目录构建以下图所示:
├── build // webpack配置
│ ├── webpack.common.js // webpack通用配置
│ ├── webpack.dev.js // webpack开发环境配置
│ └── webpack.prod.js // webpack生产环境配置
├── dist // 打包输出目录
├── public // 项目公开目录
├── src // src开发目录
│ ├── assets // 静态资源
│ ├── components // 公共组件
│ ├── layouts // 页面布局组件
│ ├── modules // 公共业务模块
│ ├── pages // 具体业务页面
│ ├── routers // 项目路由配置
│ ├── services // axios服务等相关
│ ├── stores // 全局公共 mobx store
│ ├── styles // 存放公共样式
│ ├── utils // 工具库/通用函数
│ ├── index.html // 入口html页面
│ └── main.js // 项目入口文件
├── .babelrc // babel配置
├── .editorconfig // 项目格式配置
├── .eslintrc.js // ESLint配置
├── .gitignore // git 忽略配置
├── .postcssrc.js // postcss配置
├── package.json // 依赖包配置
└── README.md // 项目说明
复制代码
页面模块目录结构,好比FormDemo页面结构:
├── FormDemo // 表单演示 页面
│ ├── index.js // 页面入口文件
│ ├── newModal.js // 弹窗组件
│ ├── searchForm.js // 搜索表单 模块组件
│ ├── store.js // 本页面使用的 mobx store 数据
│ └── style.less // 页面样式
复制代码
Hook 是 React 16.8 的新增特性。它可让你在不编写 class 的状况下使用 state 以及其余的 React 特性。
当前React版本已更新到16.12,Hooks 彻底应该成为 React 使用的主流。本项目中将彻底拥抱Hook,通常再也不用 class 来实现组件。
**
如下为部分实现代码(可暂忽略mobx的使用):
import React, { useState, useEffect, useContext } from 'react';
import { observer } from 'mobx-react';
import { Button } from 'antd';
import Store from './store';
import './style.less';
const HomePage = () => {
// useContext 订阅mobx数据
const pageStore = useContext(Store);
// useState state状态
const [num, setNum] = useState(0);
// useEffect反作用
useEffect(() => {
pageStore.qryTableDate();
}, []);
return (
<div className="page-home page-content"> <h2>{pageStore.pageTitle}</h2> <div> <span>num值:{num}</span> <Button type="primary" size="small" style={{ marginLeft: 10 }} onClick={() => setNum(num + 1)} >+1</Button> </div> </div>
);
};
export default observer(HomePage);
复制代码
项目是单页应用,路由配置通常分为约定式动态路由和集中配置式路由。
在 React 的世界里,直接采用成熟的react-router
工具管理页面路由。咱们如今说到react-router
,基本上都是在说 react-router
的第4版以后的版本,当前的最新版本已经更新到5.1.x了。
当前react-router
支持动态路由,彻底用React组件来实现路由,在渲染过程当中动态设置路由规则,匹配命中规则加载对应页面组件。
本项目采用集中配置式路由(方便路由鉴权、从服务端接口获取菜单路由配置等),同时兼顾方便地设置侧边菜单栏。 固然为简单起见,项目中读取本地静态菜单配置,也暂未引入路由鉴权。
src/routes/config.js
:import React, { lazy } from "react";
import BasicLayout from "@/layouts/BasicLayout";
import BlankLayout from "@/layouts/BlankLayout";
const config = [
{
path: "/",
component: BlankLayout, // 空白页布局
childRoutes: [ // 子菜单路由
{
path: "/login", // 路由路径
name: "登陆页", // 菜单名称 (不设置,则不展现在菜单栏中)
icon: "setting", // 菜单图标
component: lazy(() => import("@/pages/Login")) // 懒加载 路由组件
},
// login等没有菜单导航栏等基本布局的页面, 要放在基本布局BasicLayout以前。
{
path: "/",
component: BasicLayout, // 基本布局框架
childRoutes: [
{
path: "/welcome",
name: "欢迎页",
icon: "smile",
component: lazy(() => import("@/pages/Welcome"))
},
{... /* 其余 */},
{ path: "/", exact: true, redirect: "/welcome" },
{ path: "*", exact: true, redirect: "/exception/404" }
]
}
]
}
];
export default config;
复制代码
上面是静态路由的一部分配置,
注意:<Router>
中会用<Switch>
包裹,会匹配命中的第一个。"/login"
等没有菜单导航栏等基本布局的页面, 要放在基本布局BasicLayout
以前。
利用<Suspense>
和React.lazy()
实现页面组件懒加载。
src/routes/AppRouter.js:
import React, { lazy, Suspense } from "react";
import LoadingPage from "@/components/LoadingPage";
import {
HashRouter as Router,
Route,
Switch,
Redirect
} from "react-router-dom";
import config from "./config";
const renderRoutes = routes => {
if (!Array.isArray(routes)) {
return null;
}
return (
<Switch>
{routes.map((route, index) => {
if (route.redirect) {
return (
<Redirect
key={route.path || index}
exact={route.exact}
strict={route.strict}
from={route.path}
to={route.redirect}
/>
);
}
return (
<Route
key={route.path || index}
path={route.path}
exact={route.exact}
strict={route.strict}
render={() => {
const renderChildRoutes = renderRoutes(route.childRoutes);
if (route.component) {
return (
<Suspense fallback={<LoadingPage />}>
<route.component route={route}>
{renderChildRoutes}
</route.component>
</Suspense>
);
}
return renderChildRoutes;
}}
/>
);
})}
</Switch>
);
};
const AppRouter = () => {
return <Router>{renderRoutes(config)}</Router>;
};
export default AppRouter;
复制代码
react-router-dom
也已经支持 hooks语法,获取路由信息或路由跳转,可使用新的hooks 函数:
[useHistory](https://reacttraining.com/react-router/core/api/Hooks/usehistory)
:获取历史路由,回退、跳转等操做;useLocation
:查看当前路由信息;[useParams](https://reacttraining.com/react-router/core/api/Hooks/useparams)
:读取路由附带的params参数信息;[useRouteMatch](https://reacttraining.com/react-router/core/api/Hooks/useroutematch)
:匹配当前路由;只要包裹在中的子组件均可以经过这几个钩子函数获取路由信息。
代码演示:
import { useHistory } from "react-router-dom";
function HomeButton() {
const history = useHistory();
function onClick() {
history.push("/home");
}
return (
<button type="button" onClick={onClick}> 跳转Home页 </button>
);
}
复制代码
项目中是否使用状态管理工具或使用何种管理工具,依据实际项目状况而定。
本项目使用本身比较熟悉的Mobx,Mobx是一个功能强大,上手很是容易的状态管理工具。
为了使用简洁及管理方便,在组织上,分为全局公共数据状态和页面数据状态。
公用数据状态存放在/src/stores
目录下;页面几数据存放于对应页面目录下。
在实现上,利用mobx
+ useContext Hook特性
实现函数式组件的状态管理。
具体在于利用React的createdContext
构建包含Mobx 的context上下文;函数式组件中使用useContext Hook
订阅Mobx数据变化。
页面级store.js
代码:
import { createContext } from "react";
import { observable, action, computed } from "mobx";
import request from "@/services/newRequest";
class HomeStore {
@observable tableData = [];
@observable pageTitle = "Home主页";
@observable loading = false;
@action.bound setData(data = {}) {
Object.entries(data).forEach(item => {
this[item[0]] = item[1];
});
}
// 列表数据
@action.bound
async qryTableDate(page = 1, size = 10) {
this.loading = true;
const res = await request({
url: "/list",
method: "post",
data: { page, size }
});
if (res.success) {
const resData = res.data || {};
console.log(resData);
}
this.loading = false;
}
}
export default createContext(new HomeStore());
复制代码
页面组件 代码:
import React, { useContext } from "react";
import { observer } from "mobx-react";
import Store from "./store";
import "./style.less";
const HomePage = () => {
const pageStore = useContext(Store);
return (
<div className="page-home page-content"> home页面 <h2>{pageStore.pageTitle}</h2> </div>
);
};
export default observer(HomePage);
复制代码
以上为部分演示代码,具体业务实现能够查看项目代码。
Axios请求封装,具体代码见/src/services/newRequest.js
思路详见本人以前的另外一篇文章(忽略外部组件便可):「漫漫长路-Axios封装」
UI组件使用优秀的Ant Design 组件库,注意使用 babel-plugin-import 配置实现组件的按需加载。
本项目的内部页面布局采用Antd
上经典的布局方式:
页面布局须要合理拆分模块,左侧菜单导航栏根据静态菜单渲染。实际完整代码详见项目,如下为BasicLayout组件:
import React from "react";
import { Layout } from "antd";
import SiderMenu from "../SiderMenu";
import MainHeader from "../MainHeader";
import MainFooter from "../MainFooter";
import "./style.less";
const BasicLayout = ({ route, children }) => {
return (
<Layout className="main-layout">
{/* 左侧菜单导航 */}
<SiderMenu routes={route.childRoutes} />
<Layout className="main-layout-right">
{/* 顶部展现布局 */}
<MainHeader></MainHeader>
<Layout.Content className="main-layout-content">
{/* 实际页面布局 */}
{children}
{/* <MainFooter></MainFooter> */}
</Layout.Content>
</Layout>
</Layout>
);
};
export default BasicLayout;
复制代码
对于登陆页等页面无需套在上面的基本布局之类,须要单独处理(菜单配置在BasicLayout配置以前)。
ESLint+ prettier
规范化团队代码风格;项目Git地址:github.com/now1then/re…;
文章连接-语雀:www.yuque.com/nowthen/lon…;
在线演示地址:
书写不易,以为还不错或者有帮助的童鞋,欢迎关注、多多star;(-.-)