2023盛夏版:轻松搞定基于Vite4的React项目全家桶

卧梅又闻花

共 75057字,需浏览 151分钟

 ·

2023-08-07 18:21


Vite4发布有很长一段时间了,相信很多小伙伴早已经用上了Vite,体验到了Vite带来的各种喜悦。

Vite是一个基于浏览器原生ES imports的开发服务器,利用浏览器去解析 imports,在服务器端按需编译返回,相比webpack,完全省去了打包这个过程,所以编译起来非常迅速,也不会随着项目模块增多而变慢。关于Vite的详细介绍,网上已经有很多相关内容了,本次分享主要聚焦如何使用Vite搭建React+Antd工程。

在几个月前,我刚发布了《2023新春版:看这篇大宝典就够了!从零搭建React项目全家桶》。这篇文章是基于官方提供的Create-React-App进行构建。近期,React官网改版了,全篇没有提到Create-React-App,反而推荐使用其他社区的脚手架工具来使用React,其中就提到了Vite。而Create-React-App也一直停留在5.0.1版本,从2022年4月13日至今没有更新,看样子也被官方抛弃了。当然Create-React-App仍然具有它的使用价值。

编写本篇Vite版本教程,一是帮助小伙伴们省去自行摸索的时间,跟上前端技术的快车;二是关注Vite很久了,在项目中安心使用Vite的时机已成熟,是时候来次本系列教程的大版本更新了。下面就开始Vite的学习之旅吧!

先睹为快

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

1 初始化项目

• 1.1 使用Vite新建项目

• 1.2 安装并运行项目

• 1.3 精简项目

2 Vite基础配置

• 2.1 配置国内镜像源

• 2.2 支持Sass/Scss/Less/Stylus

• 2.3 设置dev环境的Server端口号

• 2.4 设置dev环境自动打开浏览器

• 2.5 设置路径别名

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 在Header组件中添加页面导航示

• 6.5 组件传参

7 二级路由配置

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

• 7.2 配置二级路由

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 设置开发环境的反向代理请求

• 11.4 兼容js文件

• 11.5 允许dev环境的IP访问

• 11.6 批量升级全部项目npm依赖包

12 build项目

• 12.1 设置静态资源引用路径

• 12.2 设置build目录名称及静态资源存放目录

• 12.3 开启build项目生成map文件(不推荐)

• 12.4 执行build项目

13 项目Git源码

结束语


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

Node.js 18.16.0

vite 4.3.9

antd 5.5.2

axios 1.4.0

mockjs 1.1.0

react 18.2.0

react-redux 8.0.7

react-router-dom 6.11.2

less 4.1.3

sass 1.62.1

stylus 0.59.0

@reduxjs/toolkit 1.9.5


※注:

代码区域每行开头的:

"+" 表示新增

"-" 表示删除

"M" 表示修改


跟着操作一遍,就可以快速上手Vite啦!下面请跟着新版教程一步步操作。


1 初始化项目
1.1 使用Vite新建项目

※注:Vite需要Node.js版本14.18+,16+。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。


先进入想要创建项目的目录,在这个目录下执行安装命令。


如果使用npm,执行:

npm create vite@latest

如果使用yarn,执行:

yarn create vite

执行后,会要求填写项目名称,这里我填写的是vite-react-app,可根据情况自定。

Project name: vite-react-app

然后,会要求选择框架,选择React:

 Select a framework:    Vanilla    Vue>   React    Preact    Lit    Svelte    Others

最后,选择开发语言,本教程选择JavaScript:

 Select a variant:    TypeScript    TypeScript + SWC>   JavaScript    JavaScript + SWC

回答以上“灵魂三问”后,即可完成Vite项目创建。


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

npm install --global yarn

yarn中文网站:

https://yarn.bootcss.com/

1.2 安装并运行项目

进入项目目录,运行命令进行项目依赖包的安装。

cd vite-react-appyarn  或者 npm install

稍等片刻,安装完成后,执行以下命令运行项目:

yarn dev 或者 npm run dev
是不是感觉编译的速度非常快,几乎没有感知就完成了。
与Create-React-App不同,Vite默认是不会自动启动浏览器打开项目页面的。
需要手动打开以下地址访问项目:
http://localhost:5173/

Vite默认开启的端口是5173,后续章节会讲解怎么修改端口号。
1.3 精简项目

接下来,删除用不到的文件,最简化项目。

   ├─ /node_modules   ├─ /public-  |  └─ vite.svg   ├─ /src-  |  ├─ /assets-  |  |  └─ react.svg-  |  ├─ App.css   |  ├─ App.jsx-  |  ├─ index.css   |  └─ main.jsx   ├─ .eslintrc.cjs   ├─ .gitignore   ├─ index.html   ├─ package.json   ├─ vite.config.js   └─ yarn.lock

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

├─ /node_modules├─ /public├─ /src|  ├─ App.jsx|  └─ main.jsx├─ .eslintrc.cjs├─ .gitignore├─ index.html├─ package.json├─ vite.config.js└─ yarn.lock
以上文件删除后,页面会报错。这是因为相应的文件引用已不存在。需要继续修改代码,先让项目正常运行起来。
逐个修改以下文件,最终精简代码依次如下:

src/App.jsx:

function App() {    return <div className="App">Vite-React-App</div>}
export default App

src/main.jsx:

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

index.html:

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <link rel="icon" type="image/svg+xml" href="/favicon.ico" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>Vite React App</title>  </head>  <body>    <div id="root"></div>    <script type="module" src="/src/main.jsx"></script>  </body></html>

在上述index.html代码中,修改了网站图标,因此需要自行准备一个图标文件favicon.ico,存放在/public目录下。当然,也可以使用svg格式。

   ├─ /node_modules   ├─ /public+  |  └─ favicon.ico   ├─ /src   |  ├─ App.jsx   |  └─ main.jsx   ├─ .eslintrc.cjs   ├─ .gitignore   ├─ index.html   ├─ package.json   ├─ vite.config.js   └─ yarn.lock
这里你可能会问,为什么在index.html中的<link>中引入图标的路径是"/favicon.ico",而favicon.ico明明是放在/public目录下,却不是"/public/favicon.ico"呢?
按照Vite官方说明:引入public中的资源永远应该使用根绝对路径,并且,public中的资源不应该被JavaScript文件引用。

public目录Vite官方说明:

https://cn.vitejs.dev/guide/assets.html#the-public-directory


执行yarn dev,运行效果如下:

2 Vite基础配置
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或者npm的配置,可以执行以下命令查看:

yarn查看方法:

yarn config list

npm查看方法:

npm config list

※注:本教程主要使用yarn,后续不再复述对应的npm命令。

2.2 支持Sass/Scss/Less/Stylus

Vite本身提供了对.scss/.sass/.less/.styl/.stylus文件的内置支持。无需再安装特定的Vite插件,但必须安装相应的预处理器依赖。

支持Sass/Scss,执行以下命令安装:
yarn add -D sass
支持Less,执行以下命令安装:
yarn add -D less
支持Stylus,执行以下命令安装:
yarn add -D stylus

安装后,就可以直接使用以上对应的CSS预处理语言了,非常方便。

CSS预处理Vite官方说明:

https://cn.vitejs.dev/guide/features.html#css-pre-processors


2.3 设置dev环境的Server端口号

dev server默认端口是5173,如果想修改为其他端口(例如习惯使用Create-React-App的3000端口),可以进行以下设置。

修改vite.config.js
    import { defineConfig } from 'vite'    import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/ export default defineConfig({+ server: {+ // 指定dev sever的端口号,默认为5173+ port: 3000,+ }, plugins: [react()], })

※注:与基于webpack的Create-React-App不同,Vite修改项目配置后,不需要重启项目即可生效。


2.4 设置dev环境自动打开浏览器

使用Create-React-App创建的工程,在启动的时候会自动打开浏览器运行当前项目。但是基于Vite创建的工程默认情况并不会自动打开浏览器。如果想要保持Create-React-App的习惯,自动打开浏览器,可进行以下设置。

修改vite.config.js
    // https://vitejs.dev/config/    export default defineConfig({        server: {            // 指定dev sever的端口号            port: 3000,+           // 自动打开浏览器运行以下页面+           open: '/',        },        ...(略)    })

open的"/"值,表示的是打开"localhost:3000/"

如果想直接打开其他页面,例如"localhost:3000/#/home",open的值则设置为"/#/home"即可。

※注:open的值修改后,虽然已经生效,但不会直接触发打开浏览器的行为。这个行为只发生在通过命令启动项目的时候,也就是执行yarn dev的时候。

2.5 设置路径别名

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

修改vite.config.js
    import { defineConfig } from 'vite'    import react from '@vitejs/plugin-react'+   import path from 'path'
// https://vitejs.dev/config/ export default defineConfig({ server: { ...(略) },+ resolve: {+ alias: {+ '@': path.resolve(__dirname, 'src'),+ },+ }, ...(略) })

这个配置里使用了path,不要忘记在代码头部要import path from 'path'哦。

这样在js代码开头的import路径中,直接使用@表示“src根目录”,不用去自己去数有多少个"../"了。

例如,src/main.jsx
// 表示该文件当前路径下的App.jsx(相对路径)import App from './App'// 表示src/App.jsx,等价于上面的文件地址(绝对路径)import App from '@/App'
3 项目架构搭建

3.1 项目目录结构设计

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

├─ /node_modules├─ /public|  └─ favicon.ico        <-- 网页图标├─ /src|  ├─ /api               <-- api目录|  |  └─ index.jsx       <-- api库|  ├─ /common            <-- 全局公用目录|  |  ├─ /fonts          <-- 字体文件目录|  |  ├─ /images         <-- 图片文件目录|  |  ├─ /js             <-- 公用js文件目录|  |  └─ /styles         <-- 公用样式文件目录|  |  |  ├─ frame.styl   <-- 全部公用样式(import本目录其他全部styl)|  |  |  ├─ reset.styl   <-- 清零样式|  |  |  └─ global.styl  <-- 全局公用样式|  ├─ /components        <-- 公共模块组件目录|  |  ├─ /header         <-- 头部导航模块|  |  |  ├─ index.jsx    <-- header主文件|  |  |  └─ header.styl  <-- header样式文件|  |  └─ ...             <-- 其他模块|  ├─ /pages             <-- 页面组件目录|  |  ├─ /home           <-- home页目录|  |  |  ├─ index.jsx    <-- home主文件|  |  |  └─ home.styl    <-- home样式文件|  |  ├─ /login          <-- login页目录|  |  |  ├─ index.jsx    <-- login主文件|  |  |  └─ login.styl   <-- login样式文件|  |  └─ ...             <-- 其他页面|  ├─ /route             <-- 路由配置目录|  ├─ /store             <-- Redux配置目录|  ├─ globalConfig.jsx   <-- 全局配置文件|  ├─ main.jsx           <-- 项目入口文件|  └─ mock.jsx           <-- mock数据文件├─ .eslintrc.cjs         <-- ESLint配置文件├─.gitignore├─ index.html            <-- HTML页模板├─ package.json├─ vite.config.js        <-- Vite配置文件└─ yarn.lock

这里需要注意的是,基于Vite脚手架的工程在src目录里并没有使用js文件,而是以jsx文件进行开发。默认情况下,js文件是不能正常加载的。在后续章节会讲解如何通过修改Vite配置来兼容js文件,但是仍然不推荐这么做。除此之外,在src目录之外的vite.config.js则相反,没有使用jsx文件。

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

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

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,代码比较全面,更新也比较及时(截至本文写作时,是2023年2月14日更新的)。

具体代码详见:

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/main.jsx里引入frame.styl
    import React from 'react'    import ReactDOM from 'react-dom/client'    import App from './App'+   // 全局样式+   import '@/common/styles/frame.styl'
ReactDOM.createRoot(document.getElementById('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.jsx 来验证下Antd:

import { Button } from 'antd'
function App() { return ( <div className="App"> <h1>Vite-React-App</h1> <Button type="primary">Button</Button> </div> )}
export default App

执行yarn dev:

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

※注:

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


4.2 设置Antd为中文语言

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

修改src/main.jsx:
    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'
M ReactDOM.createRoot(document.getElementById('root')).render(+ <ConfigProvider locale={zhCN}>+ <App />+ </ConfigProvider>+ )

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

5 页面开发

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

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

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

工程文件变动如下:
    ├─ /node_modules    ├─ /public    |  └─ favicon.ico        <-- 网页图标    ├─ /src    |  ├─ /api               <-- api目录    |  |  └─ index.jsx       <-- api库    |  ├─ /common            <-- 全局公用目录    |  ├─ /components        <-- 公共模块组件目录+   |  ├─ /pages+   |  |  ├─ /account+   |  |  |  ├─ index.jsx+   |  |  |  └─ account.styl+   |  |  ├─ /entry+   |  |  |  ├─ index.jsx+   |  |  |  └─ entry.styl+   |  |  ├─ /home+   |  |  |  ├─ index.jsx+   |  |  |  └─ home.styl+   |  |  ├─ /login+   |  |  |  ├─ index.jsx+   |  |  |  ├─ login.styl+   |  |  |  └─ logo.png    |  ├─ /route             <-- 路由配置目录    |  ├─ /store             <-- Redux配置目录    |  ├─ globalConfig.jsx   <-- 全局配置文件    |  ├─ main.jsx           <-- 项目入口文件    |  └─ mock.jsx           <-- mock数据文件    ├─ .eslintrc.cjs         <-- ESLint配置文件    ├─.gitignore    ├─ index.html            <-- HTML页模板    ├─ package.json    ├─ vite.config.js        <-- Vite配置文件    └─ yarn.lock

5.1 构建Login页面

页面构建代码不再详述,都是很基础的内容了。

新建src/pages/login/index.jsx
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/main.jsx
-   import App from './App'+   import App from '@/pages/login'
运行效果如下:

5.2 构建Home页面

直接上代码。

新建src/pages/home/index.jsx
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/main.jsx

-   import App from '@/pages/login'+   import App from '@/pages/home'
运行效果如下:

5.3 构建Account页面

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

新建src/pages/account/index.jsx
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/main.jsx:
-   import App from '@/pages/home'+   import App from '@/pages/account'
看看效果:

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

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

执行:
yarn add react-router-dom

接下来进行路由配置。

新建src/router/index.jsx
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/main.jsx
    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'
ReactDOM.createRoot(document.getElementById('root')).render( <ConfigProvider locale={zhCN}>- <App />+ <RouterProvider router={globalRouters} /> </ConfigProvider> )

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

记得删掉src/App.jsx。

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

Login页面:

http://localhost:3000/#/login

Home页面:

http://localhost:3000/#/home

Account页面:

http://localhost:3000/#/account


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

下面要实现的功能是,点击Login页面的“登录”按钮,跳转至Home页面。

修改src/pages/login/index.jsx
+   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.jsx
+   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页面;当API请求失败时,进行统一的报错提示。

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

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

新建src/api/index.jsx
import {globalRouters} from '@/router'
export const goto = (path) => { globalRouters.navigate(path)}

以上代码在非React组件中引入全局路由,并封装了goto函数。

src/pages/home/index.jsx里调用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 onClick={()=>{navigate('/login')}}>返回登录</Button> </div>
</div> ) }
export default Home

在Home页点击“组件外跳转”按钮,可以正常跳转至Login页面了,而实际执行跳转的代码是在src/api/index.jsx(非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.jsx:

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/)找到心仪的图片,然后点击按钮。

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


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


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

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.jsx

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.jsx
    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.jsx
    import { useNavigate } from 'react-router-dom'    import { Button } from 'antd'+   import Header from '@/components/header'    import './account.styl'
function Account() {
// 创建路由钩子 const navigate = useNavigate()
return ( <div className="P-account">+ <Header /> <h1>Account Page</h1> ...(略)
运行效果如下:

6.4 在Header组件中添加页面导航

现在,要在Header组件中添加页面导航,主要实现两个功能:

1.  点击导航,跳转到对应的页面

2.  根据当前所处的页面,将对应的导航进行“当前态”显示

在本示例中,Header组件仅出现在Home和Account页面,因此导航中不包括Login页面。

修改src/components/header/index.jsx
M   import { Button, Card, Menu } from 'antd'    import { MoonOutlined, ThemeOutlined } from '@/components/extraIcons'+   import { HomeOutlined, UserOutlined } from '@ant-design/icons'+   import { useLocation, useNavigate } from 'react-router-dom'    import './header.styl'
function Header() {
+ // 创建路由定位钩子+ const location = useLocation()+ // 创建路由钩子+ const navigate = useNavigate()
+ // 定义导航栏+ const menuItems = [+ { + // 导航显示的名称+ label: 'Home',+ // 导航唯一标识,为便于当前态的显示,与当前路由保持一致+ key: '/home',+ // 导航的前置图标+ icon: <HomeOutlined />,+ // 点击跳转行为+ onClick: () => {+ navigate('/home')+ },+ },+ {+ label: 'Account',+ key: '/account',+ icon: <UserOutlined />,+ onClick: () => {+ navigate('/account')+ },+ },+ ]
return ( <Card className="M-header"> <div className="header-wrapper"> <div className="logo-con">Header</div>+ <div className="menu-con">+ <Menu mode="horizontal" selectedKeys={location.pathname} items={menuItems} />+ </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        ...(略)        .header-wrapper            display: flex            .logo-con                display: flex                font-size: 30px                font-weight: bold+           .menu-con+               margin-left: 20px+               width: 300px            .opt-con                display: flex                flex: 1                justify-content: flex-end                gap: 20px

这里需要注意的就是useLocation()的使用。通过useLocation()的pathname,可以得到当前页面所处的路由地址,结合Menu组件中对导航key的定义,就可以判断是否为当前页面了。使用useLocation方法,可以很方便实现页面位置导航及当前页面状态显示等交互需求,非常适合与Antd的Menu导航菜单组件、Breadcrumb面包屑组件搭配使用。

运行效果如下:

6.5 组件传参

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

data是组件内的数据;

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

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

state是组件内的数据;

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

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

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

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

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

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

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

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

修改src/pages/home/index.jsx
    ...(略)M   <Header title="home" info={()=>{console.log('info:home')}} />    ...(略)
修改src/pages/account/index.jsx
    ...(略)M   <Header title="account" info={()=>{console.log('info:account')}} />    ...(略)
修改src/components/header/index.jsx
    ...(略)
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> ...(略)
运行效果如下:

7 二级路由配置

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


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

新建src/pages/entry/index.jsx
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/index.jsx
    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/index.jsx
    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.jsx
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


运行效果如下:

改造后,Header组件的传参不见了。这是因为把Header放到Entry页面后,就没有给Header组件传递title参数了。关于组件间传参、使用useLocation()定位当前路由,以及二级路由的使用,这些关键知识点已经讲解完,这里也就不再对Header组件进行修改了。

8 React Developer Tools浏览器插件

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

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

安装完成后,打开Chrome DevTools,点击Components按钮,可以清晰的看到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.jsx
/** * 全局配置 */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.jsx
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.actionsexport 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.jsx
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/main.jsx
    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'
ReactDOM.createRoot(document.getElementById('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.jsx
    import { Button, Card, Menu } from 'antd'+   // 新加入“太阳”图标M   import { MoonOutlined, ThemeOutlined, SunOutlined } from '@/components/extraIcons'    import { HomeOutlined, UserOutlined } from '@ant-design/icons'    import { useLocation, useNavigate } from 'react-router-dom'+   // 引入Redux+   import { useSelector, useDispatch } from 'react-redux'+   // 从主题换肤store分库引入setDark方法+   import { setDark } from '@/store/slices/theme'    import './header.styl'
function Header(props) {
// 创建路由定位钩子 const location = useLocation() // 创建路由钩子 const navigate = useNavigate()
// 定义导航栏 const menuItems = [ ...(略) ]
+ // 获取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="menu-con"> <Menu mode="horizontal" selectedKeys={location.pathname} items={menuItems} /> </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.jsx
    import { Outlet } 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() {
+ // 获取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 /> <div className="main-container"> <Outlet /> </div> </div>+ </ConfigProvider> ) }
export default Entry

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

运行效果如下:

9.7 非Ant Design组件的主题换肤

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

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

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

修改src/pages/home/index.jsx
    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">M <h1 style={{color: token.colorText}}>Home Page</h1> <div className="ipt-con"> ...(略)
运行效果如下:

这里将“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.jsx里的customColorPrimarys就是留给主题色换肤的。接下来讲解下具体实现方法。为了让交互体验稍微好一点,通过Antd的Modal组件来制作主题色选择功能。

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

新建src/components/themeModal/index.jsx
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.jsx
+   import { useState } from 'react'    import { Button, Card, Menu } from 'antd'    // 新加入“太阳”图标    import { MoonOutlined, ThemeOutlined, SunOutlined } from '@/components/extraIcons'    import { HomeOutlined, UserOutlined } from '@ant-design/icons'    import { useLocation, useNavigate } from 'react-router-dom'    // 引入Redux    import { useSelector, useDispatch } from 'react-redux'    // 从主题换肤store分库引入setDark方法    import { setDark } from '@/store/slices/theme'+   import ThemeModal from '@/components/themeModal'+   import { globalConfig } from '@/globalConfig'    import './header.styl'
function Header(props) {
...(略)
+ // 是否显示主题色选择对话框+ const [showThemeModal, setShowThemeModal] = useState(false)
return ( <Card className="M-header"> <div className="header-wrapper"> <div className="logo-con">Header:{title}</div> <div className="menu-con"> <Menu mode="horizontal" selectedKeys={location.pathname} items={menuItems} /> </div> <div className="opt-con"> ...(略)- <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组件最右侧的主题色按钮,可以弹出主题色换肤对话框。

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

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

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

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

9.9 安装Redux调试浏览器插件

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

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

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

10 基于axios封装公用API库

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

10.1 安装axios

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

执行:

yarn add axios
10.2 封装公用API库

直接上代码。

更新src/api/index.jsx
import {globalRouters} from '@/router'import axios from 'axios'import { Modal } from 'antd'import { globalConfig } from '@/globalConfig'
// 配合教程演示组件外路由跳转使用,无实际意义export const goto = (path) => { globalRouters.navigate(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页面 goto('/login')}
/* * API请求封装(带验证信息) * 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.jsx,代码如下:
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/main.jsx中引入mock.jsx
    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'+   import './mock'    ...(略)

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

※注:

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

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

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

修改src/pages/login/index.jsx

+   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模拟请求成功了。

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

11 一些细节问题

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

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

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

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

修改src/pages/home/index.jsx
-   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并未跟随主题。

继续修改src/pages/home/index.jsx
    ...(略)    function Home() {        // 获取Design Token        const { token } = useToken()
+ const [modal, contextHolder] = Modal.useModal()
// 退出登录 const exit = () => {+ // 把之前的Modal改为modalM modal.confirm({ title: '是否退出登录?', onOk() { logout() }, }) }
return ( <div className="P-home"> ...(略)+ {+ // 这是最终解决Modal.method跟随换肤的关键,contextHolder在组件DOM中随便找个地方放就行+ contextHolder+ } </div> ) }
export default Home

必要的逻辑直接看注释吧。contextHolder是关键,通过它来获取上下文从而解决主题换肤问题。效果如下:

Ant Design的Modal.useModal()说明:

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


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


※注:

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


11.2 路由守卫

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

修改src/router/index.jsx,加入以下代码:
    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.jsx
    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 设置开发环境的反向代理请求

基于Vite架构的工程,设置反向代理就很容易了,无需安装额外的依赖包。

修改vite.config.js
    ...(略)
// https://vitejs.dev/config/ export default defineConfig({ server: { // 指定dev sever的端口号 port: 3000, // 自动打开浏览器运行以下页面 open: '/',+ // 设置反向代理+ proxy: {+ // 以下示例表示:请求URL中含有"/api",则反向代理到http://localhost+ // 例如: http://localhost:3000/api/login -> http://localhost/api/login+ '/api': {+ target: 'http://localhost/',+ changeOrigin: true,+ },+ }, }, ...(略) })

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

更详细的Vite反向代理设置,请参阅官方说明:

https://cn.vitejs.dev/config/server-options.html#server-proxy

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

11.4 兼容js文件

在本项目中,src目录下并没有使用js文件,而是jsx文件。如果想引入js文件,可以通过以下设置来支持。

修改vite.config.js
    import { defineConfig } from 'vite'    import react from '@vitejs/plugin-react'    import path from 'path'+   import fs from 'fs/promises'        // https://vitejs.dev/config/    export default defineConfig({                ...(略)        +       esbuild: {+           loader: 'jsx',+           include: /src\/.*\.jsx?$/,+           exclude: [],+       },+       optimizeDeps: {+           esbuildOptions: {+               plugins: [+                   {+                       name: 'load-js-files-as-jsx',+                       setup(build) {+                           build.onLoad(+                               { filter: /src\/.*\.js$/ },+                               async (args) => ({+                                   loader: 'jsx',+                                   contents: await fs.readFile(args.path, 'utf8'),+                               })+                           )+                       },+                   },+               ],+           },+       },        plugins: [react()],    })

第一次启动的时候有小概率会报错,但是不影响实际运行,有报错强迫症的不建议使用js:

The JSX syntax extension is not currently enabledThe esbuild loader for this file is currently set to "js" but it must be set to "jsx" to be able to parse JSX syntax. You can use "loader: { '.js': 'jsx' }" to do that.

进行以上配置后,把src目录下某个jsx文件改成js文件,项目依然可以正常运行。

如果你以前项目都是使用js文件,想迁移到Vite架构,但又懒得把js都改成jsx,那就可以用以上配置来实现。

既然Vite默认不支持js,那建议还是使用jsx吧。

11.5 允许dev环境的IP访问

上述内容中,都是通过localhost:3000来访问。如果换成本机IP,则发现不能访问了,这就非常不利于被其他设备访问(例如手机访问dev环境页面)。

支持IP访问,修改vite.config.js
    ...(略)
// https://vitejs.dev/config/ export default defineConfig({ server: {+ // 支持IP访问+ host: true,
...(略)

11.6 批量升级全部项目npm依赖包

如果你希望项目所有依赖包都保持最新版本,推荐一个快速检测package.json是否都为最新版的工具。

执行以下命令进行全局安装:
yarn add -g npm-check-updates
然后在有package.json的目录下执行:
ncu

就会快速检查所有依赖是否存在更新版本。

执行:
ncu -u

则会将package.json中所有依赖修改为最新版本。

最后,再执行:
yarn

进行依赖包的更新安装即可。

※注:更新依赖包有可能会出现不兼容的情况,更新前请先备份好package.json,以便恢复。

12 build项目

在build前还可以做一些配置,以下简述几个常用的配置。

12.1 设置静态资源引用路径

默认情况下,build出来的项目,静态资源引用的一级路径都是"/",建议修改成相对路径"./",这样在部署上线的时候不需要太关注访问目录的问题。

修改vite.config.js
    ...(略)        // https://vitejs.dev/config/    export default defineConfig({+       // 静态资源引用路径,默认为"/"+       base: './',        ...(略)    })

12.2 设置build目录名称及静态资源存放目录

默认情况下,build出来的项目,将静态文件(js、图片等)都存放在assets目录下。也可以通过配置,改为其他名称(例如Create-React-App使用static)。

同样,Vite默认build生成的项目目录名为dist,也可以改为其他名称(例如Create-React-App使用build)。

修改vite.config.js
    ...(略)        // https://vitejs.dev/config/    export default defineConfig({+       build: {+           // build目录名称,默认为"dist"+           outDir: 'build',+           // 静态资源存放目录名称,默认为"assets"+           assetsDir: 'static',+       },        ...(略)    })

12.3 开启build项目生成map文件(不推荐)

map文件,即Javascript的source map文件,是为了解决被混淆压缩的js在调试的时候,能够快速定位到压缩前的源代码的辅助性文件。这个文件发布出去,会暴露源代码。因此,非常不建议在build时生成map文件。

Vite默认是不生成map文件的,但如果需要排查build后的工程,可以配置生成map文件。

修改vite.config.js
    ...(略)
// https://vitejs.dev/config/ export default defineConfig({ build: { // build目录名称,默认为"dist" outDir: 'build', // 静态资源存放目录名称,默认为"assets" assetsDir: 'static',+ // 生成map文件,默认为false(不建议设置)+ sourcemap: true, }, ...(略) })

12.4 执行build项目

执行以下命令即可build项目:
yarn build

与Create-React-App不同的是,Create-React-App build出来的项目,可以直接在本地双击index.html运行,而Vite build的项目必须部署在Sever环境下运行。当然了,项目最终都要运行在Server上。只不过,如果你的客户没有Server,又想在本地进行预览,那Vite这种架构确实在这方面有点不方便。

13 项目Git源码

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

Gitee:

https://gitee.com/betaq/vite-react-app-2023summer


GitHub:

https://github.com/Yuezi32/vite-react-app-2023summer

结束语

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

推荐阅读

2023新春版:手把手教你搭建Electron24+React18+Antd5架构工程

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

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

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

浏览 789
点赞
1评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
全部评论
莫莫莫2023-08-09 16:07
非常好
点赞回复
推荐
点赞
1评论
收藏
分享

手机扫一扫分享

分享
举报