2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

2023-05-25 0 850

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

React是近年来前端开发领域非常热门的技术框架,其背景是Facebook团队的技术支持,在全球的前端开发市场上占有率很高。结合React丰富的社区资源,可以让项目开发如虎添翼。虽然React的学习门槛要比Vue略高,但学习React对于大幅提高前端技能是非常有帮助的。本文基于Create-React-App(后简称CRA),详细梳理了从创建React工程、精简项目、配置调试环境,到集成路由、Ant Design、Redux等各种相关工具等内容,重在快速搭建标准React项目工程,对于React初学者来说,能够节省很多探索的时间。

在2022年2月,我发布了《2022新春版:超全面详细一条龙教程!从零搭建React项目全家桶》。不到一年的时间里,技术迭代了很多:React、react-router-dom、Redux、Ant Design等关键的开发利器都有重大的更新,用法也发生了较大变化,因此再更新一版。每期教程都会使用当前最新的技术版本,让你快速跟上前沿步伐。

先睹为快

先看下目录了解本教程都有哪些内容。

1 初始化项目 • 1.1 使用create-react-app新建项目 • 1.2 精简项目 2 Webpack配置 • 2.1 配置国内镜像源 • 2.2 暴露Webpack • 2.3 支持Sass/Scss • 2.4 支持Less • 2.5 支持Stylus • 2.6 设置路径别名 • 2.7 禁止build项目生成map文件 3 项目架构搭建 • 3.1 项目目录结构设计 • 3.2 关于样式命名规范 • 3.3 设置全局公用样式 4 引入Ant Design 5.x • 4.1 安装Ant Design • 4.2 设置Antd为中文语言 5 页面开发 • 5.1 构建Login页面 • 5.2 构建Home页面 • 5.3 构建Account页面 • 5.4 通过一级路由实现页面跳转 • 5.5 在React组件中实现页面路由跳转 • 5.6 在非React组件中实现页面路由跳转 6 组件开发 • 6.1 创建自定义SVG图标Icon组件 • 6.2 创建Header组件 • 6.3 引入Header组件 • 6.4 组件传参 7 二级路由配置 • 7. 9 Redux及Redux Toolkit • 9.1 安装Redux及Redux Toolkit • 9.2 创建全局配置文件 • 9.3 创建用于主题换肤的store分库 • 9.4 创建store总库 • 9.5 引入store到项目 • 9.6 store的使用:实现亮色/暗色主题切换 • 9.7 非Ant Design组件的主题换肤 • 9.8 store的使用:实现主题色切换 • 9.9 安装Redux调试浏览器插件 10 基于axios封装公用API库 • 10.1 安装axios • 10.2 封装公用API库 • 10.3 Mock.js安装与使用 • 10.4 发起API请求:实现登录功能 11 一些细节问题 • 11.1 解决Modal.method跟随主题换肤的问题 • 11.2 路由守卫 • 11.3 设置开发环境的反向代理请求 12 build项目 13 项目Git源码 结束语

本次分享Demo的主要依赖包版本:

Node.js 18.12.1

create-react-app 5.0.1

react 18.2.0

react-router-dom 6.4.5

antd 5.0.6

node-sass 8.0.0

sass-loader 12.3.0

less 4.1.3

less-loader 11.1.0

stylus 0.59.0

stylus-loader 7.1.0

axios 1.2.1

history 4.10.1

mockjs 1.1.0

react-redux 8.0.5

@reduxjs/toolkit 1.9.1

http-proxy-middleware 2.0.6 ※注:

代码区域每行开头的:

“+” 表示新增

“-” 表示删除

“M” 表示修改

即便你是新手,跟着操作一遍,也可以快速上手React项目啦!下面请跟着新版教程一步步操作。

1 初始化项目

1.1 使用create-react-app新建项目

找个合适的目录,执行:

npx create-react-app react-app

命令最后的react-app是项目的名称,可以自行更改。

编写教程时,create-react-app已经发布了5.0.1,如果一直报错:

you are running create-react-app 4.0.3 which is behind the latest release (5.0.1)

说明你还在使用旧版本的create-react-app,需要先清除npx缓存,执行:

npx clear-npx-cache

然后再执行之前的命令创建项目:

npx create-react-app react-app

稍等片刻即可完成安装。安装完成后,可以使用npm或者yarn启动项目。

进入项目目录,并启动项目:

cd react-app yarn start (或者使用npm start)

如果没有安装yarn,可执行以下命令全局安装:

npm install –global yarn
yarn中文网站: https://yarn.bootcss.com/

启动后,可以通过以下地址访问项目:

http://localhost:3000/
2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

1.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-lock.json ├─ package.json └─ README.md

现在目录结构如下,清爽许多:

├─ /node_modules ├─ /public | ├─ favicon.ico | └─ index.html ├─ /src | ├─ App.js | └─ index.js ├─ .gitignore ├─ package-lock.json ├─ package.json └─ README.md

以上文件删除后,页面会报错。这是因为相应的文件引用已不存在。需要继续修改代码,先让项目正常运行起来。

逐个修改以下文件,最终精简代码依次如下:

src/App.js:

function App() { return <div className=“App”>ReactApp</div> } export default App

src/index.js:

import React from react import ReactDOM from react-dom/client import App from ./App const root = ReactDOM.createRoot(document.getElementById(root)) root.render(<App />)

public/index.html:

<!DOCTYPE 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 App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id=“root”></div> </body> </html>

运行效果如下:

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

2 Webpack配置

2.1 配置国内镜像源

npm和yarn默认是从国外源站拉取依赖包的,为提高下载速度和稳定性,建议配置为国内镜像源。

yarn registry国内镜像:

yarn config set registry https://registry.npmmirror.com

npm registry国内镜像:

npm config set registry https://registry.npmmirror.com

yarn node-sass国内镜像:

yarn config set SASS_BINARY_SITE https://npmmirror.com/mirrors/node-sass/

npm node-sass国内镜像:

npm config set SASS_BINARY_SITE https://npmmirror.com/mirrors/node-sass/
据淘宝官方声明,原先的 http://npm.taobao.orghttp://registry.npm.taobao.org

域名于2022年5月31日零时起停止服务。新域名如下:

【Web 站点】https://npmmirror.com【Registry Endpoint】https://registry.npmmirror.com

官方公告原文:

《【望周知】淘宝 NPM 镜像站喊你切换新域名啦》(https://zhuanlan.zhihu.com/p/430580607)

如果不清楚本地当前yarn或者npm的配置,可以执行以下命令查看:

yarn查看方法:

yarn config list

npm查看方法:

npm config list

2.2 暴露Webpack

create-react-app默认情况下未暴露配置文件。如果要更灵活地配置项目,需要将配置文件暴露出来。

执行以下命令,暴露配置文件:

yarn eject

eject之前必须确保当前工程所有文件已提交git,否则会报以下错误:

Remove untracked files, stash or commit any changes, and try again.

需要先在项目根目录下执行提交git:

git add . git commit -m “初始化项目(备注)”

然后再执行:

yarn eject

即可完成Webpack的暴露,这时项目里会多出来两个目录和若干个文件。具体变化如下:

+ ├─ /config ├─ /node_modules ├─ /public + ├─ /scripts ├─ /src ├─ .gitignore M ├─ package-lock.json M ├─ package.json └─ README.md

2.3 支持Sass/Scss

eject后,虽然package.json以及webpack.config.js里有了sass相关代码,但是要正确使用Sass/Scss,还要再安装node-sass。

执行以下命令:

yarn add node-sass –dev

安装完成后,项目已支持Sass/Scss。

2.4 支持Less

支持Less稍微多一点步骤,首先安装less和less-loader:

yarn add less less-loader –dev

然后修改config/webpack.config.js:

// style files regexes const 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 : isEnvDevelopment, modules: { mode: icss, }, }, sass-loader ), // Dont 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/6571 sideEffects: 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 : isEnvDevelopment, modules: { mode: local, 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.5 支持Stylus

支持Stylus跟Less完全一样,首先安装stylus和stylus-loader:

执行以下命令:

yarn add stylus stylus-loader –dev

安装完成后,按照上一小节介绍的支持Less的方法,修改config/webpack.config.js:

// style files regexes const 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.6 设置路径别名

为了避免使用相对路径的麻烦,可以设置路径别名。

修改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.7 禁止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 项目目录结构设计

项目目录结构可根据项目实际灵活制定。这里分享下我常用的结构,主要分为公用模块目录、组件模块目录、页面模块目录、路由配置目录、Redux目录等几个部分,让项目结构更加清晰合理。

├─ /config <– webpack配置目录 ├─ /node_modules ├─ /public | ├─ favicon.ico <– 网页图标 | └─ index.html <– HTML页模板 ├─ /scripts <– node编译脚本 ├─ /src | ├─ /api <– api目录 | | └─ index.js <– api库 | ├─ /common <– 全局公用目录 | | ├─ /fonts <– 字体文件目录 | | ├─ /images <– 图片文件目录 | | ├─ /js <– 公用js文件目录 | | └─ /styles <– 公用样式文件目录 | | | ├─ frame.styl <– 全部公用样式(import本目录其他全部styl) | | | ├─ reset.styl <– 清零样式 | | | └─ 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样式文件 | | └─ … <– 其他页面 | ├─ /route <– 路由配置目录 | ├─ /store <– Redux配置目录 | ├─ globalConfig.js <– 全局配置文件 | ├─ index.js <– 项目入口文件 | ├─.gitignore | ├─ package.json | ├─ README.md | └─ yarn.lock

注意以上项目结构,已经没有src/App.js了,现在先不用删除,随着后续章节的讲解再删除。

接下来,就按照上面的目录结构设计开始构建项目。

3.2 关于样式命名规范

以我多年来的开发经验来讲,合理的样式命名规范对项目开发有很大的帮助,主要体现在以下方面:

(1)避免因样式名重复导致的污染。

(2)从命名上可直观区分“组件样式”、“页面样式”(用于给在此页面的组件样式做定制调整)、“全局样式”。

(3)快速定位模块,便于查找问题。

分享一下本教程的样式命名规范:

G-xx: 表示全局样式,用来定义公用样式。

P-xx: 表示页面样式,用来设置页面的背景色、尺寸、定制化调整在此页面的组件样式。

M-xx: 表示组件样式,专注组件本身样式。

后续教程中,可以具体看到以上规范是如何应用的。

3.3 设置全局公用样式

我个人比较喜欢Stylus简洁的语法,因此本教程以Stylus作为css预处理语言。各位可以根据自己的习惯,自由选择Sass/Scss、Less、Stylus。

新建清零样式文件,src/common/styles/reset.styl。

由于reset.css代码较多,这里不再放出。非常推荐参考这个reset css,代码比较全面,更新也比较及时(截至本文写作时,是2022年8月7日更新的)。

具体代码详见:https://github.com/elad2412/the-new-css-reset/blob/main/css/reset.css

新建全局样式文件,src/common/styles/global.styl:

html, body, #root height: 100% /*清浮动*/ .clearfix:after content: “.” display: block height: 0 clear: both visibility: hidden .clearfix display:block

全局样式将应用于项目的所有页面,可根据需要自行补充或调整。

新建全局样式总入口文件,src/common/styles/frame.styl:

@import ./reset.styl; @import ./global.styl;

在frame.styl里引入其他公用样式,就方便一次性全部应用到项目中了。

然后在src/index.js里引入frame.styl:

import React from react import ReactDOM from react-dom/client import App from ./App + // 全局样式 + import @/common/styles/frame.styl const root = ReactDOM.createRoot(document.getElementById(root)) root.render(<App />)

这样在所有页面里就可以直接使用全局样式了。

现在运行项目,可以发现reset、global中的样式已经生效。

4 引入Ant Design 5.x

Ant Design是一款非常优秀的UI库,在React项目开发中使用非常广泛。Ant Design发布5.x后,使用起来更加快捷,而且在主题换肤方面更加便捷。本次分享也特别说明下如何使用Ant Design(以下简称Antd)。

4.1 安装Ant Design

执行:

yarn add antd

然后修改src/App.js 来验证下Antd:

import { Button } from antd function App() { return ( <div className=“App”> <h1>ReactApp</h1> <Button type=“primary”>Button</Button> </div> ) } export default App

执行yarn start:

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

可以看到Antd的Button组件正常显示出来了。

※注: Antd 5.x已经没有全局污染的reset样式了。因此不用再担心使用了Antd会影响页面样式。

4.2 设置Antd为中文语言

Antd默认语言是英文,需进行以下设置调整为中文。

修改src/index.js:

import React from react import ReactDOM from react-dom/client import App from ./App + import { ConfigProvider } from antd + // 引入Ant Design中文语言包 + import zhCN from antd/locale/zh_CN // 全局样式 import @/common/styles/frame.styl const root = ReactDOM.createRoot(document.getElementById(root)) M root.render( M <ConfigProvider locale={zhCN}> M <App /> M </ConfigProvider> M )

现在还没开始构建页面,因此关于Antd 5.x酷炫的主题换肤在后续章节再讲解,先别着急。

5 页面开发

本次教程包含Login、Home、Account三个业务页面和一个二级路由页面Entry。其中:

Login页面不换肤,不需要验证登录状态。

Home页面和Account页面,跟随换肤,并通过Entry进行登录状态验证及路由切换。

工程文件变动如下:

├─ /config ├─ /node_modules ├─ /public ├─ /scripts ├─ /src | ├─ /api | ├─ /common | ├─ /components + | ├─ /pages + | | ├─ /account + | | | ├─ index.js + | | | └─ account.styl + | | ├─ /entry + | | | ├─ index.js + | | | └─ entry.styl + | | ├─ /home + | | | ├─ index.js + | | | └─ home.styl + | | ├─ /login + | | | ├─ index.js + | | | ├─ 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-login position: absolute top: 0 bottom: 0 width: 100% background: #7adbcb .logo display: block margin: 50px auto 20px .ipt-con margin: 0 auto 20px width: 400px text-align: center

别忘了还有一张图片:src/pages/login/logo.png。

暂时修改下入口文件代码,把原App页面换成Login页面,看看效果:

修改src/index.js:

import App from ./App + import App from @/pages/login
2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

5.2 构建Home页面

直接上代码。

新建src/pages/home/index.js:

import { Button } from antd import ./home.styl function Home() { return ( <div className=“P-home”> <h1>Home Page</h1> <div className=“ipt-con”> <Button type=“primary”>返回登录</Button> </div> </div> ) } export default Home

新建src/pages/home/home.styl:

.P-home position: absolute top: 0 bottom: 0 width: 100% background: linear-gradient(#f48c8d,#f4c58d) h1 margin-top: 50px text-align: center color: #fff font-size: 40px .ipt-con margin: 20px auto 0 text-align: center

暂时修改下入口文件代码,把初始页面换成Home页面。

修改src/index.js:

import App from @/pages/login + import App from @/pages/home

看看效果:

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

5.3 构建Account页面

基本与Home页面一样,直接上代码。

新建src/pages/account/index.js:

import { Button } from antd import ./account.styl function Account() { return ( <div className=“P-account”> <h1>Account Page</h1> <div className=“ipt-con”> <Button type=“primary”>返回登录</Button> </div> </div> ) } export default Account

新建src/pages/account/account.styl:

.P-account position: absolute top: 0 bottom: 0 width: 100% background: linear-gradient(#f48c8d,#f4c58d) h1 margin-top: 50px text-align: center color: #fff font-size: 40px .ipt-con margin: 20px auto 0 text-align: center

同样,暂时修改下入口文件代码,把初始页面换成Account页面。

src/index.js:

import App from @/pages/home + import App from @/pages/account

看看效果:

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

5.4 通过一级路由实现页面跳转

为了实现页面的跳转,需要安装react-router-dom。

执行:

yarn add react-router-dom

接下来进行路由配置,新建src/router/index.js:

import { createHashRouter, Navigate } from react-router-dom import Login from @/pages/login import Home from @/pages/home import Account from @/pages/account // 全局路由 export const globalRouters = createHashRouter([ // 对精确匹配”/login”,跳转Login页面 { path: /login, element: <Login />, }, // 精确匹配”/home”,跳转Home页面 { path: /home, element: <Home />, }, // 精确匹配”/account”,跳转Account页面 { path: /account, element: <Account />, }, // 如果URL没有”#路由”,跳转Home页面 { path: /, element: <Home />, }, // 未匹配,,跳转Login页面 { path: *, element: <Navigate to=“/login” />, }, ])

为循序渐进讲解,暂时先将Login、Home、Account都当做一级页面,通过一级路由实现跳转。代码注释已写明跳转逻辑,不再赘述。

接下来应用以上路由配置,修改src/index.js:

import React from react import ReactDOM from react-dom/client + import { RouterProvider } from react-router-dom + import { globalRouters } from @/router import App from @/pages/account import { ConfigProvider } from antd // 引入Ant Design中文语言包 import zhCN from antd/locale/zh_CN // 全局样式 import @/common/styles/frame.styl const root = ReactDOM.createRoot(document.getElementById(root)) root.render( <ConfigProvider locale={zhCN}> <App /> + <RouterProvider router={globalRouters} /> </ConfigProvider> )

这里使用了<RouterProvider>实现路由跳转。同时,为了减少项目文件的依赖层级深度,也删除了<App>,从此与App.js文件告别了。

记得删掉src/App.js。

执行yarn start启动项目,输入对应的路由地址,可以正常显示对应的页面了。

Login页面: http://localhost:3000/#/login

Home页面: http://localhost:3000/#/home

Account页面: http://localhost:3000/#/account

5.5 在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 ( <div className=“ipt-con”> M <Button type=“primary” block={true} onClick={()=>{navigate(/home)}}>登录</Button> </div>

同样的方法,再来实现点击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 Page</h1> <div className=“ipt-con”> M <Button type=“primary” onClick={()=>{navigate(/login)}}>返回登录</Button> </div> </div> ) } export default Home

Account页面同理,不再赘述。现在,点击按钮进行页面路由跳转已经实现了。

至于Home与Account页面之间的互相跳转,大家可以使用navigate()举一反三自行实现。

5.6 在非React组件中实现页面路由跳转

在实际项目中,经常需要在非React组件中进行页面跳转。比如,当进行API请求的时候,如果发现登录认证已失效,就直接跳转至Login页面。

针对这种情况的统一处理,当然是封装成公用模块最合适。但往往这些纯功能性的模块都不是React组件,而是纯原生js。所以就没办法使用useNavigate()了。

下面介绍一下如何在非React组件中进行页面路由跳转。

需要安装额外的history依赖包。截至本文编写时,history最新版本为5.3.0,但history.push()只改变了页面地址栏的地址,却没有进行实际的跳转。在GitHub上也有很多人反馈,应该是最新版本的bug。目前的解决办法是安装4.10.1版本。

执行:

yarn add history@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 Page</h1> + <div className=“ipt-con”> + <Button onClick={()=>{goto(/login)}}>组件外跳转</Button> + </div> <div className=“ipt-con”> <Button type=“primary” onClick={()=>{navigate(/login)}}>返回登录</Button> </div> </div> ) } export default Home

在home页点击“组件外跳转”按钮,可以正常跳转至login页面了,而实际执行跳转的代码是在src/api/index.js(非React组件)中,这样就非常适合封装统一的处理逻辑。

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

后续章节会讲述如何封装API接口,并通过组件外路由的方式实现API调用失败时的统一跳转。

6 组件开发

为了配合后续章节介绍二级路由和主题换肤,构建一个公用的头部组件。

6.1 创建自定义SVG图标Icon组件

Antd自带了很多Icon,非常方便直接使用。但在项目中遇到Antd没有的图标怎么办?当然,前提要求是自己构建的图标也能支持随时改变颜色和大小等样式。

例如针对切换亮色/暗色主题功能,Antd没有提供“太阳”“月亮”“主题色”的Icon。

第一个方法是在iconfont网站(https://www.iconfont.cn/)上制作自己的iconfont字体,然后以字体文件的方式应用到项目中。这种方式相信从事前端开发的同学都很熟悉了,不再赘述。这种方式相对来说比较麻烦,每次图标有变动时,都要重新生成一遍,而且遇到iconfont网站打不开等突发情况时,只能干着急。不是很推荐。

这里推荐第二个方法,就是基于Antd的Icon组件制作本地的自定义图标,而且用起来跟Antd自带的Icon是一样的,也不用额外考虑换肤的问题。虽然Antd官网介绍了制作方法,但讲解得不够具体。

Ant Design官方说明: https://ant-design.antgroup.com/components/icon-cn#自定义-icon

下面具体分享一下这种高效的方案。

第一步:创建自定义图标库

新建src/components/extraIcons/index.js:

import Icon from @ant-design/icons const SunSvg = () => ( // 这里粘贴“太阳”图标的SVG代码) const MoonSvg = () => ( // 这里粘贴“月亮”图标的SVG代码 ) const ThemeSvg = () => ( // 这里粘贴“主题色”图标的SVG代码 ) export const SunOutlined = (props) => <Icon component={SunSvg} {…props} /> export const MoonOutlined = (props) => <Icon component={MoonSvg} {…props} /> export const ThemeOutlined = (props) => <Icon component={ThemeSvg} {…props} />

第二步:在iconfont网站(https://www.iconfont.cn/)找到心仪的图片,然后点击按钮。

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

第三步:在弹出的图标详情弹层里,点击“复制SVG代码”。

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

第四步:将选好的SVG代码依次粘贴到src/components/extraIcons/index.js中对应的位置。

※注:一定要仔细坚持以下三方面。 检查svg代码中是否有class以及与颜色相关的fill、stroke等属性,如有,必须连带属性一起删除。

确保标签中有fill=”currentColor”,否则图标的颜色将不能改变。

确保标签中width和height属性的值为1em,否则图标的大小将不能改变。

这里以“太阳”图标为例:

const SunSvg = () => ( // 这里粘贴“太阳”图标的SVG代码 <svg t=“1670490651290” class=“icon” viewBox=“0 0 1024 1024” version=“1.1” xmlns=“http://www.w3.org/2000/svg” pid=“1344” width=“400” + width=“1em” height=“400” + height=“1em” + fill=“currentColor” > <path d=“…(略)” pid=“1345” ></path> </svg> )

SVG代码太长了,这里就不全部贴出来了。

这样,自定义Icon就制作好了。使用方法在下一小节介绍。

6.2 创建Header组件

新建src/components/header/index.js:

import { Button, Card } from antd import { MoonOutlined, ThemeOutlined } from @/components/extraIcons import ./header.styl function Header() { return ( <Card className=“M-header”> <div className=“header-wrapper”> <div className=“logo-con”>Header</div> <div className=“opt-con”> <Button icon={<MoonOutlined />} shape=“circle”></Button> <Button icon={<ThemeOutlined />} shape=“circle”></Button> </div> </div> </Card> ) } export default Header

新建src/components/header/header.styl:

.M-header position: relative z-index: 999 border-radius: 0 overflow hidden .ant-card-body padding: 16px 24px height: 62px line-height: 32px .header-wrapper display: flex .logo-con display: flex font-size: 30px font-weight: bold .opt-con display: flex flex: 1 justify-content: flex-end gap: 20px

简单说明一下:

使用Antd的<Card>组件,是为了跟随主题换肤,否则Header的背景色、边框色、文字色等元素的换肤都要单独实现。<Card>组件默认是圆角的,这里通过CSS将其还原成直角。当然你也可以使用Antd提供的SeedToken来对特定组件实现圆角,但不如CSS直接来得痛快。

6.3 引入Header组件

在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 Page</h1>

同样,在Account页面也引入Header组件。

修改src/pages/account/index.js:

import { Button } from antd + import Header from @/components/header import ./account.styl function Account() { return ( <div className=“P-account”> + <Header /> <h1>Account Page</h1>

运行效果如下:

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

6.4 组件传参

使用过Vue的同学都知道,Vue组件有data和props。

data是组件内的数据;

props用来接收父组件传递来的数据。

在React中,如果使用的是Class方式定义的组件:

state是组件内的数据;

props用来接收父组件传递来的数据。

如果使用的是function方式定义的组件(也叫“无状态组件”或“函数式组件”):

使用useState()管理组件内的数据(hook);

使用props接收父组件传递来的数据。

Class组件有明确的声明周期管理,但是代码相对来说不如无状态组件简洁优雅。

无状态组件通过hook管理声明周期,效率更高。因此本教程全程使用无状态组件讲解。

下面简单演示下如何实现向子组件传递数据。

通过Home和Account分别向Header组件传递不同的值,并显示在Header组件中。

修改src/pages/home/index.js:

M <Header title=“home” info={()=>{console.log(info:home)}} />

修改src/pages/account/index.js:

M <Header title=“account” info={()=>{console.log(info:account)}} />

修改src/components/header/index.js:

M function Header(props) { + // 接收来自父组件的数据 + const { title, info } = props + // 如果info存在,则执行info()+ info && info() return ( <Card className=“M-header”> <div className=“header-wrapper”> M <div className=“logo-con”>Header:{title}</div>

运行看下已经生效。

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

7 二级路由配置

在第6章节中,将Header组件分别导入到Home和Account页面,这显然是一种非常低效的方式。如果有N个页面,那要引入N多次。结合这个问题,下面来讲解如何通过二级路由来解决这个问题。

7.1 创建二级路由的框架页面

新建src/pages/entry/index.js:

import { Outlet } from react-router-dom import Header from @/components/header import ./entry.styl function Entry() { return ( <div className=“M-entry”> <Header /> <div className=“main-container”> <Outlet /> </div> </div> ) } export default Entry

新建src/pages/entry/entry.styl:

.M-entry display: flex flex-direction: column height: 100% .main-container position: relative flex: 1

这里的<Outlet>就是为二级路由页面挖好的“坑”,Entry下的路由页面会放到<Outlet>位置,而Header组件则是一次性引入,非常方便。

然后把Home和Account页面中的Header组件删掉。否则会与Entry里的Header组件重复出现。

修改src/pages/home.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 title=“home” info={()=>{console.log(info:home)}} /> <h1>Home Page</h1>

同样,修改src/pages/account.js:

import { Button } from antd import Header from @/components/header import ./account.styl function Account() { return ( <div className=“P-account”> + <Header title=“account” info={()=>{console.log(info:account)}} /> <h1>Account Page</h1>

7.2 配置二级路由

修改src/router/index.js:

import { createHashRouter, Navigate } from react-router-dom import Login from @/pages/login import Home from @/pages/home import Account from @/pages/account // 引入Entry框架页面 import Entry from @/pages/entry // 全局路由 export const globalRouters = createHashRouter([ // 对精确匹配”/login”,跳转Login页面 { path: /login, element: <Login />, }, { // 未匹配”/login”,全部进入到entry路由 path: /, element: ( <Entry /> ), // 定义entry二级路由 children: [ { // 精确匹配”/home”,跳转Home页面 path: /home, element: <Home />, }, { // 精确匹配”/account”,跳转Account页面 path: /account, element: <Account />, }, { // 如果URL没有”#路由”,跳转Home页面 path: /, element: <Navigate to=“/home” />, }, { // 未匹配,,跳转Login页面 path: *, element: <Navigate to=“/login” />, }, ], }, ])

由于代码变动较多,这里就不采用代码对比的方式了,直接放出最终代码。新变化的地方就是引入了Entry页面,并且把除Login以外的页面,全都放到Entry的二级路由(children)里。也就是说,改造后,一级路由只有Login和Entry两个页面。

改造后,各页面的访问地址还是保持不变:

Login页面: http://localhost:3000/#/login

Home页面: http://localhost:3000/#/home

Account页面: http://localhost:3000/#/account

运行效果如下:

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

改造后,Header组件的传参不见了。这是因为把Header放到Entry页面后,需要根据当前路由来判断处于哪个页面,再传给Header。接下来就介绍下如何解决这个问题。

使用react-router-dom提供的useLocation方法,可以很方便地获得当前路由地址。

修改src/pages/entry/index.js:

M import { Outlet, useLocation } from react-router-dom import Header from @/components/header import ./entry.styl function Entry() { + // 获得路由钩子 + const location = useLocation() return ( <div className=“M-entry”> M <Header title={location.pathname} /> <div className=“main-container”> <Outlet /> </div> </div> ) } export default Entry

运行效果如下:

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

使用useLocation方法,可以很方便实现页面位置导航及当前页面状态显示等交互需求,非常适合与Antd的Menu导航菜单组件、Breadcrumb面包屑组件搭配使用。

8 React Developer Tools浏览器插件

为了更方便调试React项目,建议安装Chrome插件。

先科学上网,在Chrome网上应用店里搜索“React Developer Tools”并安装。

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

安装完成后,打开Chrome DevTools,点击Components按钮,可以清晰的看到React项目代码结构以及各种传参。

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

9 Redux及Redux Toolkit

Redux是用来做什么的?简单通俗的解释,Redux是用来管理项目级别的全局变量,而且是可以实时监听变化并改变DOM的。当多个模块都需要动态显示同一个数据,并且这些模块从属于不同的父组件,或者在不同的页面中,如果没有Redux,那实现起来就很麻烦了,问题追踪也很痛苦。Redux就是解决这个问题的。

做过Vue开发的同学都知道Vuex,React对应的工具就是Redux。在以前,在React中使用Redux还需要redux-thunk、immutable等插件,逻辑非常麻烦,也很难理解。现在官方推出了Redux Toolkit,一个开箱即用的高效的Redux开发工具集,不需要依赖第三方插件了,使用起来也很简洁。

9.1 安装Redux及Redux Toolkit

执行:

yarn add @reduxjs/toolkit react-redux

9.2 创建全局配置文件

新建src/globalConfig.js:

/** * 全局配置 */ export const globalConfig = { // 初始主题(localStorage未设定的情况) initTheme: { // 初始为亮色主题 dark: false, // 初始主题色 // 与customColorPrimarys数组中的某个值对应 // null表示默认使用Ant Design默认主题色或customColorPrimarys第一种主题色方案 colorPrimary: null, }, // 供用户选择的主题色,如不提供该功能,则设为空数组 customColorPrimarys: [ #1677ff, #f5222d, #fa8c16, #722ed1, #13c2c2, #52c41a, ], // localStroge用户主题信息标识 SESSION_LOGIN_THEME: userTheme, // localStroge用户登录信息标识 SESSION_LOGIN_INFO: userLoginInfo, }

globalConfig其实与Redux没有太深入的关系,只是为了方便配置一些初始化默认值而已,以及定义localStorage的变量名,这么做就是为了把配置项都抽出来方便维护。

9.3 创建用于主题换肤的store分库

为了便于讲解,先创建分库。按照官方的概念,分库叫做slice。可以为不同的业务创建多个slice,便于独立维护。这里结合主题换肤功能,创建对应的分库。

新建store/slices/theme.js:

import { createSlice } from @reduxjs/toolkit import { globalConfig } from @/globalConfig const sessionTheme = JSON.parse(window.localStorage.getItem(globalConfig.SESSION_LOGIN_THEME)) // 如果localStorage里没有主题配置,则使用globalConfig里的初始化配置const initTheme = sessionTheme?sessionTheme: globalConfig.initTheme //该store分库的初始值const initialState = { dark: initTheme.dark, colorPrimary: initTheme.colorPrimary } export const themeSlice = createSlice({ // store分库名称 name: theme, // store分库初始值 initialState, reducers: { // redux方法:设置亮色/暗色主题 setDark: (state, action) => { // 修改了store分库里dark的值(用于让全项目动态生效) state.dark = action.payload // 更新localStorage的主题配置(用于长久保存主题配置) window.localStorage.setItem(globalConfig.SESSION_LOGIN_THEME, JSON.stringify(state)) }, // redux方法:设置主题色 setColorPrimary: (state, action) => { // 修改了store分库里colorPrimary的值(用于让全项目动态生效) state.colorPrimary = action.payload // 更新localStorage的主题配置(用于长久保存主题配置) window.localStorage.setItem(globalConfig.SESSION_LOGIN_THEME, JSON.stringify(state)) }, }, }) // 将setDark和setColorPrimary方法抛出 export const { setDark } = themeSlice.actions export const { setColorPrimary } = themeSlice.actions export default themeSlice.reducer

再啰嗦一下这部分的关键逻辑:

e没有主题配置,则从globalConfig读取默认值,然后再写入localStorage。这种情况一般是用户使用当前浏览器第一次浏览该项目时会用到。setDark用来设置“亮色/暗色主题”,setColorPrimary用来设置“主题色”。每次设置后,除了变更store里的值(为了项目全局动态及时生效),还要同步写入localStorage(为了刷新或重新打开时及时生效)。“亮色/暗色主题”和“主题色”虽然都是颜色改变,但是完全不同的两个维度的换肤。“亮色/暗色主题”主要是对默认的文字、背景、边框等基础元素进行黑白切换,而“主题色”则是对带有“品牌色”的按钮等控件进行不同色系的颜色切换。

9.4 创建store总库

新建store/index.js:

import { configureStore } from @reduxjs/toolkit // 引入主题换肤store分库 import themeReducer from @/store/slices/theme export const store = configureStore({ reducer: { // 主题换肤store分库 theme: themeReducer // 可以根据需要在这里继续追加其他分库 }, })

原理就是创建总库,把各个分库都汇总起来。注释已写明,不再赘述。

9.5 引入store到项目

首先,将store引入到项目工程中。

修改src/index.js:

import React from react import ReactDOM from react-dom/client import { RouterProvider } from react-router-dom import { globalRouters } from @/router import { ConfigProvider } from antd + import { store } from @/store + import { Provider } from react-redux // 引入Ant Design中文语言包 import zhCN from antd/locale/zh_CN // 全局样式 import @/common/styles/frame.styl const root = ReactDOM.createRoot(document.getElementById(root)) root.render( + <Provider store={store}> <ConfigProvider locale={zhCN}> <RouterProvider router={globalRouters} /> </ConfigProvider> + </Provider> )

其实就是用react-redux提供的Provider带上store把项目包起来,这样整个项目就可以随时随地访问store了。

9.6 store的使用:实现亮色/暗色主题切换

由于主题换肤的交互操作位于Header组件,所以让Header组件对接store总库。

修改src/components/header/index.js:

import { Button, Card } from antd + // 新加入“太阳”图标M import { MoonOutlined, ThemeOutlined, SunOutlined } from @/components/extraIcons + // 引入Redux + import { useSelector, useDispatch } from react-redux + // 从主题换肤store分库引入setDark方法 + import { setDark } from @/store/slices/theme import ./header.styl function Header(props) { + + const dispatch = useDispatch() + e中的主题配置+ const theme = useSelector((state) => state.theme) // 接收来自父组件的数据 const { title, info } = props // 如果info存在,则执行info() info && info() return ( <Card className=“M-header”> <div className=“header-wrapper”> <div className=“logo-con”>Header:{title}</div> <div className=“opt-con”> <Button icon={<MoonOutlined />} shape=“circle”></Button> + {theme.dark ? ( + <Button + icon={<SunOutlined />} + shape=“circle” + onClick={() => { + dispatch(setDark(false)) + }} + ></Button> + ) : ( + <Button + icon={<MoonOutlined />} + shape=“circle” + onClick={() => { + dispatch(setDark(true)) + }} + ></Button> + )} <Button icon={<ThemeOutlined />} shape=“circle”></Button> </div> </div> </Card> ) } export default Header

必要的注释已经写好了。useDispatch和useSelector可以通俗理解为:

useDispatch用于写入store库,调用store里定义的方法。useSelector用于读取store库里的变量值。

进而确定是显示“月亮”按钮还是“太阳”按钮。

现在运行起来,点击Header里的“月亮/太阳”图标,可以进行切换了。但是并没有看到暗色主题效果?这是因为还没有把主题配置传递给Antd。

在本教程的需求中,Login页面不参与主题换肤,而其他页面参与主题换肤。因此,只需要在Entry页面通过useSelector将当前store里的主题配置读取出来,再应用给Antd即可。

修改src/entry/index.js:

import { Outlet, useLocation } from react-router-dom import Header from @/components/header + import { useSelector } from react-redux + import { ConfigProvider, theme } from antd import ./entry.styl + // darkAlgorithm为暗色主题,defaultAlgorithm为亮色(默认)主题+ // 注意这里的theme是来自于Ant Design的,而不是store + const { darkAlgorithm, defaultAlgorithm } = theme function Entry() { // 获得路由钩子 const location = useLocation() + + const globalTheme = useSelector((state) => state.theme) + // Ant Design主题变量 + let antdTheme = { + // 亮色/暗色配置 + algorithm: globalTheme.dark ? darkAlgorithm : defaultAlgorithm, + } return ( + <ConfigProvider theme={antdTheme}> <div className=“M-entry”> <Header title={location.pathname} /> <div className=“main-container”> <Outlet /> </div> </div> + </ConfigProvider> ) } export default Entry

必要的注释已经写好了。主要逻辑就是从store里读取当前的主题配置,然后通过Antd提供的ConfigProvider带着antdTheme,把Entry页面包起来。

运行效果如下:

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

9.7 非Ant Design组件的主题换肤

细心的同学可能发现了,上一章节中的主题切换,页面中的“Home Page”始终是白色,并没有跟随换肤。这是因为它并没有包裹在Antd的组件中。而Header组件能够换肤是因为其外层用了Antd的<Card>组件。所以在开发过程中,建议尽量使用Antd组件。当然,很可能会遇到自行开发的组件也要换肤。

接下来,就以“Home Page”换肤为目标,讲解下如何实现非Ant Design组件的主题换肤。

实现方式就是用Ant Design提供的useToken方法将当前主题的颜色赋值给非自定义组件。

修改src/pages/home/index.js:

import { useNavigate } from react-router-dom M import { Button, theme } from antd import { goto } from @/api import ./home.styl + const { useToken } = theme function Home() { // 创建路由钩子 const navigate = useNavigate() + n+ const { token } = useToken() return ( <div className=“P-home”> + <h1 style={{color: token.colorText}}>Home Page</h1> <div className=“ipt-con”>

运行效果如下:

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

这里将“Home Page”的文字色设为了token.colorText,即当前Antd文本色,因此会跟随主题进行换肤。同理,如果想让自定义组件的背景色换肤,可以使用token.colorBgContainer;边框色换肤,可以使用token.colorBorder;使用当前Antd主题色,可以使用token.colorPrimary。

以上这些token,就是Antd官网所介绍的SeedToken、MapToken、AliasToken,这些token涵盖了各种场景的颜色,大家参照官网列出的token说明挑选合适参数即可。

Ant Design 定制主题官方说明:

https://ant-design.antgroup.com/docs/react/customize-theme-cn#theme

9.8 store的使用:实现主题色切换

在src/globalConfig.js里的customColorPrimarys就是留给主题色换肤的。接下来讲解下具体实现方法。为了让交互体验稍微好一点,通过Antd的Modal组件来制作主题色选择功能。

9.8.1 创建主题色选择对话框组件

新建src/components/themeModal/index.js:

import { Modal } from antd import { useSelector, useDispatch } from react-redux import { CheckCircleFilled } from @ant-design/icons import { setColorPrimary } from @/store/slices/theme import { globalConfig } from @/globalConfig import ./themeModal.styl function ThemeModal({ onClose }) { const dispatch = useDispatch() const theme = useSelector((state) => state.theme) return ( <Modal className=“M-themeModal” open={true} title=“主题色” onCancel={() => { onClose() }} maskClosable={false} footer={null} > <div className=“colors-con”> { // 遍历globalConfig配置的customColorPrimarys主题色 globalConfig.customColorPrimarys && globalConfig.customColorPrimarys.map((item, index) => { return ( <div className=“theme-color” style={{ backgroundColor: item }} key={index} onClick={() => { dispatch(setColorPrimary(item)) }} > { // 如果是当前主题色,则显示“对勾”图标 theme.colorPrimary === item && ( <CheckCircleFilled style={{ fontSize: 28, color: #fff, }} /> ) } </div> ) }) } </div> </Modal> ) } export default ThemeModal

补充相应的样式,新建src/components/themeModal/themeModal.styl:

.M-themeModal .colors-con margin-top: 20px display: grid grid-template-columns: repeat(6, 1fr) row-gap: 10px .theme-color margin: 0 auto width: 60px height: 60px line-height: 68px border-radius: 6px cursor: pointer text-align: center

9.8.2 引入主题色选择对话框组件

修改src/components/header/index.js:

+ import { useState } from react import { Button, Card } from antd import { MoonOutlined, ThemeOutlined, SunOutlined } from @/components/extraIcons import { useSelector, useDispatch } from react-redux import { setDark } from @/store/slices/theme + import ThemeModal from @/components/themeModal + import { globalConfig } from @/globalConfig import ./header.styl function Header(props) { const dispatch = useDispatch() const theme = useSelector((state) => state.theme) // 接收来自父组件的数据 const { title, info } = props + // 是否显示主题色选择对话框 + const [showThemeModal, setShowThemeModal] = useState(false) // 如果info存在,则执行info() info && info() return ( <Card className=“M-header”> <div className=“header-wrapper”> <div className=“logo-con”>Header:{title}</div> <div className=“opt-con”> {theme.dark ? ( <Button icon={<SunOutlined />} shape=“circle” onClick={() => { dispatch(setDark(false)) }} ></Button> ) : ( <Button icon={<MoonOutlined />} shape=“circle” onClick={() => { dispatch(setDark(true)) }} ></Button> )} <Button icon={<ThemeOutlined />} shape=“circle”></Button> + { + // 当globalConfig配置了主题色,并且数量大于0时,才显示主题色换肤按钮 + globalConfig.customColorPrimarys && + globalConfig.customColorPrimarys.length > 0 && ( + <Button + icon={<ThemeOutlined />} + shape=“circle” + onClick={() => { + setShowThemeModal(true) + }} + ></Button> + ) + } </div> </div> + { + // 显示主题色换肤对话框+ showThemeModal && ( + <ThemeModal + onClose={() => { + setShowThemeModal(false) + }} + /> + ) + } </Card> ) } export default Header

运行项目,点击Header组件最右侧的主题色按钮,可以弹出主题色换肤对话框。

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

但现在点击颜色后还不能生效,这是因为还没有把主题色传递给Antd。

9.8.3 将主题色配置应用于项目

修改src/pages/entry/index.js:

function Entry() { // Ant Design主题变量 let antdTheme = { // 亮色/暗色配置 algorithm: globalTheme.dark ? darkAlgorithm : defaultAlgorithm, } + // 应用自定义主题色 + if (globalTheme.colorPrimary) { + antdTheme.token = { + colorPrimary: globalTheme.colorPrimary, + } + } return (

现在点击主题色对话框里的颜色就会立即生效了,刷新页面或者重新打开网页也会保留上次的主题色。

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

9.9 安装Redux调试浏览器插件

本章节讲解的Redux使用,每次对store的操作变化跟踪如果用console.log()显然很麻烦,也不及时。为了更方便地跟踪Redux状态,建议安装Chrome插件。这个插件可记录每次Redux的变化,非常便于跟踪调式。

先科学上网,在Chrome网上应用店里搜索“Redux DevTools”并安装。

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

具体使用方法很简单,大家可在网上查阅相关资料,不再赘述。

10 基于axios封装公用API库

为了方便API的维护,把各个API地址和相关方法集中管理是一个很不错的方案。

10.1 安装axios

axios是一款非常流行的API请求工具,先来安装一下。

执行:

yarn add axios

10.2 封装公用API库

直接上代码。

更新src/api/index.js:

import axios from axios import { createHashHistory } from history import { Modal } from antd import { globalConfig } from @/globalConfig 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 = globalConfig.SESSION_LOGIN_INFO // 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.data config.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, }) Modal.error({ title: 登录失败, }) }) }, // 管登出(登出后将登录信息从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) }, } export function getLocalLoginInfo() { return JSON.parse(window.localStorage[SESSION_LOGIN_INFO]) } // 退出登录 export function logout() { // 清除localStorage中的登录信息 window.localStorage.removeItem(SESSION_LOGIN_INFO) // 跳转至Login页面 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.data config.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() }) }

代码比较多,必要的备注都写了,不再赘述。

这里主要实现了以下几方面:

通过apiReqs把项目所有API进行统一管理。

通过apiRequest方法,实现了统一的token验证、登录状态失效报错以及请求错误报错等业务逻辑。

为什么signIn和signOut方法没有像getUserList和modifyUser一样调用apiRequest呢?

因为signIn和signOut的逻辑比较特殊,signIn并没有读取localStorage,而signOut需要清除localStorage,这两个逻辑是与其他API不同的,所以单独实现了。

10.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: yyds2023, }, } return result })

然后在src/index.js中引入mock.js:

import React from react import ReactDOM from react-dom/client import { RouterProvider } from react-router-dom import { globalRouters } from @/router import { ConfigProvider } from antd import { store } from @/store import { Provider } from react-redux + import ./mock …(略)

如此简单。这样,在项目中请求/api/login的时候,就会被Mock.js拦截,并返回Mock.js中模拟好的数据。

※注:

正式上线前,一定不要忘记关掉Mock.js!!!直接在src/index.js中注释掉import ./mock这段代码即可。

10.4 发起API请求:实现登录功能

继续完善Login页面,实现一个API请求。

修改src/pages/login/index.js:

+ import { useState } from react + import { apiReqs } from @/api import { useNavigate } from react-router-dom import { Button, Input } from antd import imgLogo from ./logo.png import ./login.styl function Login() { // 创建路由钩子 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 ( <div className=“P-login”> <img src={imgLogo} alt=“” className=“logo” /> <div className=“ipt-con”> M <Input placeholder=“账号” value={account} onChange={(e)=>{setAccount( e.target.value)}} /> </div> <div className=“ipt-con”> M <Input.Password placeholder=“密码” value={password} onChange={(e)=>{setPassword(e.target.value)}} /> </div> <div className=“ipt-con”> M <Button type=“primary” block={true} onClick={login}>登录</Button> </div> </div> ) } export default Login

运行项目,进入http://localhost:3000/#/login,账号、密码随便输入,点击“登录”,已经通过mock模拟请求成功了。

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

查看浏览器localStorage,登录信息也成功写入。

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

11 一些细节问题

11.1 解决Modal.method跟随主题换肤的问题

Antd的Modal提供了直接的函数式调用,比如Modal.success、Modal.error、Modal.error、Modal.confirm等。

这种方式并没有使用<Modal>包裹,所以是无法跟随主题换肤的。

下面通过完善退出登录的交互,来复现下这个问题。

修改src/pages/home/index.js:

import { useNavigate } from react-router-dom M import { Button, theme, Modal } from antd M import { logout, goto } from @/api import ./home.styl const { useToken } = theme function Home() { // 创建路由钩子 // const navigate = useNavigate() const { token } = useToken() + // 退出登录 + const exit = () => { + Modal.confirm({ + title: 是否退出登录?, + onOk() { + logout() + }, + }) + } return ( <div className=“P-home”> <h1 style={{ color: token.colorText }}>Home Page</h1> <div className=“ipt-con”> <Button onClick={() => { goto(/login) }} > 组件外跳转 </Button> </div> <div className=“ipt-con”> M <Button type=“primary” onClick={exit}>返回登录</Button> </div> </div> ) } export default Home

这里通过Modal.confirm来确认是否退出登录,点击后可以发现,在暗色主题下,Modal.confirm并未跟随主题。

2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

继续修改src/pages/home/index.js:

function Home() { const { token } = useToken() + const [modal, contextHolder] = Modal.useModal() // 退出登录 const exit = () => { + // 把之前的Modal改为modal M modal.confirm({ title: 是否退出登录?, onOk() { logout() }, }) } return ( <div className=“P-home”> + { + // 这是最终解决Modal.method跟随换肤的关键,contextHolder在组件DOM中随便找个地方放就行 + contextHolder + } </div> ) } export default Home
2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶

Ant Design的Modal.useModal()说明:

https://ant-design.antgroup.com/components/modal-cn#modalusemodal

Account页面的“返回登录”也用同样的方式修改,不再赘述。

※注:

从@/api中引入的logout()方法,会清除localStorage中的登录信息并跳转至Login页面。具体可参看src/api/index.js中该方法的注释。

11.2 路由守卫

现在实现一个简单的路由守卫,通过Entry进行登录状态验证,未登录用户访问Home或者Account页面则强制跳转至Login页面。

修改src/router/index.js,加入以下代码:

import { createHashRouter, Navigate } from react-router-dom import Login from @/pages/login import Home from @/pages/home import Account from @/pages/account import Entry from @/pages/entry + import { globalConfig } from @/globalConfig + // 路由守卫 + export function PrivateRoute(props) { + // 判断localStorage是否有登录用户信息,如果没有则跳转登录页 + return window.localStorage.getItem(globalConfig.SESSION_LOGIN_INFO) ? ( + props.children + ) : ( + <Navigate to=“/login” /> + ) + }

然后再修改src/pages/entry/index.js:

import { Outlet, useLocation } from react-router-dom import Header from @/components/header import { useSelector } from react-redux import { ConfigProvider, theme } from antd + import { PrivateRoute } from @/router import ./entry.styl function Entry() { return ( + <PrivateRoute> <ConfigProvider theme={antdTheme}> </ConfigProvider> + </PrivateRoute> ) } export default Entry

再次运行项目,这时,如果未经Login页面正常登录(即localStorage里没有登录信息),直接通过浏览器地址栏输入http://localhost:3000/#/home或者http://localhost:3000/#/account则会直接返回到Login页面。这是因为在Entry框架页面中引入了PrivateRoute,先检查localStorage是否有登录用户信息,没有则强制跳转至Login页面。

当然,如果你想在路由守卫中实现更多的业务逻辑判断,请自行丰富PrivateRoute方法即可。

11.3 设置开发环境的反向代理请求

在React开发环境中,与后端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://xxxx, changeOrigin: true, }) ) }

这代码的意思就是,只要请求地址是以”/api”开头,那就反向代理到http://xxxx域名下,跨域问题解决!大家可以根据实际需求进行修改。

一定记得要把mock.js注释掉,否则会先被mock.js拦截,到不了反向代理这一步。

※注:

setupProxy.js设置后,一定要重启项目才生效。

12 build项目

在build前还需要做一步配置,否则build版本网页中的文件引用都是绝对路径,运行后是空白页面。

修改package.json:

“name”: “react-app”, “version”: “0.1.0”, “private”: true, + “homepage”: “./”, …(略)

然后执行:

yarn build

生成的文件在项目根目录的build目录中,打开index.html即可看到正常运行的项目。

13 项目Git源码

本项目已上传至Gitee和GitHub,方便各位下载。

Gitee:

https://gitee.com/betaq/react-app-2023spring

GitHub:

https://github.com/Yuezi32/react-app-2023spring

结束语

以上就是本次Reac

更多精品阅读

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务