2022新春版:超详细一条龙教程!从零搭建React项目全家桶


React是近几年来前端项目开发非常火的一个框架,其背景是Facebook团队的技术支持,市场占有率也很高。很多初学者纠结一开始是学react还是vue。个人觉得,有时间的话,最好两个都掌握一下。从学习难度上来说,react要比vue稍难一些。万事开头难,但是掌握了react对于大幅提高前端技能还是非常有帮助的。本文一步步详细梳理了从创建react、精简项目、配置调试环境,到集成各种相关工具等内容,重在快速搭建标准React项目工程,对于react初学者来说,能够节省很多探索的时间。
在2020年2月,我发布了《超全面详细一条龙教程!从零搭建React项目全家桶》,时隔两年,技术迭代了很多,因此更新一版。本教程使用目前最新的技术版本,章节内容做了调整和补充,更便于理解,同时也修正了旧版内容中的纰漏。下面请跟着新版教程一步步操作。
本次分享Demo的主要依赖包版本:
Node.js 16.13.2create-react-app 5.0.0react 17.0.2react-router-dom 6.2.1antd 4.18.7node-sass 7.0.1sass-loader 12.3.0less 4.1.2less-loader 10.2.0stylus 0.56.0stylus-loader 6.2.0axios 0.26.0history 4.10.1immutable 4.0.0mockjs 1.1.0react-redux 7.2.6redux 4.1.2redux-immutable 4.0.0redux-thunk 2.4.1babel-plugin-import 1.13.3http-proxy-middleware 2.0.3
先睹为快
先看下目录了解本教程都有哪些内容。
1 初始化项目
• 1.1 使用create-react-app 5.0新建项目
• 1.2 精简项目
2 Webpack配置
• 2.1 暴露webpack
• 2.2 支持Sass/Scss
• 2.3 支持Less
• 2.4 支持Stylus
• 2.5 设置路径别名
• 2.6 禁止build项目生成map文件
3 项目架构搭建
• 3.1 项目目录结构设计
• 3.2 设置全局公用样式
• 3.3 关于样式命名规范
4 引入Ant Design
• 4.1 安装Ant Design
• 4.2 实现按需加载
• 4.3 设置Antd为中文语言
• 4.4 自定义Antd主题颜色(非必须)
5 页面开发
• 5.1 构建login页面
• 5.2 构建home页面
• 5.3 实现页面路由跳转
• 5.4 在React组件中实现页面路由跳转
• 5.5 在非React组件中实现页面路由跳转
6 组件开发
• 6.1 创建Header组件
• 6.2 引入Header组件
• 6.3 组件传参
7 React Developer Tools浏览器插件
8 Redux及相关插件
• 8.1 安装redux
• 8.2 安装react-redux
• 8.3 安装redux-thunk
• 8.4 安装Redux浏览器插件
• 8.5 创建store
• 8.6 复杂项目store分解
• 8.7 安装使用immutable
• 8.8 对接react-redux与store
• 8.9 在login页面设置并实时读取Redux变量
• 8.10 在header组件实时读取Redux变量
• 8.11 Redux开发小结
9 基于axios封装公用API库
• 9.1 安装axios
• 9.2 封装公用API库
• 9.3 Mock.js安装与使用
• 9.4 发起API请求
• 9.5 设置开发环境的反向代理请求
10 build项目
11 项目Git源码
结束语
※注:
代码区域每行开头的:
"+" 表示新增
"-" 表示删除
"M" 表示修改
即便你是新手,跟着操作一遍,也可以快速上手React项目啦!
1 初始化项目
1.1 使用create-react-app 5.0新建项目
找个合适的目录,执行:
npx create-react-app react-app5命令最后的react-app5是项目的名称,可以自行更改。
编写教程时,create-react-app已经发布了5.0.0,如果一直报错:
you are running create-react-app 4.0.3 which is behind the latest release (5.0.0)说明你还在使用旧版本的create-react-app,需要先清除npx缓存,执行:
npx clear-npx-cache然后再执行npx create-react-app react-app5创建项目。
稍等片刻即可完成安装。安装完成后,可以使用npm或者yarn启动项目。
进入项目目录,并启动项目:
cd react-app5yarn start (或者使用npm start)
如果没有安装yarn,可以前往yarn中文网站安装:
https://yarn.bootcss.com/
启动后,可以通过以下地址访问项目:
http://localhost:3000/

2.2 精简项目
接下来,删除一般项目中用不到的文件,最简化项目。
├─ /node_modules├─ /public| ├─ favicon.ico| ├─ index.html- | ├─ logo192.png- | ├─ logo512.png- | ├─ mainfest.json- | └─ robots.txt├─ /src- | ├─ App.css| ├─ App.js- | ├─ App.test.js- | ├─ index.css| ├─ index.js- | ├─ logo.svg- | ├─ reportWebVitals.js- | └─ setupTests.js├─ .gitignore├─ package.json├─ README.md└─ yarn.lock
现在目录结构如下,清爽许多:
├─ /node_modules├─ /public| ├─ favicon.ico| └─ index.html├─ /src| ├─ App.js| └─ index.js├─ .gitignore├─ package.json├─ README.md└─ yarn.lock
以上文件删除后,页面会报错。这是因为相应的文件引用已不存在。需要继续修改代码,先让项目正常运行起来。
逐个修改以下文件:
src/App.js:
function App() {return <div className="App">React-App5div>}export default App
src/index.js:
import React from 'react'import ReactDOM from 'react-dom'import App from './App'ReactDOM.render(, document.getElementById('root'))
public/index.html:
<html lang="en"><head><meta charset="utf-8" /><link rel="icon" href="%PUBLIC_URL%/favicon.ico" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>React Apptitle>head><body><noscript>You need to enable JavaScript to run this app.noscript><div id="root">div>body>html>
运行效果如下:

2 Webpack配置
2.1 暴露webpack
create-react-app默认情况下未暴露配置文件。要更灵活配置项目,需要将配置文件暴露出来。
执行以下命令,暴露配置文件:
yarn ejecteject之前必须确保当前工程所有文件已提交git,否则会报以下错误:
Remove untracked files, stash or commit any changes, and try again.需要先在项目根目录下执行,提交git:
git add .git commit -m "初始化项目(备注)"
然后再执行:
yarn eject即可完成webpack的暴露,这时项目里会多出来很多文件。
2.2 支持Sass/Scss
eject后,虽然package.json以及webpack.config.js里有了sass相关代码,但是要正确使用Sass/Scss,还要再安装node-sass。
先设置node-sass的淘宝镜像,下载node-sass更快:
yarn config set SASS_BINARY_SITE http://npm.taobao.org/mirrors/node-sass如果还没有设置常规registry的淘宝镜像,也一并设置下:
yarn config set registry https://registry.npm.taobao.org/执行以下命令:
yarn add node-sass --dev安装完成后,项目已支持Sass/Scss。
2.3 支持Less
支持Less稍微多一点步骤,首先安装less和less-loader:
yarn add less less-loader --dev然后修改config/webpack.config.js:
// style files regexesconst cssRegex = /\.css$/;const cssModuleRegex = /\.module\.css$/;const sassRegex = /\.(scss|sass)$/;const sassModuleRegex = /\.module\.(scss|sass)$/;+ const lessRegex = /\.less$/;+ const lessModuleRegex = /\.module\.less$/;...(略)// Opt-in support for SASS (using .scss or .sass extensions).// By default we support SASS Modules with the// extensions .module.scss or .module.sass{test: sassRegex,exclude: sassModuleRegex,use: getStyleLoaders({importLoaders: 3,sourceMap: isEnvProduction && shouldUseSourceMap,},'sass-loader'),// Don't consider CSS imports dead code even if the// containing package claims to have no side effects.// Remove this when webpack adds a warning or an error for this.// See https://github.com/webpack/webpack/issues/6571sideEffects: true,},// Adds support for CSS Modules, but using SASS// using the extension .module.scss or .module.sass{test: sassModuleRegex,use: getStyleLoaders({importLoaders: 3,sourceMap: isEnvProduction && shouldUseSourceMap,modules: {getLocalIdent: getCSSModuleLocalIdent,},},'sass-loader'),},+ // 支持less+ {+ test: lessRegex,+ exclude: lessModuleRegex,+ use: getStyleLoaders(+ {+ importLoaders: 3,+ sourceMap: isEnvProduction+ ? shouldUseSourceMap+ : isEnvDevelopment,+ modules: {+ mode: 'icss',+ },+ },+ 'less-loader'+ ),+ sideEffects: true,+ },+ {+ test:lessModuleRegex,+ use: getStyleLoaders(+ {+ importLoaders: 3,+ sourceMap: isEnvProduction+ ? shouldUseSourceMap+ : isEnvDevelopment,+ modules: {+ mode: 'local',+ getLocalIdent: getCSSModuleLocalIdent,+ },+ },+ 'less-loader'+ ),+ },
其实就把上面sass配置代码复制一遍,改成less。按照以上操作后,项目已支持less。
2.4 支持Stylus
支持Stylus跟Less完全一样,首先安装stylus和stylus-loader:
执行以下命令:
yarn add stylus stylus-loader --dev安装完成后,按照上一小节介绍的支持less的方法,修改config/webpack.config.js:
// style files regexesconst cssRegex = /\.css$/;const cssModuleRegex = /\.module\.css$/;const sassRegex = /\.(scss|sass)$/;const sassModuleRegex = /\.module\.(scss|sass)$/;const lessRegex = /\.less$/;const lessModuleRegex = /\.module\.less$/;+ const stylusRegex = /\.styl$/;+ const stylusModuleRegex = /\.module\.styl$/;...(略)+ // 支持stylus+ {+ test: stylusRegex,+ exclude: stylusModuleRegex,+ use: getStyleLoaders(+ {+ importLoaders: 3,+ sourceMap: isEnvProduction+ ? shouldUseSourceMap+ : isEnvDevelopment,+ modules: {+ mode: 'icss',+ },+ },+ 'stylus-loader'+ ),+ sideEffects: true,+ },+ {+ test:stylusModuleRegex,+ use: getStyleLoaders(+ {+ importLoaders: 3,+ sourceMap: isEnvProduction+ ? shouldUseSourceMap+ : isEnvDevelopment,+ modules: {+ mode: 'local',+ getLocalIdent: getCSSModuleLocalIdent,+ },+ },+ 'stylus-loader'+ ),+ },
按照以上操作后,项目已支持stylus。
2.5 设置路径别名
为了避免使用相对路径的麻烦,可以设置路径别名。
修改config/webpack.config.js:
alias: {// Support React Native Web// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/'react-native': 'react-native-web',// Allows for better profiling with ReactDevTools...(isEnvProductionProfile && {'react-dom$': 'react-dom/profiling','scheduler/tracing': 'scheduler/tracing-profiling',}),...(modules.webpackAliases || {}),+ '@': path.join(__dirname, '..', 'src'),},
这样在js代码开头的import路径中,直接使用@表示“src根目录”,不用去自己去数有多少个"../"了。
例如,src/app.js:
// 表示该文件当前路径下的app.styl(相对路径)import './app.styl'// 表示src/app.styl,等价于上面的文件地址(绝对路径)import '@/app.styl'
2.6 禁止build项目生成map文件
map文件,即Javascript的source map文件,是为了解决被混淆压缩的js在调试的时候,能够快速定位到压缩前的源代码的辅助性文件。这个文件发布出去,会暴露源代码。因此,建议直接禁止build时生成map文件。
修改config/webpack.config.js,把shouldUseSourceMap的值改成false:
// Source maps are resource heavy and can cause out of memory issue for large source files.- // const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';+ const shouldUseSourceMap =false;
3 项目架构搭建
3.1 项目目录结构设计
项目目录结构可根据项目实际灵活制定。这里分享下我常用的结构,主要分为公用模块目录、组件模块目录、页面模块目录等几个部分,让项目结构更加清晰合理。
├─ /config <-- webpack配置目录├─ /node_modules├─ /public| ├─ favicon.ico <-- 网页图标| └─ index.html <-- HTML页模板├─ /scripts <-- node编译脚本├─ /src| ├─ /api <-- api目录| | └─ index.js <-- api库| ├─ /common <-- 全局公用目录| | ├─ /fonts <-- 字体文件目录| | ├─ /images <-- 图片文件目录| | ├─ /js <-- 公用js文件目录| | └─ /style <-- 公用样式文件目录| | | ├─ frame.styl <-- 全部公用样式(import本目录其他全部styl)| | | ├─ reset.styl <-- 清零样式(如果使用Ant Design,就无需此文件)| | | └─ global.styl <-- 全局公用样式| ├─ /components <-- 公共模块组件目录| | ├─ /header <-- 头部导航模块| | | ├─ index.js <-- header主文件| | | └─ header.styl <-- header样式文件| | └─ ... <-- 其他模块| ├─ /pages <-- 页面组件目录| | ├─ /home <-- home页目录| | | ├─ index.js <-- home主文件| | | └─ home.styl <-- home样式文件| | ├─ /login <-- login页目录| | | ├─ index.js <-- login主文件| | | └─ login.styl <-- login样式文件| | └─ ... <-- 其他页面| ├─ App.js <-- 项目主模块| ├─ index.js <-- 项目入口文件| ├─.gitignore| ├─ package.json| ├─ README.md| └─ yarn.lock
接下来,就按照上面的目录结构设计开始构建项目。
3.2 设置全局公用样式
本教程以Stylus作为css预处理语言。各位可以根据自己的习惯,自由选择Sass/Scss、Less、Stylus。
在frame.styl里引入其他公用样式。
src/common/stylus/frame.styl:
@import './reset.styl';@import './global.styl';
src/common/stylus/global.styl:
/*清浮动*/.clearfix:aftercontent: "."display: blockheight: 0clear: bothvisibility: hidden.clearfixdisplay:block.G-col-errorcolor: #f81d22.G-col-succcolor: #0b8235
src/common/stylus/reset.styl:
创建文件后,代码为空即可。因为本教程后续要引入Ant Design,因此不需要自行设置reset样式。
然后在src/index.js里引入frame.styl:
import React from 'react'import ReactDOM from 'react-dom'import App from './App'+ // 全局样式+ import '@/common/stylus/frame.styl'ReactDOM.render(, document.getElementById('root'))
这样在所有页面里就可以直接使用全局样式了。
3.3 关于样式命名规范
以我多年来的开发经验来讲,合理的样式命名规范对项目开发有很大的帮助,主要体现在以下方面:
(1)避免因样式名重复导致的污染。
(2)从命名上可直观区分“组件样式”、“页面样式”(用于给在此页面的组件样式做定制调整)、“全局样式”。
(3)快速定位模块,便于查找问题。
分享一下本教程的样式命名规范:
G-xx: 表示全局样式,用来定义公用样式。
P-xx: 表示页面样式,用来设置页面的背景色、尺寸、定制化调整在此页面的组件样式。
M-xx: 表示组件样式,专注组件本身样式。
后续教程中,可以具体看到以上规范是如何应用的。
4 引入Ant Design
Ant Design是一款非常优秀的UI库,在React项目开发中使用非常广泛。本次分享也特别说明下如何引入Ant Design。
4.1 安装Ant Design
执行:
yarn add antd4.2 实现按需加载
Ant Design的样式非常多,但项目中可能只使用了其中个别组件,没有必要加载全部样式。可以使用babel-plugin-import实现样式的按需加载。
安装babel-plugin-import:
yarn add babel-plugin-import --dev修改package.json:
"babel": {"presets": ["react-app"M ],+ "plugins": [+ [+ "import",+ {+ "libraryName": "antd",+ "style": "css"+ }+ ]+ ]}
然后修改src/App.js 来验证下Antd:
import { Button } from 'antd'function App() {return (<div className="App"><h1>React-App5h1><Button type="primary">ButtonButton>div>)}export default App
执行yarn start:

4.3 设置Antd为中文语言
Antd默认语言是英文,需进行以下设置调整为中文。
修改src/index.js:
import React from 'react'import ReactDOM from 'react-dom'+ import { ConfigProvider } from 'antd'+ import zhCN from 'antd/es/locale/zh_CN'import App from './App'// 全局样式import '@/common/stylus/frame.styl'+ const antdConfig = {+ locale: zhCN,+ }M ReactDOM.render(+ <ConfigProvider {...antdConfig}>+ <App />+ ConfigProvider>,+ document.getElementById('root')+ )
4.4 自定义Antd主题颜色(非必须)
如果不需要自定义Antd主题颜色,此章节可直接跳过。
Antd 的样式使用了 Less 作为开发语言,如果想自定义Ant Design的主题颜色,需要先让项目支持Less(已在2.3章节介绍)。
修改config/webpack.config.js:
if (preProcessor) {+ let preProcessorOptions = {+ sourceMap: true,+ }+ if (preProcessor === "less-loader") {+ preProcessorOptions = {+ sourceMap: true,+ //自定义主题+ lessOptions: {+ modifyVars: {+ // 自定义全局主色,绿色+ 'primary-color':' #67C23A',+ },+ javascriptEnabled: true,+ },+ }+ }loaders.push({loader: require.resolve('resolve-url-loader'),options: {sourceMap: isEnvProduction && shouldUseSourceMap,},},{loader: require.resolve(preProcessor),- // options: {- // sourceMap: true,- // },+ options: preProcessorOptions});}
修改package.json,把style的值由原来的"css"改为true:
"babel": {"presets": ["react-app"],"plugins": [["import",{"libraryName": "antd",M "style": true}]]},
重启项目,可以看到按钮已经变成自定义主题颜色了。
更多主题颜色配置请参考Antd官网:
https://ant.design/docs/react/customize-theme-cn
5 页面开发
本次教程包含两个页面,login和home。
工程文件变动如下:
├─ /config├─ /node_modules├─ /public├─ /scripts├─ /src| ├─ /api| ├─ /common| ├─ /components| ├─ /pages| | ├─ /homeM | | | ├─ index.jsM | | | └─ home.styl| | ├─ /loginM | | | ├─ index.jsM | | | ├─ login.styl+ | | | └─ logo.png| ├─ App.js| ├─ index.js| ├─.gitignore| ├─ package.json| ├─ README.md| └─ yarn.lock
5.1 构建login页面
页面构建代码不再详述,都是很基础的内容了。
src/pages/login/index.js:
import { Button, Input } from 'antd'import imgLogo from './logo.png'import './login.styl'function Login() {return (<div className="P-login"><img src={imgLogo} alt="" className="logo" /><div className="ipt-con"><Input placeholder="账号" />div><div className="ipt-con"><Input.Password placeholder="密码" />div><div className="ipt-con"><Button type="primary" block={true}>登录Button>div>div>)}export default Login
src/pages/login/login.styl:
.P-loginposition: absolutetop: 0bottom: 0width: 100%background: #7adbcb.logodisplay: blockmargin: 50px auto 20px.ipt-conmargin: 0 auto 20pxwidth: 400px: center
暂时修改下入口文件代码,把原App页面换成login页面,看看效果:
src/index.js:
- import App from './App'+ import App from '@/pages/login'

5.2 构建home页面
直接上代码。
src/pages/home/index.js:
import { Button } from 'antd'import './home.styl'function Home() {return (<div className="P-home"><h1>Home Pageh1><div className="ipt-con"><Button>返回登录Button>div>div>)}export default Home
src/pages/home/home.styl:
.P-homeposition: absolutetop: 0bottom: 0width: 100%background: linear-gradient(#f48c8d,#f4c58d)h1: 50px: centercolor: #fff.ipt-conmargin: 20px auto 0: center
暂时修改下入口文件代码,把初始页面换成home页面,看看效果:
src/index.js:
- import App from '@/pages/login'+ import App from '@/pages/home'

5.3 实现页面路由跳转
为了实现页面的跳转,需要安装react-router-dom。
执行:
yarn add react-router-dom现在,将src/App.js正式作为路由配置页,进行代码重构。
src/App.js:
import { HashRouter, Route, Routes, Navigate } from 'react-router-dom'import Login from '@/pages/login'import Home from '@/pages/home'function App() {return ({/* 路由精确匹配"/home",跳转Home页面 */}"/home" element={} /> {/* 路由精确匹配"/login",跳转Login页面 */}"/login" element={} /> {/* 未匹配,则跳转Login页面 */}"*" element={"/login" />} /> )}export default App
接下来,将入口文件改回到App路由页面。
src/index.js:
- import App from '@/pages/home'+ import App from './App'
执行yarn start启动项目,输入对应的路由地址,可以正常显示对应的页面了。
login页面:
http://localhost:3000/#/login
home页面:
http://localhost:3000/#/home
5.4 在React组件中实现页面路由跳转
下面要实现的功能是,点击login页面的“登录”按钮,跳转至home页面。
修改src/pages/login/index.js:
+ import { useNavigate } from 'react-router-dom'import { Button, Input } from 'antd'import imgLogo from './logo.png'import './login.styl'function Login() {+ // 创建路由钩子+ const navigate = useNavigate()return (...(略)"ipt-con">M block={true} onClick={()=>{navigate('/home')}}>登录...(略)
同样的方法,再来实现点击home页面的“返回登录”按钮,跳转至login页面。
修改src/pages/home/index.js:
+ import { useNavigate } from 'react-router-dom'import { Button } from 'antd'import './home.styl'function Home() {+ // 创建路由钩子+ const navigate = useNavigate()return (<div className="P-home"><h1>Home Pageh1><div className="ipt-con">M <Button onClick={()=>{navigate('/login')}}>返回登录Button>div>div>)}export default Home
现在,点击按钮进行页面路由跳转已经实现了。
5.5 在非React组件中实现页面路由跳转
在实际项目中,经常需要在非React组件中进行页面跳转。比如,当进行API请求的时候,如果发现登录认证已失效,就直接跳转至login页面;当API请求失败时,进行统一的报错提示。
以上这些情况的统一处理,当然是封装成公用的模块最合适。但往往这些纯功能性的模块都不是React组件,也就是纯原生js。所以就没办法使用useNavigate()了。
下面介绍一下如何实现在非React组件中进行页面路由跳转。
需要安装额外的history依赖包。截至本文编写时,history最新版本为5.2.0,但history.push()只改变了页面地址栏的地址,却没有进行实际的跳转。在GitHub上也有很多人反馈,应该是最新版本的bug。目前的解决办法是安装4.10.1版本。
执行:
yarn add history@4.10.1在阅读本文时,建议先确认下history是否已经发布了更新的版本(>5.2.0),安装最新版本的history试一试,如果bug依旧,再安装4.10.1。
安装完成后,新建目录及文件,src/api/index.js:
import { createHashHistory } from 'history'let history = createHashHistory()export const goto = (path) => {history.push(path)}
在src/pages/home/index.js里调用goto方法:
import { useNavigate } from 'react-router-dom'import { Button } from 'antd'+ import { goto } from '@/api'import './home.styl'function Home() {// 创建路由钩子const navigate = useNavigate()return (<div className="P-home"><h1>Home Pageh1>+ <div className="ipt-con">+ <Button onClick={()=>{goto('/login')}}>组件外跳转Button>+ div><div className="ipt-con"><Button onClick={()=>{navigate('/login')}}>返回登录Button>div>div>)}export default Home
在home页点击“组件外跳转”按钮,可以正常跳转至login页面了,而实际执行跳转的代码是在src/api/index.js(非React组件)中,这样就非常适合封装统一的处理逻辑。

后续章节会讲述如何封装api接口,并通过组件外路由的方式实现API调用失败时的统一跳转。
6 组件开发
这章节内容也很容易,接触过vue的同学应该也很清楚,为了教程的完整性,还是简单说一下。下面来简单实现一个公用的头部组件。
6.1 创建Header组件
目录结构变动如下:
| ├─ /components <-- 公共模块组件目录+ | | ├─ /header <-- 公用header组件+ | | | ├─ index.js+ | | | └─ header.styl
src/components/header/index.js代码:
import './header.styl'function Header() {return <div className="M-header">Headerdiv>}export default Header
src/components/header/header.styl:
.M-headerheight: 40pxline-height: 40pxfont-size: 36pxcolor: #fffbackground: #409EFF
6.2 引入Header组件
在login页面里引入Header组件。
src/pages/login/index.js:
import { useNavigate } from 'react-router-dom'import { Button, Input } from 'antd'import imgLogo from './logo.png'+ import Header from '@/components/header'import './login.styl'function Login() {// 创建路由钩子const navigate = useNavigate()return (浏览 31M...(略)
同样的方式在home页面里引入Header组件。
src/pages/home/index.js:
import { useNavigate } from 'react-router-dom'import { Button } from 'antd'+ import Header from '@/components/header'import { goto } from '@/api'import './home.styl'function Home() {// 创建路由钩子const navigate = useNavigate()return (<div className="P-home">+ <Header /><h1>Home Pageh1>
运行项目,Header组件已经成功加入。
6.3 组件传参
使用过vue的同学都知道,vue组件有data和props。
data是组件内的数据;
props用来接收父组件传递来的数据。
在React中,如果使用的是Class方式定义的组件:
state是组件内的数据;
props用来接收父组件传递来的数据。
如果使用的是function方式定义的组件(也叫无状态组件):
使用useState()管理组件内的数据(hook);
使用props接收父组件传递来的数据。
Class组件有明确的声明周期管理,但是代码相对来说不如无状态组件简洁优雅。
无状态组件通过hook管理声明周期,效率更高。因此本教程全程使用无状态组件进行讲解。
下面简单演示下如何实现向子组件传递数据。
通过login和home分别向Header组件传递不同的值,并显示在Header组件中。
修改src/pages/login/index.js:
...(略)M"login" info={()=>{console.log('info:login')}}/>...(略)
修改src/pages/home/index.js:
...(略)M"home" info={()=>{console.log('info:home')}}/>...(略)
修改src/components/header/index.js:
import './header.styl'M function Header(props) {+ // 接收来自父组件的数据+ const { title, info } = props+ // 如果info存在,则执行info()+ info && info()M return <div className="M-header">Header:{title}div>}export default Header
运行看下已经生效。
7 React Developer Tools浏览器插件
为了更方便调试react项目,建议安装chrome插件。
先科学上网,在chrome网上应用店里搜索“React Developer Tools”并安装。
安装完成后,打开chrome DevTools,点击Components按钮,可以清晰的看到react项目代码结构以及各种传参。
8 Redux及相关插件
Redux是用来做什么的?简单通俗的解释,Redux是用来管理项目级别的全局变量,而且是可以实时监听变量的变化并改变DOM的。当多个模块都需要动态显示同一个数据,并且这些模块从属于不同的父组件,或者在不同的页面中,如果没有Redux,那实现起来就很麻烦了,问题追踪也很痛苦。因此Redux就是解决这个问题的。
做过vue开发的同学都知道vuex,react对应的工具就是Redux,当然还有一些附属工具,比如react-redux、redux-thunk、immutable。
redux涉及的内容较多,把各个依赖组件的官方文档都阅读一遍确实不容易消化。本次分享通过一个简单的Demo,把redux、react-redux、redux-thunk、immutable这些依赖组件的使用方法串起来,非常有利于理解。
8.1 安装redux
执行:
yarn add redux
仅安装redux也是可以使用的,但是比较麻烦。redux里更新store里的数据,需要手动订阅(subscribe)更新,这里就不展开介绍了。可以借助另一个插件(react-redux)提高开发效率。
8.2 安装react-redux
执行:
yarn add react-redux
react-redux允许通过connect方法,将store中的数据映射到组件的props,省去了store订阅。原state中读取store的属性改用props读取。
由于store(8.5章节)还没讲到,react-redux使用方法在8.8章节介绍。
8.3 安装redux-thunk
执行:
yarn add redux-thunk
redux-thunk允许在actionCreators里传递函数类型的数据。这样可以把业务逻辑(例如接口请求)集中写在actionCreator.js,方便复用的同时,可以使组件的主文件更简洁。
8.4 安装Redux浏览器插件
为了更方便跟踪redux状态,建议安装chrome插件。这个插件可记录每次redux的变化,非常便于跟踪调式。
先科学上网,在chrome网上应用店里搜索“Redux DevTools”并安装。
安装完成后还不能直接使用,需要在项目代码中进行配置。接下来进行说明。
8.5 创建store
安装以上各种插件后,可以store用来管理状态数据了。
如果项目比较简单,只有一两个页面,可以只创建一个总store管理整体项目。目录结构参考如下:
├─ /src+ | ├─ /store+ | | ├─ actionCreators.js+ | | ├─ constants.js <-- 定义方法的常量+ | | ├─ index.js+ | | └─ reducer.js
以下是各文件的代码:
src/store/index.js:
import { createStore, applyMiddleware, compose } from 'redux'import reducer from './reducer'import thunk from 'redux-thunk'// 这里让项目支持浏览器插件Redux DevToolsconst composeEnhancers = typeof window === 'object' &&window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : composeconst enhancer = composeEnhancers(applyMiddleware(thunk));const store = createStore(reducer,enhancer)export default store
以上是store的核心代码,支持了Redux DevTools。同时,利用redux的集成中间件(applyMiddleware)功能将redux-thunk集成进来,最终创建了store。
src/store/constants.js:
export const SET_DATA = 'SET_DATA'
创建这个定义常量的文件,是因为方便被下面的reducer.js和actionCreators.js同时引用,便于统一修改和管理。
src/store/actionCreators.js:
import * as constants from './constants'export const getData = (data) => ({type: constans.SET_DATA,data})
src/store/reducer.js:
import * as constants from './constants'// 初始默认的stateconst defaultState = {myData: null}const reducer = (state = defaultState, action) => {// 由于state是引用型,不能直接修改,否则是监测不到state发生变化的。因此需要先复制一份进行修改,然后再返回新的state。let newState = Object.assign({}, state)switch(action.type) {case constants.SET_DATA:newState.myData = action.datareturn newStatedefault:return state}}export default reducer
以上代码,在store设置了一个myData。现在,state修改起来还是有点小麻烦,如何更好地解决这个问题,在8.8章节会提到。
到这里,你可能还是不知道Redux怎么用。实际项目中很少只用一个总store库来管理的。因此,在下面章节的分库内容中具体讲述Redux的使用方法。
8.6 复杂项目store分解
当项目的页面较多,如果数据都集中放在一个store里,维护成本将会变高。接下来分享下如何将store分解到各个组件中。
一般来说,每个组件有自己的store(分库),再由src/store作为总集,集成每个组件的store。
以header和login两个组件为例,分别创建组件自己的store,文件结构跟store总集一致。
目录结构变动如下:
| | ├─ /components| | | └─ /header+ | | | ├─ /store+ | | | | ├─ actionCreators.js+ | | | | ├─ constants.js+ | | | | ├─ index.js+ | | | | └─ reducer.js| | | ├─ header.styl| | | └─ index.js| | ├─ /pages| | | ├─ /login+ | | | | ├─ /store+ | | | | | ├─ actionCreators.js+ | | | | | ├─ constants.js+ | | | | | ├─ index.js+ | | | | | └─ reducer.js| | | | ├─ login.styl| | | | └─ index.js
src/components/header/store/index.js及
src/pages/login/store/index.js:
import reducer from './reducer'import * as actionCreators from './actionCreators'import * as constants from './constants'export { reducer, actionCreators, constants}
其实就是把当前组件store(分库)下的其他文件集中起来作为统一输出口。
src/components/header/store/constants.js:
const ZONE = 'components/header/'export const SET_DATA = ZONE + 'SET_DATA'
ZONE是用来避免与其他组件的constants重名。
同样的方式,在login下进行创建store。
src/pages/login/store/constants.js:
const ZONE = 'pages/login/'export const SET_DATA = ZONE + 'SET_DATA'
src/components/header/store/actionCreators.js及
src/pages/login/store/actionCreators.js:
import * as constants from './constants'export const setData = (data) => ({type: constants.SET_DATA,data})
src/components/header/store/reducer.js:
import * as constants from './constants'// 初始默认的stateconst defaultState = {myHeaderData: null}const reducer = (state = defaultState, action) => {// 由于state是引用型,不能直接修改,否则是监测不到state发生变化的。因此需要先复制一份进行修改,然后再返回新的state。let newState = Object.assign({}, state)switch(action.type) {case constants.SET_DATA:newState.myHeaderData = action.datareturn newStatedefault:return state}}export default reducer
同样的方式,src/pages/login/store/reducer.js:
import * as constants from './constants'// 初始默认的stateconst defaultState = {myLoginData: null}const reducer = (state = defaultState, action) => {// 由于state是引用型,不能直接修改,否则是监测不到state发生变化的。因此需要先复制一份进行修改,然后再返回新的state。let newState = Object.assign({}, state)switch(action.type) {case constants.SET_DATA:newState.myLoginData = action.datareturn newStatedefault:return state}}export default reducer
然后修改项目store总集,目录结构变动如下:
├─ /src| ├─ /store- | | ├─ actionCreators.js <-- 删除- | | ├─ constants.js <--删除| | ├─ index.jsM | | └─ reducer.js
src/store/reducer.js重写如下:
import { combineReducers } from 'redux'import { reducer as loginReducer } from '@/pages/login/store'import { reducer as headerReducer } from '@/components/header/store'const reducer = combineReducers({login: loginReducer,header: headerReducer})export default reducer
以上代码的作用就是把login和header的store引入,然后通过combineReducers合并在一起,并分别加上唯一的对象key值。
这样的好处非常明显:
1. 避免各组件的store数据互相污染。
2. 组件独立维护自己的store,减少维护成本。
非常建议使用这种方式维护store。
8.7 安装使用immutable
在8.5章节,提到了store里不能直接修改state,因为state是引用类型,直接修改可能导致监测不到数据变化。
immutable.js从字面上就可以明白,immutable的意思是“不可改变的”。使用immutable创建的数据是不可改变的,对immutable数据的任何修改都会返回一个新的immutable数据,不会改变原始immutable数据。
immutable.js提供了很多方法,非常方便修改对象或数组类型的引用型数据。
安装immutable和redux-immutable,执行:
yarn add immutable redux-immutable
然后对代码进行改造:
src/store/reducer.js:
- import { combineReducers } from 'redux'+ import { combineReducers } from 'redux-immutable'...(略)
以上代码就是把combineReducers换成redux-immutable里的。
然后修改src/pages/login/store/reducer.js:
import * as constants from './constants'+ import { fromJS } from 'immutable'// 初始默认的stateM const defaultState = fromJS({myLoginData: null,M })+ const getData = (state, action) => {+ return state.set('myLoginData', action.data)+ }const reducer = (state = defaultState, action) => {// 由于state是引用型,不能直接修改,否则是监测不到state发生变化的。因此需要先复制一份进行修改,然后再返回新的state。- // let newState = Object.assign({}, state)switch (action.type) {case constants.SET_DATA:- // newState.myLoginData = action.data- // return newStatereturn getData(state, action)default:return state}}export default reducer
immutable的介入,就是利用fromJS方法,把原始的JS类型转化为immutable类型。
由于state已经是immutable类型了,可以使用immutable的set方法进行数据修改,并返回一个新的state。代码简洁很多,不需要手动通过Object.assign等方法去复制再处理了。
header组件的代码修改同理不再赘述。
immutable还有很多其他非常使用方法,具体请参阅官方文档:
https://immutable-js.com/docs/v4.0.0
8.8 对接react-redux与store
下面来对接react-redux与store,让全部组件都能方便引用store。
修改src/index.js:
import React from 'react'import ReactDOM from 'react-dom'import { ConfigProvider } from 'antd'import zhCN from 'antd/es/locale/zh_CN'import App from './App'+ import { Provider } from 'react-redux'+ import store from './store'// 全局样式import '@/common/stylus/frame.styl'const antdConfig = {locale: zhCN,}ReactDOM.render(<ConfigProvider {...antdConfig}>+ <Provider store={store}><App />+ Provider>ConfigProvider>,document.getElementById('root'))
以上代码就是用react-redux提供的Provider,把store传给了整个App。
在需要使用store的组件中,要使用react-redux提供的connect方法对组件进行包装。
8.9 在login页面设置并实时读取Redux变量
以login为例,修改src/pages/login/index.js:
import { useNavigate } from 'react-router-dom'import { Button, Input } from 'antd'import imgLogo from './logo.png'import Header from '@/components/header'+ import { connect } from 'react-redux'+ import * as actionCreators from './store/actionCreators'import './login.styl'M function Login(props) {+ const { myLoginData, setData } = props// 创建路由钩子const navigate = useNavigate()return ("P-login">title="login"info={() => {console.log('info:login')}}/>"" className="logo" />+"ipt-con">login store: myData = {myLoginData}+"ipt-con">++"ipt-con">"账号" />"ipt-con">"密码" />"ipt-con">type="primary"block={true}onClick={() => {navigate('/home')}}>登录)}+ // 把store中的数据映射到组件的props+ const mapStateToProps = (state) =>{+ return {+ // 数组第一个元素的login,对应的是src/store/reducer.js中定义的login分库名称+ myLoginData: state.getIn(['login', 'myLoginData']),+ }+ }++ // 把store的Dispatch映射到组件的props+ const mapDispatchToProps = (dispatch) => ({+ setData(data) {+ const action = actionCreators.setData(data)+ dispatch(action)+ },+ })- // export default LoginM export default connect(mapStateToProps, mapDispatchToProps)(Login)
关键点说明:
1. 注意代码最后一行,export的数据被connect方法包装了。
2. 通过mapStateToProps和mapDispatchToProps方法,把store里的state和dispatch都映射到了组件的props。这样可以直接通过props进行访问了,store中数据的变化会直接改变props从而触发组件的视图更新。
3. state.getIn()方法是来自于redux-immutable的。
点击按钮后,可以看到页面中显示的myData发生了变化,通过Redux DevTools可进行可视化跟踪查看。
8.10 在header组件实时读取Redux变量
接下来,要实现在header组件中实时读取在login页面设置的myLoginData。
修改src/components/header/index.js:
+ import { connect } from 'react-redux'import './header.styl'function Header(props) {M // 接收来自父组件及Redux的数据M const { title, info, myLoginData } = props// 如果info存在,则执行info()info && info()return (<div className="M-header">Header:{title}+ <span style={{ marginLeft: 20 }}>myLoginData:{myLoginData}span>div>)}+ // 把store中的数据映射到组件的props+ const mapStateToProps = (state) => {+ return {+ // 数组第一个元素的login,对应的是src/store/reducer.js中定义的login分库名称+ myLoginData: state.getIn(['login', 'myLoginData']),+ }+ }- // export default HeaderM export default connect(mapStateToProps, null)(Header)
由于在header中只用到了读取Redux的myLoginData,所以不需要mapDispatchToProps方法了。
这里是通过Redux实时获取的,而非通过父子组件传递方式。因此同样的方式可以在其他页面或者组件中直接使用,无需考虑组件的父子关系。
现在点击“更改login store的myData”,可以发现header组件可以正常实时获取myLoginData了。
在8.6章节中,header组件的store也设置了myHeaderData,实际demo中并没有用到,只是为了演示store分库管理。
8.11 Redux开发小结
上述Redux相关内容较多,跟着操作一遍好像大概知道了,但又说不清为什么使用这些依赖包。这里做一下小结,便于消化理解。
其实react-redux、redux-thunk、immutable都是围绕如何简化redux开发的。
react-redux是为了简化redux通过订阅方式修改state的繁琐过程。
redux-thunk是为了redux的dispatch能够支持function类型的数据,请回顾8.9章节中login页面代码的mapDispatchToProps。
immutable是为了解决store中的数据不能被直接赋值修改的问题(引用类型数据的变化导致无法监测到数据的变化)。
深入学习请参阅官方文档:
redux:
https://redux.js.org/introduction/getting-started
react-redux:
https://www.redux.org.cn/docs/react-redux/
redux-thunk:
https://redux.js.org/usage/writing-logic-thunks
immutable:
https://immutable-js.com/docs/v4.0.0
9 基于axios封装公用API库
为了方便API的维护,把各个API地址和相关方法集中管理是一个很不错的方案。
9.1 安装axios
axios是一款非常流行的API请求工具,先来安装一下。
执行:
yarn add axios
9.2 封装公用API库
直接上代码。
src/api/index.js:
import axios from 'axios'import { createHashHistory } from 'history'import { Modal } from 'antd'let history = createHashHistory()// 配合教程演示组件外路由跳转使用,无实际意义export const goto = (path) => {history.push(path)}// 开发环境地址let API_DOMAIN = '/api/'if (process.env.NODE_ENV === 'production') {// 正式环境地址API_DOMAIN = 'http://xxxxx/api/'}// 用户登录信息在localStorage中存放的名称export const SESSION_LOGIN_INFO = 'loginInfo';// API请求正常,数据正常export const API_CODE = {// API请求正常OK: 200,// API请求正常,数据异常ERR_DATA: 403,// API请求正常,空数据ERR_NO_DATA: 301,// API请求正常,登录异常ERR_LOGOUT: 401,}// API请求异常统一报错提示export const API_FAILED = '网络连接异常,请稍后再试'export const API_LOGOUT = '您的账号已在其他设备登录,请重新登录'export const apiReqs = {// 登录(成功后将登录信息存入localStorage)signIn: (config) => {axios.post(API_DOMAIN + 'login', config.data).then((res) => {let result = res.dataconfig.done && config.done(result)if (result.code === API_CODE.OK) {window.localStorage.setItem(SESSION_LOGIN_INFO,JSON.stringify({uid: result.data.loginUid,nickname: result.data.nickname,token: result.data.token,}))config.success && config.success(result)} else {config.fail && config.fail(result)}}).catch(() => {config.done && config.done()config.fail &&config.fail({message: API_FAILED,})})},// 管登出(登出后将登录信息从localStorage删除)signOut: () => {const { uid, token } = getLocalLoginInfo()let headers = {loginUid: uid,'access-token': token,}let axiosConfig = {method: 'post',url: API_DOMAIN + 'logout',headers,}axios(axiosConfig).then((res) => {logout()}).catch(() => {logout()})},// 获取用户列表getUserList: (config) => {config.method = 'get'config.url = API_DOMAIN + 'user/getUserList'apiRequest(config)},// 修改用户信息modifyUser: (config) => {config.url = API_DOMAIN + 'user/modify'apiRequest(config)},}// 从localStorage获取用户信息export function getLocalLoginInfo() {return JSON.parse(window.localStorage[SESSION_LOGIN_INFO])}// 失效退出界面export function logout() {window.localStorage.removeItem(SESSION_LOGIN_INFO)history.push('/login')}/** API请求封装(带验证信息)* config.history: [必填]用于页面跳转等逻辑* config.method: [必须]请求method* config.url: [必须]请求url* config.data: 请求数据* config.formData: 是否以formData格式提交(用于上传文件)* config.success(res): 请求成功回调* config.fail(err): 请求失败回调* config.done(): 请求结束回调*/export function apiRequest(config) {const loginInfo = JSON.parse(window.localStorage.getItem(SESSION_LOGIN_INFO))if (config.data === undefined) {config.data = {}}config.method = config.method || 'post'// 封装header信息let headers = {loginUid: loginInfo ? loginInfo.uid : null,'access-token': loginInfo ? loginInfo.token : null,}let data = null// 判断是否使用formData方式提交if (config.formData) {headers['Content-Type'] = 'multipart/form-data'data = new FormData()Object.keys(config.data).forEach(function (key) {data.append(key, config.data[key])})} else {data = config.data}// 组装axios数据let axiosConfig = {method: config.method,url: config.url,headers,}// 判断是get还是post,并加入发送的数据if (config.method === 'get') {axiosConfig.params = data} else {axiosConfig.data = data}// 发起请求axios(axiosConfig).then((res) => {let result = res.dataconfig.done && config.done()if (result.code === API_CODE.ERR_LOGOUT) {// 如果是登录信息失效,则弹出Antd的Modal对话框Modal.error({title: result.message,// 点击OK按钮后,直接跳转至登录界面onOk: () => {logout()},})} else {// 如果登录信息正常,则执行success的回调config.success && config.success(result)}}).catch((err) => {// 如果接口不通或出现错误,则弹出Antd的Modal对话框Modal.error({title: API_FAILED,})// 执行fail的回调config.fail && config.fail()// 执行done的回调config.done && config.done()})}
代码比较多,必要的备注都写了,不再赘述。
这里主要实现了以下几方面:
1. 通过apiReqs把项目所有api进行统一管理。
2. 通过apiRequest方法,实现了统一的token验证、登录状态失效报错以及请求错误报错等业务逻辑。
为什么signIn和signOut方法没有像getUserList和modifyUser一样调用apiRequest呢?
因为signIn和signOut的逻辑比较特殊,signIn并没有读取localStorage,而signOut需要清除localStorage,这两个逻辑是与其他API不同的,所以单独实现了。
9.3 Mock.js安装与使用
在开发过程中,为了方便前端独自调试接口,经常使用Mock.js拦截Ajax请求,并返回预置好的数据。本小节介绍下如何在react项目中使用Mock.js。
执行安装:
yarn add mockjs
在src下新建mock.js,代码如下:
import Mock from 'mockjs'const domain = '/api/'// 模拟login接口Mock.mock(domain + 'login', function () {let result = {code: 200,message: 'OK',data: {loginUid: 10000,nickname: '兔子先生',token: 'yyds2022'}}return result})
然后在src/index.js中引入mock.js:
import React from 'react'import ReactDOM from 'react-dom'import { ConfigProvider } from 'antd'import zhCN from 'antd/es/locale/zh_CN'import App from './App'import { Provider } from 'react-redux'import store from './store'+ import './mock'...(略)
如此简单。这样,在项目中请求/api/login的时候,就会被Mock.js拦截,并返回mock.js中模拟好的数据。
9.4 发起API请求
继续完善login页面,实现一个API请求。
src/pages/login/index.js:
+ import { useState } from 'react'import { useNavigate } from 'react-router-dom'import { Button, Input } from 'antd'import imgLogo from './logo.png'import Header from '@/components/header'import { connect } from 'react-redux'import * as actionCreators from './store/actionCreators'+ import { apiReqs } from '@/api'import './login.styl'function Login(props) {const { myLoginData, setData } = props// 创建路由钩子const navigate = useNavigate+ // 组件中自维护的实时数据+ const [account, setAccount] = useState('')+ const [password, setPassword] = useState('')// 登录+ const login = () => {+ apiReqs.signIn({+ data: {+ account,+ password+ },+ success: (res) => {+ console.log(res)+ navigate('/home')+ }+ })+ }return (...(略)M {setAccount(e.target.value)}} />M{setPassword(e.target.value)}} /> M...(略)
在login页面点击“登录”按钮,页面正常跳转。
在Console中可以看到mockjs返回的模拟请求数据。
但是在Network的Fetch/XHR里是看不到任何发起的请求的。因为请求被mockjs拦截了,实际上并没有发出真正的请求。
9.5 设置开发环境的反向代理请求
在react开发环境中,发起真正的情况通常会遇到跨域问题。比如,默认情况下当前demo项目执行yarn start后会运行在http://localhost:3000。本地在http://localhost启动了一个API后端服务。由于端口不一致,请求会存在跨域问题。可以借助http-proxy-middleware工具实现反向代理。
执行安装:
yarn add http-proxy-middleware --dev
在src下创建setupProxy.js,代码如下:
/*** 反向代理配置*/const { createProxyMiddleware } = require('http-proxy-middleware');module.exports = function (app) {app.use(// 开发环境API路径匹配规则'^/api',createProxyMiddleware({// 要代理的真实接口API域名target: 'http://localhost',changeOrigin: true}))}
这代码的意思就是,只要请求地址是以"/api"开头,那就反向代理到http://localhost域名下,跨域问题解决!大家可以根据实际需求进行修改。
一定记得要把mockjs注释掉,否则会被拦截的。
修改src/index.js:
- import './mock'※注:setupProxy.js设置后,一定要重启项目才生效。
通过Console可以看到后端API返回的真实数据,在Network里也可以看到发起的请求了。
10 build项目
在build前还需要做一步配置,否则build版本网页中的文件引用都是绝对路径,运行后是空白页面。
修改package.json:
"name": "react-app5","version": "0.1.0","private": true,+ "homepage": "./",...(略)
执行:
yarn build
生成的文件在项目根目录的build目录中,打开index.html即可看到正常运行的项目。
11 项目Git源码
本项目已上传至Gitee和GitHub,方便各位下载。
Gitee:
https://gitee.com/betaq/react-app5
GitHub:
https://github.com/Yuezi32/react-app5
结束语
以上就是本次React全家桶教程的全部内容。篇幅较长,确实是花了很长时间精心整理、反复验证、句句斟酌的完整教程,希望能够帮助到你。更多精彩详实的开发教程,欢迎阅读我的微信公众号「卧梅又闻花」。
推荐阅读
2022新春版:React+Antd开发Chrome插件教程(Manifest V3)上篇
2022新春版:React+Antd开发Chrome插件教程(Manifest V3)下篇
2022新春版:手把手教你搭建Electron17+React17+Antd架构工程
欢迎关注我的微信公众号「卧梅又闻花」,随时获取最新文章。









