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

卧梅又闻花

共 103066字,需浏览 207分钟

 · 2023-05-18

97c6dd1ed64d3c081312626f84574fd1.webp

4f8f51561e1dc429a4cead1258e71f8d.webp

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.1 创建二级路由的框架页面

• 7.2 配置二级路由

• 7.3 获取当前路由地址

8 React Developer Tools浏览器插件

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/
      
    

dd424b8890a18b7f78c6efc11ce6ca30.webp


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">React-App</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>
      
    


运行效果如下:

d7717b9dcff74f386239a5d785c8fd94.webp


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.org 和 http://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' ), // 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/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>React-App</h1> <Button type="primary">Button</Button> </div> ) }
export default App

执行yarn start:

e32a7af7d50b3e58df2783820b812d2a.webp

可以看到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'
      
    


46547cd0951cd9be5848cce876741850.webp


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'
      
    

看看效果:

ff19a9d645bb8a6a5938fa0005166447.webp

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'
      
    
看看效果:

ad98a124bca862ed4edd3b8048630729.webp

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组件)中,这样就非常适合封装统一的处理逻辑。

560bfc306bb1dad7f3392bbae13e7ea7.webp

后续章节会讲述如何封装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/)找到心仪的图片,然后点击按钮。

fa362d815f3f706b5ef0bffa31fd66d9.webp

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


ce92a16d38c198089e9a0753ead2edcd.webp

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


※注:一定要仔细坚持以下三方面。

1. 检查svg代码中是否有class以及与颜色相关的fill、stroke等属性,如有,必须连带属性一起删除。

2. 确保<SVG>标签中有fill="currentColor",否则图标的颜色将不能改变。

3. 确保<SVG>标签中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"
      
      
                    p-id="1344"
      
      
        -           width="400"
      
      
        +           width="1em"
      
      
        -           height="400"
      
      
        +           height="1em"
      
      
        +           fill="currentColor"
      
      
                >
      
      
                    <path
      
      
                        d="...(略)"
      
      
                        p-id="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
      
    

简单说明一下:

1. 使用Antd的<Card>组件,是为了跟随主题换肤,否则Header的背景色、边框色、文字色等元素的换肤都要单独实现。

2. <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>
...(略)


运行效果如下:


07aac3f7a176d21e15feede19991e3f0.webp

6.4 组件传参

使用过Vue的同学都知道,Vue组件有dataprops

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> ...(略)
运行看下已经生效。

e0e80429ad78ac6cdb6ca0271882b348.webp


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


运行效果如下:


e47e2ae356e486f36c1b11956aefb59c.webp

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


7.3 获取当前路由地址

使用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


运行效果如下:

f3cbd992b581794f666917ebb46b1ded.webp

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

8 React Developer Tools浏览器插件

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

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

aad503db6e74700f2525a214b7d6fc0e.webp

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

2fd555a2fe0637495099b8d72b256176.webp

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'
      
      
        
          
// 先从localStorage里获取主题配置 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


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

1. 先从localStorage里获取主题配置,这么做是为了将用户的主题配置保存在浏览器中,用户在刷新或者重新打开该项目的时候,会直接应用之前设置的主题配置。

2. 如果localStorage没有主题配置,则从globalConfig读取默认值,然后再写入localStorage。这种情况一般是用户使用当前浏览器第一次浏览该项目时会用到。

3. setDark用来设置“亮色/暗色主题”,setColorPrimary用来设置“主题色”。每次设置后,除了变更store里的值(为了项目全局动态及时生效),还要同步写入localStorage(为了刷新或重新打开时及时生效)。

4. “亮色/暗色主题”和“主题色”虽然都是颜色改变,但是完全不同的两个维度的换肤。“亮色/暗色主题”主要是对默认的文字、背景、边框等基础元素进行黑白切换,而“主题色”则是对带有“品牌色”的按钮等控件进行不同色系的颜色切换。

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) {
+ // 获取redux派发钩子 + const dispatch = useDispatch()
+ // 获取store中的主题配置 + 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库里的变量值。
以上代码中的theme就是从总库中获取的theme分库。theme.dark就是从theme分库中读取的dark值,从而判断当前是亮色还是暗色主题,进而确定是显示“月亮”按钮还是“太阳”按钮。现在运行起来,点击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()
+ // 获取store中的主题配置 + 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页面包起来。

运行效果如下:

318374085798db4ed648c07e3a2d8502.webp

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()
+ // 获取Design Token + const { token } = useToken()
return ( <div className="P-home"> + <h1 style={{color: token.colorText}}>Home Page</h1> <div className="ipt-con"> ...(略)
运行效果如下:

f0cc5926cfaaf25189cd062f46567bba.webp

这里将“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 }) {
      
      
            // 获取redux派发钩子
      
      
            const dispatch = useDispatch()
      
      
            // 获取store中的主题配置
      
      
            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) { // 获取redux派发钩子 const dispatch = useDispatch()
// 获取store中的主题配置 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组件最右侧的主题色按钮,可以弹出主题色换肤对话框。

ce107a6bb8c52c458216c12889f1d225.webp

但现在点击颜色后还不能生效,这是因为还没有把主题色传递给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 ( ...(略)
现在点击主题色对话框里的颜色就会立即生效了,刷新页面或者重新打开网页也会保留上次的主题色。

ae7fb648cc9c5d76be8e7bec4a94187b.webp

9.9 安装Redux调试浏览器插件

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

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

93b74f3c1fe21941e4bddb49351bf6db.webp

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

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) }, }
// 从localStorage获取用户信息 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() }) }

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

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

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

2. 通过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模拟请求成功了。

666e2ac33fd7569ca993ea50ade080ab.webp

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

b41d8889ee62a51f5ed2f197ee4300b8.webp

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()
// 获取Design Token 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并未跟随主题。

7d578cde751755543112ec79af10bbd4.webp

继续修改src/pages/home/index.js:
      
            ...(略)
      
      
            function Home() {
      
      
                // 获取Design Token
      
      
                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
必要的逻辑直接看注释吧。contextHolder是关键,通过它来获取上下文从而解决主题换肤问题。效果如下:

56411c9e2a219f83c24db041424c5eb9.webp

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


结束语


以上就是本次React全家桶教程的全部内容。篇幅较长,确实是花了很长时间精心整理、反复验证、句句斟酌的完整教程,希望能够帮助到你。更多精彩详实的开发教程,欢迎阅读我的微信公众号「卧梅又闻花」


推荐阅读


2022新春版:React+Antd开发Chrome插件教程(Manifest V3)上篇

2022新春版:React+Antd开发Chrome插件教程(Manifest V3)下篇

2022新春版:手把手教你搭建Electron17+React17+Antd架构工程

看了就会的Next.js SSR/SSG 实战教程


欢迎关注我的微信公众号「卧梅又闻花」,随时获取最新文章。

浏览 30
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报