快速构建Web应用,从零学习React后台项目模版
1.创建相关应用模版请参考链接:https://developer.aliyun.com/article/878171?spm=a2c6h.12873581.0.dArticle878171.c61253e8nVBtAv
2.完成创建后就可以在github中查看到新增的react仓库
1.将应用模版克隆到本地
-  
  
首先假定你已经安装了Git、node,没有安装请移步node官网进行安装。克隆项目:  
git clone + 项目地址 
-  
  
进入项目文件  
cd create-react-app 
-  
  
切换到feature/1.0.0 分支上  
git checkout feature/1.0.0 
-  
  
使用一下命令全局安装 React :  
npm install -g create-react-app 
安装依赖包
npm install 
启动服务
npm start 
2.架构与效果预览
《后台管理》项目架构
效果预览
3.初始化项目
初始化package.json
npm init 
安装webpack
npm add -D webpack webpack-cli webpack-merge 
比如:针对开发模式的加快打包速度,合并chunk; 针对生产模式的代码压缩,减少打包体积等。
// 一部分默认配置optimization: {removeAvailableModules: true, // 删除已解决的chunk (默认 true)removeEmptyChunks: true, // 删除空的chunks (默认 true)mergeDuplicateChunks: true // 合并重复的chunk (默认 true)}// 针对生产环境默认配置optimization: {sideEffects:true, //配合tree shakingsplitChunks: {...}, //拆包namedModules: false, // namedChunks:false 不启用chunk命名,默认自增idminimize: true, // 代码压缩}
根据开发环境/生产环境 区分webpack配置非常有必要,可以加快开发环境的打包速度,有时候遇到开发环境打包过慢,可以排查下是否配置有误(比如开发环境开启了代码压缩等)。
项目中配合webpack-merge根据开发环境/生产环境进行拆分配置:
Webpack4.0发布已经很长时间了,相信基本上项目都已迁移至4.0,在这里就不多赘述了。
配置Html模版
npm add -D html-webpack-plugin 
配置:
const srcDir = path.join(__dirname, "../src");plugins: [new HtmlWebpackPlugin({template: `${srcDir}/index.html`})]
配置本地服务及热更新
npm 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()],
配置Babel
安装:
npm 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
其中:
配置:
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 文件}]]}
处理Less样式和图片等资源
安装:
npm add -D less less-loader style-loader css-loader url-loadermini-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.jsmodule.exports = {plugins: {autoprefixer: {}}};// package.json中配置兼容浏览器"browserslist": ["> 1%","last 2 versions","not ie <= 10"]
利用happypack多线程打包
安装:
npm 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"}}}}
其他配置
npm add -D prettier babel-eslint eslint eslint-loader eslint-config-airbnbeslint-config-prettier eslint-plugin-babel eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
npm scripts
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"},...}
npm start 
// 生产环境打包压缩;
npm build 
// 图形化分析打包文件大小;
npm build:report 
// 方便排查生产环境打包后文件的错误信息(文件source map);
npm build:watch 
// 方便排查生产环境打包后文件的错误信息(文件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());}
项目代码架构
npm add react react-dom react-router-dom mobx mobx-react mobx-react-routeraxios antd moment
4.函数化Hooks
当前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);
5.Router路由配置
在 React 的世界里,直接采用成熟的react-router工具管理页面路由。我们现在说到react-router,基本上都是在说 react-router 的第4版之后的版本,当前的最新版本已经更新到5.1.x了。
当前react-router支持动态路由,完全用React组件来实现路由,在渲染过程中动态设置路由规则,匹配命中规则加载对应页面组件。
本项目采用集中配置式路由(方便路由鉴权、从服务端接口获取菜单路由配置等),同时兼顾方便地设置侧边菜单栏。当然为简单起见,项目中读取本地静态菜单配置,也暂未引入路由鉴权。
6.静态路由配置 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之前。
7.路由组建渲染 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 (<Redirectkey={route.path || index}exact={route.exact}strict={route.strict}from={route.path}to={route.redirect}/>);}return (<Routekey={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;
8.路由 hooks 语法
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>);}
9.结合mobx管理数据状态
本项目使用自己比较熟悉的Mobx,Mobx是一个功能强大,上手非常容易的状态管理工具。
公用数据状态存放在/src/stores目录下;页面几数据存放于对应页面目录下。
具体在于利用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.boundasync 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);
10.Axios Http 请求封装
11.UI组件及页面布局
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;
1.上传代码
git add .git commit -m '添加你的注释'git push
2.在日常环境部署
3.配置自定义域名在线上环境上线
-  
  
配置线上环境自定义域名。在功能开发验证完成后要在线上环境进行部署,在线上环境的「部署配置」-「自定义域名」中填写自己的域名。例如我们添加一个二级域名 company.workbench.fun 来绑定我们部署的前端应用。然后复制自定义域名下方的API网关地址对添加的二级域名进行CNAME配置。 
 
-  
  
配置CNAME地址。复制好 API网关域名地址后,来到你自己的域名管理平台(此示例中的域名管理是阿里云的域名管理控制台,请去自己的域名控制台操作)。添加记录的「记录类型」选择「CNAME」,在「主机记录」中输入你要创建的二级域名,这里我们输入「company」,在「记录值」中粘贴我们之前复制的 API网关域名地址,「TTL」保留默认值或者设置一个你认为合适的值即可。 
 
-  
  
在线上环境部署上线。回到云开发平台的应用详情页面,按照部署的操作,点击线上环境的「部署按钮」,部署完成以后就在你自定义的域名进行了上线。CNAME 生效之后,我们输入 company.workbench.fun(示例网址) 可以打开部署的页面。至此,如何部署一个应用到线上环境,如何绑定自己的域名来访问一个线上的应用就完成了,赶紧部署自己的应用到线上环境,用自己的域名玩起来吧 ;)  
参考文献:https://juejin.cn/post/6844904035099623437
👇 戳「阅读原文」,让云计算开箱即用!
