最近真是赶上了大版本升级高峰期,2024年11月26日Vite6正式发布了,紧接着12月5日React19又发布了。Vite6进一步提升了启动和构建性能,增强了TypeScript和原生ESM支持,改进了CSS、插件和SSR 配置,优化了错误提示和调试,强化了生态兼容性,并且支持了最新框架和Node.js版本,为现代浏览器提供更高效的开发体验。随着React19的发布,相关的开源包(Redux Toolkit、Antd等)也随之更新。Antd从5.22.5版本开始逐步适配React19,按照Ant Design官方的计划,Antd v6发布还需要一段时间。为了赶上大版本的首发车,本教程就不等Antd v6了。2023年6月我发布了《2023盛夏版:轻松搞定基于Vite4的React项目全家桶》,时隔一年半,旧版教程已经有些过时了,因此对本系列教程进行全面更新。
本教程的目的就是帮助大家快速掌握当前最新的Vite6+React19全家桶技术栈,跟上前端技术的快车,省去自行摸索的时间,特别适合作为初学者的入门教程。
先睹为快
先看下目录了解本教程都有哪些内容。
章节目录
1 初始化项目
• 1.1 配置npm国内镜像源
• 1.2 使用Vite新建项目
• 1.3 安装并运行项目
• 1.4 精简项目
2 Vite基础配置
• 2.1 支持Sass/Scss/Less/Stylus
• 2.2 设置Vite服务监听端口
• 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为中文语言
• 4.3 兼容React19
• 4.4 批量升级全部项目npm依赖包到最新版本
5 页面开发
• 5.1 构建Login页面
• 5.2 构建公共组件PageHeaderWrapper
• 5.3 构建Home页面
• 5.4 构建Users页面
6 页面路由
• 6.1 构建全局路由
• 6.2 构建框架页面Entry
• 6.3 引入路由
• 6.4 路由跳转:构建左侧导航Sider组件
7 React Developer Tools浏览器插件
8 Redux及Redux Toolkit
• 8.1 安装Redux及Redux Toolkit
• 8.2 创建全局配置文件
• 8.3 创建用于主题换肤的store分库
• 8.4 创建store总库
• 8.5 引入store到项目
• 8.6 store的读取:将主题色和亮暗模式应用于项目
• 8.7 store的变更:在Header组件中实现亮暗模式切换
• 8.8 自建组件使用Antd的主题色:完善Sider
• 8.9 store的使用:实现主题色切换
• 8.9.1 创建主题色选择对话框组件
• 8.9.2 创建自定义SVG图标Icon组件
• 8.9.3 在Header组件中实现主题色切换
• 8.10 安装Redux调试浏览器插件
9 基于axios封装公用API库
• 9.1 安装axios
• 9.2 封装公用API库
• 9.3 Mock.js安装与使用
• 9.4 发起API请求:实现登录功能
10 其他优化
• 10.1 路由守卫
• 10.2 设置开发环境的反向代理请求
• 10.3 使用ESLint9实时检查代码
• 10.4 针对Windows系统的页面滚动条样式优化
11 build项目
• 11.1 设置静态资源引用路径
• 11.2 设置build目录名称及静态资源存放目录(选读)
• 11.3 执行build项目
12 项目Git源码
结束语
本教程的主要依赖包版本:
Node.js 22.12.0
vite 6.0.6
antd 5.22.7
axios 1.7.9
mockjs 1.1.0
react 19.0.0
react-redux 9.2.0
react-router-dom 7.1.1
@reduxjs/toolkit 2.5.0
eslint 9.17.0
less 4.2.1
sass 1.83.0
stylus 0.64.0
vite-plugin-checker 0.8.0
@ant-design/v5-patch-for-react-19 1.0.3
※注:
代码区域每行开头的:
"+" 表示新增
"-" 表示删除
跟着操作一遍,就可以快速上手Vite6啦!下面请跟着新版教程一步步操作。
1 初始化项目
1.1 配置npm国内镜像源
npm默认是从国外源站拉取依赖包的,为提高下载速度和稳定性,建议配置为国内镜像源。
执行:
npm config set registry https://registry.npmmirror.com
执行后,可以通过以下命令查看npm配置,确认是否设置成功:
npm config list
输出的内容中,检查registry的值是否已改为国内镜像源地址:
registry = "https://registry.npmmirror.com/"
1.2 使用Vite新建项目
※注:Vite6要Node.js版本18+或20+。然而,有些模板需要依赖更高的Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。
先进入想要创建项目的目录,在这个目录下执行安装命令:
npm create vite@latest
执行后,会要求填写项目名称,这里我填写的是vite-react-app,可根据情况自定。
? Project name: » vite-react-app
然后,会要求选择框架,选择React:
? Select a framework:
Vanilla
Vue
> React
Preact
Lit
Svelte
Solid
Qwik
Angular
Others
最后,选择开发语言,本教程选择JavaScript:
? Select a variant:
TypeScript
TypeScript + SWC
> JavaScript
JavaScript + SWC
React Router v7 ↗
回答以上“灵魂三问”后,即可完成Vite项目创建。
1.3 安装并运行项目
进入项目目录,运行命令进行项目依赖包的安装。
cd vite-react-app
npm install
稍等片刻,安装完成后,执行以下命令运行项目:
npm run dev
需要手动打开以下地址访问项目:
http://localhost:5173/
Vite默认开启的端口是5173,后续章节会讲解怎么修改端口号。
1.4 精简项目
接下来,删除用不到的文件,最简化项目。
├─ /node_modules
├─ /public
- | └─ vite.svg
├─ /src
- | ├─ /assets
- | | └─ react.svg
- | ├─ App.css
| ├─ App.jsx
- | ├─ index.css
| └─ main.jsx
├─ .gitignore
├─ eslint.config.js
├─ index.html
├─ package-lock.json
├─ package.json
├─ README.md
└─ vite.config.js
现在目录结构如下,清爽许多:
├─ /node_modules
├─ /public
├─ /src
| ├─ App.jsx
| └─ main.jsx
├─ .gitignore
├─ eslint.config.js
├─ index.html
├─ package-lock.json
├─ package.json
├─ README.md
└─ vite.config.js
以上文件删除后,页面会报错。这是因为相应的文件引用已不存在。需要继续修改代码,先让项目正常运行起来。
逐个修改以下文件。
src/App.jsx:
function App() {
return <div className="App">Vite-React-App</div>
}
export default App
src/main.jsx:
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
├─ .gitignore
├─ eslint.config.js
├─ index.html
├─ package-lock.json
├─ package.json
├─ README.md
└─ vite.config.js
这里你可能会问,为什么在index.html
的<link>
中引入图标的路径是/favicon.ico
,而favicon.ico明明是放在/public
目录下,却不是/public/favicon.ico
呢?
按照Vite官方说明:引入public中的资源永远应该使用根绝对路径
,并且,public中的资源不应该被JavaScript文件引用。
public目录Vite官方说明:
运行项目,执行:
npm run dev
页面效果如下:
2 Vite基础配置
2.1 支持Sass/Scss/Less/Stylus
Vite本身提供了对.scss/.sass/.less/.styl/.stylus文件的内置支持。无需再安装特定的Vite插件,但必须安装相应的预处理器依赖。
支持Sass/Scss,执行以下命令安装:
npm install -D sass
支持Less,执行以下命令安装:
npm install -D less
支持Stylus,执行以下命令安装:
npm install -D stylus
安装后,就可以直接使用以上对应的CSS预处理语言了,非常方便。
CSS预处理器Vite官方说明:
2.2 设置Vite服务监听端口
默认情况下,在执行npm run dev
后,Vite只允许通过localhost访问。有时候,需要让其他人通过你的IP访问你的Vite项目,这就需要开放所有IP,设置如下:
修改vite.config.js
:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
+ server: {
+ // 监听所有IP地址
+ host: '0.0.0.0',
+ },
plugins: [react()],
})
2.3 设置dev环境的Server端口号
dev server默认端口是5173,如果想修改为其他端口(例如:3000端口),可以进行以下设置。
修改vite.config.js
:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
server: {
// 监听所有IP地址
host: '0.0.0.0',
+ // 指定dev sever的端口号,默认为5173
+ port: 3000,
},
plugins: [react()],
})
2.4 设置dev环境自动打开浏览器
Vite在启动dev环境时,默认情况并不会自动打开浏览器。如果想自动打开浏览器,可进行以下设置。
修改vite.config.js
:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
server: {
// 监听所有IP地址
host: '0.0.0.0',
// 指定dev sever的端口号,默认为5173
port: 3000,
+ // 自动打开浏览器运行以下路径的页面
+ open: '/',
},
...(略)
})
open的"/"值,表示打开localhost:3000/
。
如果想直接打开其他页面,例如localhost:3000/#/home
,将open的值设置为/#/home
即可。
※注:open的值修改后,虽然已经生效,但不会直接触发打开浏览器的行为。这个行为只发生在通过命令启动项目的时候,也就是执行npm run dev的时候。
2.5 设置路径别名
为了避免使用相对路径的麻烦,可以设置路径别名。
修改vite.config.js
:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+ import path from 'path'
// https://vite.dev/config/
export default defineConfig({
server: {
// 监听所有IP地址
host: '0.0.0.0',
// 指定dev sever的端口号,默认为5173
port: 3000,
// 自动打开浏览器运行以下路径的页面
open: '/',
},
+ resolve: {
+ // 路径别名配置
+ alias: {
+ '@': path.resolve(import.meta.dirname, 'src'),
+ },
+ },
plugins: [react()],
})
这样在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数据文件
├─.gitignore
├─ eslint.config.js <-- ESLint配置文件
├─ index.html <-- HTML页模板
├─ package-lock.json
├─ package.json
├─ README.md
└─ vite.config.js <-- Vite配置文件
这里需要注意的是,基于Vite脚手架的工程在src目录中是以jsx文件进行开发。虽然可以通过修改Vite配置来兼容js文件,但不建议这么做。相反,在src目录之外,则可以使用js文件(例如:vite.config.js、eslint.config.js)。
另外,以上项目结构已经没有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.styl代码较多,这里不再放出。非常推荐参考这个reset css,代码比较全面,更新也比较及时(截至本文写作时,是2024年8月25日更新的)。
具体代码详见:github.com/elad2412/th…
新建全局样式文件,src/common/styles/global.styl
:
html, body, #root
height: 100%
.G-main
padding: 20px
.G-fullpage
display: flex
height: 100%
.G-layout-main
flex: 1
overflow: auto
全局样式将应用于项目的所有页面和组件,可根据需要自行补充或调整。
新建全局样式总入口文件,src/common/styles/frame.styl
:
@import './reset.styl';
@import './global.styl';
在frame.styl
里引入其他公用样式,就方便一次性全部应用到项目中了。
然后在src/main.jsx
里引入frame.styl
:
import ReactDOM from 'react-dom/client'
import App from '@/App'
+ // 全局样式
+ import '@/common/styles/frame.styl'
ReactDOM.createRoot(document.getElementById('root')).render(<App />)
这样在所有页面里就可以直接使用全局样式了。
现在运行项目,可以发现reset.styl、global.styl中的样式已经生效。
4 引入Ant Design 5.x
Ant Design是一款非常优秀的UI库,在React项目开发中使用非常广泛。Ant Design发布5.x后,使用起来更加快捷,而且在主题换肤方面更加便捷。本次分享也特别说明下如何使用Ant Design(以下简称Antd)。
4.1 安装Ant Design
执行:
npm install 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
可以看到Antd的Button组件正常显示出来了。
※注:Antd 5.x已经没有全局污染的reset样式了。因此不用再担心使用了Antd会影响页面样式。
4.2 设置Antd为中文语言
Antd默认语言是英文,需进行以下设置调整为中文。
修改src/main.jsx
:
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'
- ReactDOM.createRoot(document.getElementById('root')).render(<App />)
+ ReactDOM.createRoot(document.getElementById('root')).render(
+ <ConfigProvider locale={zhCN}>
+ <App />
+ </ConfigProvider>
+ )
现在还没开始构建页面,因此关于Antd 5.x酷炫的主题换肤在后续章节再讲解,先别着急。
4.3 兼容React19
目前,Antd 5.x并没有完全适配React19,在使用过程中会有警告或者错误。
但是,Antd官方已经推出了兼容方案,就是安装兼容包@ant-design/v5-patch-for-react-19。
执行安装命令:
npm install --save-dev @ant-design/v5-patch-for-react-19
然后,在项目的入口文件引入兼容包。
修改src/main.jsx:
...(略)
import { ConfigProvider } from 'antd'
+ import '@ant-design/v5-patch-for-react-19'
...(略)
这样就可以让Antd 5.x兼容React19了。
尽管如此,还是非常期待Antd v6的发布,能够更好地发挥性能与React19搭配使用。
Ant Design官方说明:React 19 兼容
4.4 批量升级全部项目npm依赖包到最新版本
本教程在创建vite项目时使用的create-vite版本为6.1.1,其生成的项目所依赖的React版本为18.3.1,并不是19。
推荐一个快速检测package.json是否都为最新版的工具。
执行以下命令进行全局安装:
npm install -g npm-check-updates
然后在项目根目录(有package.json文件的目录)下执行:
ncu
就会快速检查所有依赖是否存在更新版本。
执行:
ncu -u
则会将package.json中所有依赖修改为最新版本。
最后,再执行:
npm install
进行依赖包的更新安装即可。
※注:更新依赖包有可能会出现不兼容的情况,更新前请先备份好package.json,以便恢复。
经过以上批量升级操作,项目的全部依赖包会升级到最新版本。
编写本教程时,react及相关依赖包升级到的最新版本如下:
react 19.0.0
react-dom 19.0.0
@types/react 19.0.2
@types/react-dom 19.0.2
eslint-plugin-react 7.37.3
eslint-plugin-react-hooks 5.1.0
vite 6.0.6
5 页面开发
本次教程将基于Ant Design搭建一个简易的管理系统DEMO,涉及以下知识点:
- 页面路由跳转
- 二级路由
- 登录状态验证(路由守卫)
- 主题色换肤
- 亮色/暗色模式
特例是:登录页Login不参与主题色换肤、亮色/暗色模式、不验证登录状态。
5.1 构建Login页面
页面构建代码不再详述,都是很基础的内容了。
新建src/pages/login/index.jsx
:
import { useState } from 'react'
import { Button, Form, Input } from 'antd'
import { KeyOutlined, UserOutlined } from '@ant-design/icons'
import imgLogo from '@/common/images/logo.png'
import imgCover from './cover.svg'
import './login.styl'
function Login() {
// 登录按钮loading状态
const [loading, setLoading] = useState(false)
// 提交登录
const loginSubmit = (values) => {
setLoading(true)
let data = {
account: values.account,
password: values.password,
}
console.log('submitData', data)
}
return (
<div className="P-login">
<div className="login-con">
<div className="cover-con">
<img src={imgLogo} alt="" className="img-logo" />
<h2>Vite React APP</h2>
<img src={imgCover} alt="" className="img-cover" />
<div className="m-footer">公众号:卧梅又闻花</div>
</div>
<div className="pannel-con">
<h3>Welcome!</h3>
<p className="subtext">请登录您的账号</p>
<Form onFinish={loginSubmit}>
<Form.Item
name="account"
rules={[
{ required: true, message: '请输入您的账号' },
]}
>
<Input
size="large"
placeholder="请输入账号"
prefix={<UserOutlined />}
autoComplete="account"
/>
</Form.Item>
<Form.Item
name="password"
rules={[
{
required: true,
message: '请输入您的密码',
},
]}
>
<Input.Password
size="large"
placeholder="请输入密码"
prefix={<KeyOutlined />}
autoComplete="password"
/>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
size="large"
block={true}
loading={loading}
>
登录
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
)
}
export default Login
别忘了以上代码中有两张图片文件要添加到项目中:
src/common/images/logo.png
src/pages/login/cover.svg
新建src/pages/login/login.styl
:
.P-login
position: absolute
top: 0
bottom: 0
left: 0
right: 0
background: #4b72fe
overflow: hidden
.login-con
position: absolute
top: 50%
left: 50%
z-index: 10
display: flex
margin: -280px 0 0 -450px
width: 900px
height: 560px
background: #fff
border-radius: 8px
box-shadow: 0 0 6px rgba(0,0,0,0.4)
overflow: hidden
.cover-con
position: relative
width: 500px
height: 100%
background: #f1f3ff
.img-logo
display: block
margin: 50px auto 20px
height: 60px
h2
margin-bottom: 30px
text-align: center
font-size: 20px
font-weight: bold
color: #646672
.img-cover
display: block
margin: 0 auto
width: 300px
.m-footer
margin-top: 55px
text-align: center
font-size: 12px
color: #646672
.pannel-con
flex: 1
padding: 100px 60px 0
height 100%
background: #fff
color:#333
.img-con
margin-top: 100px
text-align: center
img
width: 300px
h3
margin-bottom: 10px
font-size: 30px
font-weight: bold
text-align: center
.subtext
margin-bottom: 40px
text-align: center
暂时修改下入口文件代码,把原App页面换成Login页面。
修改src/main.jsx
:
- import App from '@/App'
+ import App from '@/pages/login'
运行效果如下:
输入账号和密码,然后点击登录,可以看到登录按钮变成了loading状态。在调试工具中可以看到提交的账号和密码数据。
5.2 构建公共组件PageHeaderWrapper
PageHeaderWrapper放置于每个页面中,用来显示当前页面的名称或者面包屑导航。
新建src/components/pageHeaderWrapper/index.jsx
:
import { Card } from 'antd'
import './pageHeaderWrapper.styl'
function PageHeaderWrapper(props) {
const { title, subtitle } = props
return (
<Card className="M-pageHeaderWrapper" bordered={false}>
<span className="page-header-title">{title}</span>
<span className="page-header-subtitle">{subtitle}</span>
</Card>
)
}
export default PageHeaderWrapper
新建src/components/pageHeaderWrapper/pageHeaderWrapper.styl
:
.M-pageHeaderWrapper
border-radius: 0
.ant-card-body
padding: 16px 24px
.page-header-title
margin-right: 10px
font-size: 20px
font-weight: bold
实际效果将在后续的页面中展示。
5.3 构建Home页面
直接上代码。
新建src/pages/home/index.jsx
:
import { Card } from 'antd'
import PageHeaderWrapper from '@/components/pageHeaderWrapper'
import './home.styl'
function Home() {
return (
<>
<PageHeaderWrapper title="首页" subtitle="(副标题)" />
<section className="G-main P-home">
<Card title="主题换肤说明">
<p>
1. 管理后台中,每一个面板模块,使用Antd的<Card
/>组件进行作为最外层,以便适用主题换肤。
</p>
<p>
2.
任何自行写的样式颜色,都不会换肤。如需换肤,使用Antd的useToken获取当前主题token中对应的颜色值进行换肤。
</p>
</Card>
<Card title="版权声明">
<p>公众号:卧梅又闻花</p>
<p>掘金:Mr_兔子先生</p>
<p>知乎:兔子先生</p>
</Card>
</section>
</>
)
}
export default Home
Home页面引入了公共组件<PageHeaderWrapper>
。
新建src/pages/home/home.styl
:
.P-home
display: flex
flex-direction: column
gap: 20px
暂时修改下入口文件代码,把初始页面换成Home页面。
修改src/main.jsx
:
- import App from '@/pages/login'
+ import App from '@/pages/home'
运行效果如下:
5.4 构建Users页面
基本与Home页面一样,直接上代码。
新建src/pages/users/index.jsx
:
import { Card } from 'antd'
import PageHeaderWrapper from '@/components/pageHeaderWrapper'
import './users.styl'
function Users() {
return (
<>
<PageHeaderWrapper title="用户管理" subtitle="(副标题)" />
<section className="G-main P-users">
<Card>Users页面</Card>
</section>
</>
)
}
export default Users
先创建Users页面对应的样式文件,新建src/pages/users/users.styl
。
代码先空着。
同样,暂时修改下入口文件代码,把初始页面换成Users页面。
修改src/main.jsx:
- import App from '@/pages/home'
+ import App from '@/pages/users'
运行效果如下:
6 页面路由
本章节将基于react-router-dom,实现Login、Home、Users三个页面的跳转。
但是,Login、Home、Users三个页面并不是平级的关系。这是因为,大多数的管理系统界面主要由顶部、左侧导航、主区域三部分组成。Home页面和Users页面就是嵌套框架页面的主区域中的。本教程将框架页面起名为Entry
。
因此:
一级路由页面包括:Login、Entry
二级路由页面包括:Home、Users
6.1 构建全局路由
安装react-router-dom,执行:
npm install react-router-dom
接下来进行路由配置。
新建src/router/globalRouter.jsx
:
import { Suspense, lazy } from 'react'
import { createHashRouter, Navigate } from 'react-router-dom'
import { Spin } from 'antd'
import Login from '@/pages/login'
import Home from '@/pages/home'
import Users from '@/pages/users'
// 全局路由
function globalRoute() {
// 二级路由框架页面采用懒加载方式
const Entry = lazy(() => import('@/pages/entry'))
return createHashRouter([
{
// 精确匹配"/login",跳转Login页面
path: '/login',
element: <Login />,
},
{
// 未匹配"/login",则进入到Entry页面
path: '/',
element: (
// 懒加载过程中先使用Spin组件占位
<Suspense fallback={<Spin />}>
<Entry />
</Suspense>
),
// 定义Entry二级路由
children: [
{
// 精确匹配"/home",跳转Home页面
path: '/home',
element: <Home />,
},
{
// 精确匹配"/users",跳转Home页面
path: '/users',
element: <Users />,
},
{
// 如果URL没有"#路由",跳转Home页面
path: '/',
element: <Navigate to="/home" />,
},
{
// 未匹配,跳转Login页面
path: '*',
element: <Navigate to="/login" />,
},
],
},
])
}
const globalRouter = globalRoute()
export default globalRouter
构建路由组件。
新建src/router/index.jsx
:
import { RouterProvider } from 'react-router-dom'
import globalRouter from '@/router/globalRouter'
function Routers() {
return <RouterProvider router={globalRouter} />
}
export default Routers
以上代码,使用<RouterProvider>
实现了路由跳转。
6.2 构建框架页面Entry
接下来开始构建框架页面,把页面和组件的“坑”挖好。
新建src/pages/entry/index.jsx
:
import { Outlet } from 'react-router-dom'
import { Layout } from 'antd'
const { Content, Sider } = Layout
function Entry() {
return (
<Layout className="G-fullpage">
<div style={{ height: 70, background: '#bae0ff', fontSize: 20 }}>Header</div>
<Layout style={{ overflow: 'hidden', flex: 1 }}>
<Sider />
<Layout>
<div className="G-layout-main">
<Content style={{ minWidth: 800 }}>
{/* Outlet用来放置二级路由页面 */}
<Outlet />
</Content>
</div>
</Layout>
</Layout>
</Layout>
)
}
export default Entry
以上代码,使用Antd的Layout组件,为Header、Sider和主区域划分好了空间。<Outlet>
就是为Home和Users页面挖好的“坑”。
6.3 引入路由
下面将路由引入到项目中。
修改src/main.jsx
:
import ReactDOM from 'react-dom/client'
- import App from '@/pages/users'
+ import Routers from '@/router'
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 />
+ <Routers />
</ConfigProvider>
)
路由组件<Routers>
替换掉了原来的<App>
,从此与src/App.jsx
文件告别了。
记得删掉src/App.jsx
。
页面对应的地址如下:
Login页面: http://localhost:3000/#/login
Home页面: http://localhost:3000/#/home
Users页面: http://localhost:3000/#/users
嵌入Entry的Home页面效果:
嵌入Entry的Users页面效果:
6.4 路由跳转:构建左侧导航Sider组件
目前,框架页面Entry里的Sider只是一个临时占位。本小节来构建一个完整的Sider组件。
新建src/components/sider/index.jsx
:
import { useState } from 'react'
import { Layout, Menu } from 'antd'
import { useNavigate, useLocation } from 'react-router-dom'
import {
HomeOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
UserOutlined,
} from '@ant-design/icons'
import './sider.styl'
const AntdSider = Layout.Sider
function Sider() {
// 当前路由地址
const location = useLocation()
// 路由hook
const navigate = useNavigate()
// 左侧导航的开合状态
const [collapsed, setCollapsed] = useState(false)
// 左侧导航列表
const items = [
{
label: '首页',
key: '/home',
icon: <HomeOutlined />,
children: null,
type: null,
onClick: () => {
// 跳转至路径地址/home
navigate('/home')
},
},
{
label: '用户管理',
key: '/users',
icon: <UserOutlined />,
children: null,
type: null,
onClick: () => {
// 跳转至路径地址/users
navigate('/users')
},
},
]
// 切换左侧导航开合状态
const onCollapse = () => {
setCollapsed(!collapsed)
}
// 左侧导航展开时的宽度
const fullWidth = 200
// 左侧导航收起时的宽度
const collapsedWidth = 49
return (
<AntdSider
className="M-sider"
trigger={null}
collapsible
collapsed={collapsed}
collapsedWidth={collapsedWidth}
width={fullWidth}
>
<div className="sider-main">
<Menu
mode="inline"
selectedKeys={location.pathname}
items={items}
className="sider-menu"
></Menu>
<div className="sider-footer" onClick={onCollapse} style={{ color: '#ffffff' }}>
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</div>
</div>
</AntdSider>
)
}
export default Sider
新建src/components/sider/sider.styl
:
.M-sider
overflow: hidden
border-right-style: solid
border-right-width: 1px
.sider-main
display: flex
flex-direction: column
height: 100%
.sider-menu
flex: 1
overflow: hidden
overflow-y: auto
.sider-footer
padding: 10px 14px
height: 40px
cursor: pointer
border-top-style: solid
border-top-width: 1px
.anticon
font-size: 20px
在Entry中引入Sider组件,修改src/pages/entry/index.jsx
:
import { Outlet } from 'react-router-dom'
import { Layout } from 'antd'
+ import Sider from '@/components/sider'
- const { Content, Sider } = Layout
+ const { Content } = Layout
...(略)
替换掉了原来的Sider占位,应用自建的Sider。
效果如下:
Home页面:http://localhost:3000/#/home
Users页面:http://localhost:3000/#/users
左侧导航收起状态:
现在看上去还挺丑的,别着急,后续章节会讲解Antd主题色及暗色模式,会再次修改Sider组件样式的。
7 React Developer Tools浏览器插件
为了更方便调试React项目,建议安装Chrome插件。
先科学上网,在Chrome网上应用店里搜索“React Developer Tools”并安装。
安装完成后,打开Chrome DevTools,点击Components按钮,可以清晰的看到React项目代码结构以及各种传参。
8 Redux及Redux Toolkit
Redux是用来做什么的?简单通俗的解释,Redux是用来管理项目级别的全局变量,而且是可以实时监听变化并改变DOM的。当多个模块都需要动态显示同一个数据,并且这些模块从属于不同的父组件,或者在不同的页面中,如果没有Redux,那实现起来就很麻烦了,问题追踪也很痛苦。Redux就是解决这个问题的。
做过Vue开发的同学都知道Vuex,React对应的工具就是Redux。React官方推出了Redux Toolkit,一个开箱即用的高效的Redux开发工具集,使用起来也很简洁。
8.1 安装Redux及Redux Toolkit
执行:
npm install @reduxjs/toolkit react-redux
8.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',
],
// 左侧导航sider主题,light=亮色,dark=暗色,theme=跟随主题
siderTheme: 'theme',
// localStroge用户登录信息标识
SESSION_LOGIN_INFO: 'userLoginInfo',
// localStroge用户主题信息标识:
SESSION_LOGIN_THEME: 'userTheme',
}
globalConfig.jsx其实与Redux没有太深入的关系,只是为了方便配置一些初始化默认值而已,以及定义localStorage的变量名,这么做就是为了把配置项都抽离出来方便维护。
8.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.actions
export const { setColorPrimary } = themeSlice.actions
export default themeSlice.reducer
再啰嗦一下这部分的关键逻辑:
先从localStorage里获取主题配置,这么做是为了将用户的主题配置保存在浏览器中,用户在刷新或者重新打开该项目的时候,会直接应用之前设置的主题配置。
如果localStorage没有主题配置,则从globalConfig读取默认值,然后再写入localStorage。这种情况一般是用户使用当前浏览器第一次浏览该项目时会用到。
setDark用来设置“亮色/暗色主题”,setColorPrimary用来设置“主题色”。每次设置后,除了变更store里的值(为了项目全局动态及时生效),还要同步写入localStorage(为了刷新或重新打开时及时生效)。
“亮色/暗色主题”和“主题色”虽然都是颜色改变,但是完全不同的两个维度的换肤。“亮色/暗色主题”主要是对默认的文字、背景、边框等基础元素进行黑白切换,而“主题色”则是对带有“品牌色”的按钮等控件进行不同色系的颜色切换。
8.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
// 可以根据需要在这里继续追加其他分库
},
})
原理就是创建总库,把各个分库都汇总起来。注释已写明,不再赘述。
8.5 引入store到项目
首先,将store引入到项目工程中。
修改src/main.jsx
:
import ReactDOM from 'react-dom/client'
import Routers 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}>
<Routers />
</ConfigProvider>
+ </Provider>
)
其实就是用react-redux提供的Provider带上store把项目包起来,这样整个项目就可以随时随地访问store了。
8.6 store的读取:将主题色和亮暗模式应用于项目
接下来,让Antd组件从redux中读取主题色和亮暗模式配置。
修改src/pages/entry/index.jsx
:
import { Outlet } from 'react-router-dom'
+ import { useSelector } from 'react-redux'
- import { Layout } from 'antd'
+ import { ConfigProvider, Layout, theme } from 'antd'
import Sider from '@/components/sider'
+ const { darkAlgorithm, defaultAlgorithm } = theme
const { Content } = Layout
function Entry() {
+ // 使用useSelector获取store中的theme
+ const globalTheme = useSelector((state) => state.theme)
+
+ // 在body上添加theme-mode属性,标记当前主题模式(便于实现亮暗模式下的CSS差异化)
+ globalTheme.dark
+ ? document.body.setAttribute('theme-mode', 'dark')
+ : document.body.setAttribute('theme-mode', 'light')
+
+ // Ant Design主题
+ let antdTheme = {
+ algorithm: globalTheme.dark ? darkAlgorithm : defaultAlgorithm,
+ }
+ // 应用自定义主题色
+ if (globalTheme.colorPrimary) {
+ antdTheme.token = {
+ colorPrimary: globalTheme.colorPrimary,
+ }
+ }
return (
+ <ConfigProvider theme={antdTheme}>
<Layout className="G-fullpage">
...(略)
</Layout>
+ </ConfigProvider>
)
}
export default Entry
必要的地方已添加注释,不再赘述。核心就是使用Redux提供的useSelector来获取store中的theme。当store中的theme发生改变时,使用它的组件也会及时更新。
※注: 因为Login页面不参与主题色换肤,所以只对Entry页面套用了Antd的主题色。
8.7 store的变更:在Header组件中实现亮暗模式切换
在上一章节中,通过useSelector实现了store的读取。本章节介绍如何使用useDispatch进行store的变更。
主题换肤的交互操作位于Header组件。
新建src/components/header/index.jsx
:
import { useSelector, useDispatch } from 'react-redux'
import { setDark } from '@/store/slices/theme'
import { Button, Card, Dropdown, Modal } from 'antd'
import {
ExportOutlined,
CaretDownOutlined,
SunOutlined,
MoonOutlined,
} from '@ant-design/icons'
import { globalConfig } from '@/globalConfig'
import { useNavigate } from 'react-router-dom'
import logoImg from '@/common/images/logo.png'
import './header.styl'
function Header() {
// 获取redux派发钩子
const dispatch = useDispatch()
// 路由hook
const navigate = useNavigate()
// 主题配置
const theme = useSelector((state) => state.theme)
// Antd的Modal组件API
const [modal, contextHolder] = Modal.useModal()
// 退出登录
const logout = () => {
modal.confirm({
title: '确定要退出系统么?',
okText: '退出',
onOk: () => {
// 清空localStorage
window.localStorage.removeItem(globalConfig.SESSION_LOGIN_INFO)
// 跳转至路径地址/login
navigate('/login')
},
})
}
const loginInfo = JSON.parse(
window.localStorage.getItem(globalConfig.SESSION_LOGIN_INFO)
)
const menuItems = [
{
label: '退出登录',
key: 'exit',
icon: <ExportOutlined />,
onClick: logout,
},
]
return (
<Card className="M-header" bordered={false}>
<div className="header-wrapper">
<div className="logo-con">
<img src={logoImg} alt="" />
<span className="logo-text">Vite React APP</span>
</div>
<div className="header-con">
{theme.dark ? (
<Button
icon={<SunOutlined />}
shape="circle"
onClick={() => {
dispatch(setDark(false))
}}
></Button>
) : (
<Button
icon={<MoonOutlined />}
shape="circle"
onClick={() => {
dispatch(setDark(true))
}}
></Button>
)}
<Dropdown menu={{ items: menuItems }}>
<div className="user-menu">
<span>
{loginInfo ? loginInfo.nickname : '未登录'}
</span>
<CaretDownOutlined className="arrow" />
</div>
</Dropdown>
</div>
</div>
{contextHolder}
</Card>
)
}
export default Header
需要说明以下4点:
- 使用useDispatch来派发对store的修改,使用方式就是
dispatch(setDark(false))
。setDark
是在theme的reducer(src/store/slices/theme.jsx
)中定义好的方法。 - 使用useModal来调用Antd的Modal组件,
contextHolder
随意放置在组件任何位置即可(建议放置组件内部的最下面)。 - 使用useNavigate来实现路由跳转。
- 使用useSelector来获取store中的theme。
一个小小的Header组件,容纳了很多核心知识点。
新建src/components/header/header.styl
:
.M-header
position: relative
z-index: 10
margin-bottom: 2px
border-radius: 0
overflow hidden
.ant-card-body
padding: 16px 24px
height: 62px
line-height: 32px
.header-wrapper
display: flex
min-width: 520px
.logo-con
display: flex
img
height: 32px
.logo-text
margin-left: 10px
font-size: 18px
font-weight: bold
.header-con
display: flex
flex: 1
justify-content: flex-end
gap: 20px
.user-menu
cursor pointer
.avatar
width: 42px
height: 42px
border-radius: 50%
.arrow
margin-left: 10px
在Entry中引入Header组件。
修改src/pages/entry/index.jsx
:
import { Outlet } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { ConfigProvider, Layout, theme } from 'antd'
import Sider from '@/components/sider'
+ import Header from '@/components/header'
const { darkAlgorithm, defaultAlgorithm } = theme
const { Content } = Layout
function Entry() {
...(略)
return (
<ConfigProvider theme={antdTheme}>
<Layout className="G-fullpage">
- <div style={{ height: 70, background: '#bae0ff', fontSize: 20 }}>Header</div>
+ <Header />
<Layout style={{ overflow: 'hidden', flex: 1 }}>
...(略)
运行效果如下:
点击“月亮”小图标按钮后,即可切换到暗色模式:
细心的你应该会发现,左侧导航Sider的底部没有跟随亮暗模式。接下来,结合下一章节的知识点来完善Sider组件。
8.8 自建组件使用Antd的主题色:完善Sider
修改src/components/sider/index.jsx
:
import { useState } from 'react'
- import { Layout, Menu } from 'antd'
+ import { Layout, Menu, theme } from 'antd'
+ import { useSelector } from 'react-redux'
+ import { globalConfig } from '@/globalConfig'
import { useNavigate, useLocation } from 'react-router-dom'
import {
HomeOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
UserOutlined,
} from '@ant-design/icons'
import './sider.styl'
const AntdSider = Layout.Sider
+ const { useToken } = theme
function Sider() {
+ // 获取store中的主题色配置
+ const globalTheme = useSelector((state) => state.theme)
+ // Antd的主题色hook
+ const { token } = useToken()
// 当前路由地址
const location = useLocation()
// 路由hook
const navigate = useNavigate()
// 左侧导航的开合状态
const [collapsed, setCollapsed] = useState(false)
// 左侧导航列表
const items = [
...(略)
]
// 切换左侧导航开合状态
const onCollapse = () => {
setCollapsed(!collapsed)
}
+ // 判断sider主题色
+ let siderTheme = 'dark'
+ if (globalConfig.siderTheme === 'light') {
+ siderTheme = 'light'
+ } else if (globalConfig.siderTheme === 'theme') {
+ globalTheme.dark ? (siderTheme = 'dark') : (siderTheme = 'light')
+ }
// 左侧导航展开时的宽度
const fullWidth = 200
// 左侧导航收起时的宽度
const collapsedWidth = 49
return (
<AntdSider
className="M-sider"
trigger={null}
collapsible
collapsed={collapsed}
collapsedWidth={collapsedWidth}
width={fullWidth}
+ theme={siderTheme}
+ style={{
+ backgroundColor: token.colorBgContainer,
+ borderColor: token.colorBorderSecondary,
+ }}
>
<div className="sider-main">
<Menu
mode="inline"
selectedKeys={location.pathname}
items={items}
className="sider-menu"
></Menu>
<div
className="sider-footer"
onClick={onCollapse}
- style={{ color: '#ffffff' }}
+ style={{
+ color: token.colorTextBase,
+ borderTopColor: token.colorBorder,
+ }}
>
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</div>
</div>
</AntdSider>
)
}
export default Sider
Sider底部的<div className="sider-footer>
是自建的元素,除了文字色,其他并不会跟随Antd主题色。这里使用了Antd的useToken
来实现主题色跟随,从token
中可以选择Antd提供的各种颜色变量。因此,在Antd项目中,对于自建元素的颜色,要重点考虑使用Antd的useToken
。
至于都有哪些主题色变量,可以前往Ant Design主题编辑器查看。
Ant Design主题编辑器
再来看看Sider的效果,舒服多了:
8.9 store的使用:实现主题色切换
在src/globalConfig.jsx
里的customColorPrimarys就是留给主题色换肤的。接下来讲解下具体实现方法。为了让交互体验稍微好一点,通过Antd的Modal组件来制作主题色选择功能。
8.9.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 }) {
const dispatch = useDispatch()
// 主题配置
const theme = useSelector((state) => state.theme)
return (
<Modal
className="M-themeModal"
open={true}
title="主题色"
onCancel={() => {
onClose()
}}
maskClosable={false}
footer={null}
>
<div className="colors-con">
{globalConfig.customColorPrimarys &&
globalConfig.customColorPrimarys.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
这个组件将在<Header>
组件中调用,详见后续第8.9.3章节。
8.9.2 创建自定义SVG图标Icon组件
Antd自带了很多Icon,非常方便直接使用。但在项目中遇到Antd没有的图标怎么办?当然,前提要求是自己构建的图标也能支持随时改变颜色和大小等样式。
上一章节创建的<ThemeModal>
组件将引入到<Header>
中,但是Antd没有提供主题色换肤的Icon。正好,来讲解一下如何创建自定义SVG图标Icon组件。
第一步:创建自定义图标库
新建src/components/extraIcons/index.jsx
:
import Icon from '@ant-design/icons'
const ThemeSvg = () => (
// 这里粘贴“主题色”图标的SVG代码
)
export const ThemeOutlined = (props) => <Icon component={ThemeSvg} {...props} />
第二步:在iconfont网站(www.iconfont.cn)找到心仪的图片,然后点击下载按钮。找到心仪的图片,然后点击下载按钮。)
第三步:在弹出的图标详情弹层里,点击“复制SVG代码”。
第四步:将选好的SVG代码依次粘贴到src/components/extraIcons/index.jsx
中对应的位置。
※注:一定要仔细坚持以下三方面。
检查svg代码中是否有class以及与颜色相关的fill、stroke等属性,如有,必须连带属性一起删除。像p-id这种自定义的属性也可以删除。
确保<SVG>标签中有fill="currentColor",否则图标的颜色将不能改变。
确保<SVG>标签中width和height属性的值为1em,否则图标的大小将不能改变。
这里以“主题色”图标为例:
<svg
- t="1733378443800"
- class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
- p-id="5017"
- width="512"
+ width="1em"
- height="512"
+ height="1em"
+ fill="currentColor"
>
<path
d="...(略)"
- p-id="5018"
- fill="#666666"
></path>
<path
d="...(略)"
- p-id="5019"
- fill="#666666"
></path>
<path
d="...(略)"
- p-id="5019"
- fill="#666666"
></path>
<path
d="...(略)"
- p-id="5020"
- fill="#666666"
></path>
</svg>
这样,自定义Icon就制作好了。使用方法在下一小节介绍。
8.9.3 在Header组件中实现主题色切换
修改src/components/header/jsx
:
+ import { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { setDark } from '@/store/slices/theme'
import { Button, Card, Dropdown, Modal } from 'antd'
import {
ExportOutlined,
CaretDownOutlined,
SunOutlined,
MoonOutlined,
} from '@ant-design/icons'
+ import { ThemeOutlined } from '@/components/extraIcons'
+ import ThemeModal from '@/components/themeModal'
import { globalConfig } from '@/globalConfig'
import { useNavigate } from 'react-router-dom'
import logoImg from '@/common/images/logo.png'
import './header.styl'
function Header() {
// 获取redux派发钩子
const dispatch = useDispatch()
// 路由hook
const navigate = useNavigate()
// 主题配置
const theme = useSelector((state) => state.theme)
// Antd的Modal组件API
const [modal, contextHolder] = Modal.useModal()
+ // 是否显示主题色Modal
+ const [showThemeModal, setShowThemeModal] = useState(false)
// 退出登录
const logout = () => {
...(略)
}
const loginInfo = JSON.parse(
window.localStorage.getItem(globalConfig.SESSION_LOGIN_INFO)
)
const menuItems = [
...(略)
]
return (
<Card className="M-header" bordered={false}>
<div className="header-wrapper">
<div className="logo-con">
<img src={logoImg} alt="" />
<span className="logo-text">Vite React APP</span>
</div>
<div className="header-con">
{theme.dark ? (
<Button
icon={<SunOutlined />}
shape="circle"
onClick={() => {
dispatch(setDark(false))
}}
></Button>
) : (
<Button
icon={<MoonOutlined />}
shape="circle"
onClick={() => {
dispatch(setDark(true))
}}
></Button>
)}
+ {globalConfig.customColorPrimarys &&
+ globalConfig.customColorPrimarys.length > 0 && (
+ <Button
+ icon={<ThemeOutlined />}
+ shape="circle"
+ onClick={() => {
+ setShowThemeModal(true)
+ }}
+ ></Button>
+ )}
<Dropdown menu={{ items: menuItems }}>
...(略)
</Dropdown>
</div>
</div>
+ {showThemeModal && (
+ <ThemeModal
+ onClose={() => {
+ setShowThemeModal(false)
+ }}
+ />
+ )}
{contextHolder}
</Card>
)
}
export default Header
运行效果:
点击主题色对话框里的颜色,主题色切换就会立即生效了。
刷新页面或者重新打开网页也会保留上次的主题色。这是因为主题色在localStorage和Redux的store中都同步保存了。
8.10 安装Redux调试浏览器插件
本章节讲解的Redux使用,每次对store的操作变化跟踪如果用console.log()
显然很麻烦,也不及时。为了更方便地跟踪Redux状态,建议安装Chrome插件。这个插件可记录每次Redux的变化,非常便于跟踪调式。
先科学上网,在Chrome网上应用店里搜索“Redux DevTools”并安装。
在Redux DevTools插件中,可以清晰地看到Redux数据的状态变化,非常便于调试。
9 基于axios封装公用API库
为了方便API的维护,把各个API地址和相关方法集中管理是一个很不错的方案。
9.1 安装axios
axios是一款非常流行的API请求工具,先来安装一下。
执行:
npm install axios
9.2 封装公用API库
直接上代码。
新建src/api/index.jsx
:
import axios from 'axios'
import globalRouter from '@/router/globalRouter'
import { Modal } from 'antd'
import { globalConfig } from '@/globalConfig'
// 开发环境地址
let API_DOMAIN = '/api/'
if (import.meta.env.MODE === '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页面
globalRouter.navigate('/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()
})
}
代码比较多,必要的备注都写了,不再赘述。
这里主要实现了以下几方面:
- 通过apiReqs把项目所有API进行统一管理。
- 通过apiRequest方法,实现了统一的token验证、登录状态失效报错以及请求错误报错等业务逻辑。
- 通过globalRouter.navigate实现了在React组件外的路由跳转(
src/api/index.jsx
并不是React组件,只是一个简易的js变量和方法库而已)。
Q:为什么signIn和signOut方法没有像getUserList和modifyUser一样调用apiRequest呢?
A:因为signIn和signOut的逻辑比较特殊,signIn并没有读取localStorage,而signOut需要清除localStorage,这两个逻辑是与其他API不同的,所以单独实现了。
9.3 Mock.js安装与使用
在开发过程中,为了方便前端独自调试接口,经常使用Mock.js拦截Ajax请求,并返回预置好的数据。本小节介绍下如何在React项目中使用Mock.js。
执行安装:
npm install 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:
...(略)
// 引入Ant Design中文语言包
import zhCN from 'antd/locale/zh_CN'
// 全局样式
import '@/common/styles/frame.styl'
+ // mock.js模拟数据
+ import './mock'
...(略)
如此简单。这样,在项目中请求/api/login
的时候,就会被Mock.js拦截,并返回Mock.js中模拟好的数据。
※注:正式上线前,一定不要忘记关掉Mock.js!!!直接在
src/main.jsx
中注释掉import './mock'
这段代码即可。
9.4 发起API请求:实现登录功能
继续完善Login页面,实现一个API请求。
修改src/pages/login/index.jsx
:
import { useState } from 'react'
import { Button, Form, Input } from 'antd'
+ import { apiReqs } from '@/api'
+ import { useNavigate } from 'react-router-dom'
import { KeyOutlined, UserOutlined } from '@ant-design/icons'
import imgLogo from '@/common/images/logo.png'
import imgCover from './cover.svg'
import './login.styl'
function Login() {
+ // 创建路由hook
+ const navigate = useNavigate()
// 登录按钮loading状态
const [loading, setLoading] = useState(false)
// 提交登录
const loginSubmit = (values) => {
setLoading(true)
let data = {
account: values.account,
password: values.password,
}
- console.log('submitData', data)
+ apiReqs.signIn({
+ data,
+ success: (res) => {
+ console.log(res)
+ navigate('/home')
+ },
+ })
}
return ...(略)
}
export default Login
运行项目,打开登录界面:
http://localhost:3000/#/login
随便输入账号和密码,点击登录按钮后,登录成功,并且用户昵称也如期显示到了<Header>
组件的右上角。
通过调试工具查看localStorage,可以看到登录信息已正确保存。
10 其他优化
10.1 路由守卫
现在实现一个简单的路由守卫,通过Entry进行登录状态验证,未登录用户访问Home或者AUsers页面则强制跳转至Login页面。
修改src/pages/entry/index.jsx
:
import { Outlet } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { ConfigProvider, Layout, theme } from 'antd'
+ import { Navigate } from 'react-router-dom'
+ import { globalConfig } from '@/globalConfig'
import Header from '@/components/header'
import Sider from '@/components/sider'
const { darkAlgorithm, defaultAlgorithm } = theme
const { Content } = Layout
+ function PrivateRoute(props) {
+ // 判断localStorage是否有登录用户信息,如果没有则跳转登录页
+ return window.localStorage.getItem(globalConfig.SESSION_LOGIN_INFO) ? (
+ props.children
+ ) : (
+ <Navigate to="/login" />
+ )
+ }
function Entry() {
...(略)
return (
+ <PrivateRoute>
<ConfigProvider theme={antdTheme}>
...(略)
</ConfigProvider>
+ </PrivateRoute>
)
}
export default Entry
再次运行项目,这时,如果未经Login页面正常登录(即localStorage里没有登录信息),直接通过浏览器访问http://localhost:3000/#/home
或者http://localhost:3000/#/users
则会直接返回到Login页面。这是因为在Entry框架页面中引入了PrivateRoute
,先检查localStorage是否有登录用户信息,没有则强制跳转至Login页面。
当然,如果你想在路由守卫中实现更多的业务逻辑判断,请自行丰富PrivateRoute
逻辑。
10.2 设置开发环境的反向代理请求
基于Vite架构的工程,设置反向代理就很容易了,无需安装额外的依赖包。
修改vite.config.js
:
...(略)
// https://vitejs.dev/config/
export default defineConfig({
server: {
// 监听所有IP地址
host: '0.0.0.0',
// 指定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反向代理设置,请参阅官方说明:
一定记得要把mock.js注释掉,否则会先被mock.js拦截,到不了反向代理这一步。
10.3 使用ESLint9实时检查代码
如果想在保存代码的时候,实时检查代码语法格式,可以在vite.config.js
中配置ESlint9。
安装Vite官方推荐的插件vite-plugin-checker
,执行:
npm install -D vite-plugin-checker
修改vite.config.js
:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
+ import checker from 'vite-plugin-checker'
// https://vite.dev/config/
export default defineConfig({
server: {
...(略)
},
resolve: {
...(略)
},
- plugins: [react()],
+ plugins: [
+ react(),
+ checker({
+ eslint: {
+ // useFlatConfig: true 表示使用扁平模式配置(eslint.config.js)
+ // useFlatConfig: false 表示使用传统模式配置(如.eslintrc.json、.eslintrc.cjs)
+ useFlatConfig: true,
+ lintCommand: 'eslint "./src/**/*.{js,jsx,ts,tsx}"',
+ },
+ }),
+ ],
})
运行项目,页面效果如下:
可以看到,ESlint9直接将语法告警显式在页面中了,相比之前只是在终端窗口中告警,这种显示方式更加督促开发者尽快修改,特别适合代码格式强迫症。
当然,有些ESlint语法校验过于严格,可以对校验规则进行修改。
修改eslint.config.js
:
...(略)
rules: {
...js.configs.recommended.rules,
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
...reactHooks.configs.recommended.rules,
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
+ // 设置未使用变量的检查规则
+ 'no-unused-vars': [
+ 'warn',
+ { vars: 'all', args: 'none', ignoreRestSiblings: false },
+ ],
+ // 取消对react prop传参的检查
+ 'react/prop-types': 'off',
+ // 取消对自定义HTML属性的检查 react/no-unknown-property
+ 'react/no-unknown-property': 'off',
},
...(略)
修改eslint.config.js
配置,需要重启服务才能生效。经过以上规则调整,相应的警告已消除。
10.4 针对Windows系统的页面滚动条样式优化
基于Antd可以快速搭建美观的UI,macOS系统默认的页面滚动条已经很完美了,不需要修改。但在Windows系统中,系统默认的滚动条样式显得有点“出戏”。
可以通过自定义样式,并与Antd的亮暗模式配合优化。
首先,要先判断当前是否为Windows系统。
新建src/common/js/commonLib.jsx
:
/**
* 判断系统类型
*/
export function detectOS() {
let sUserAgent = navigator.userAgent
const platform = navigator?.userAgentData?.platform || navigator?.platform || 'unknown'
let isWin = platform === 'Win32' || platform === 'Windows'
if (isWin) return 'windows'
let isMac =
platform === 'macOS' ||
platform === 'Mac68K' ||
platform === 'MacPPC' ||
platform === 'Macintosh' ||
platform === 'MacIntel'
if (isMac) return 'mac'
let isUnix = platform === 'X11' && !isWin && !isMac
if (isUnix) return 'unix'
var isLinux = String(platform).indexOf('Linux') > -1
var bIsAndroid = sUserAgent.toLowerCase().match(/android/i) === 'android'
if (isLinux) {
if (bIsAndroid) return 'android'
else return 'linux'
}
}
修改src/main.jsx
:
...(略)
// mock.js模拟数据
import './mock'
+ import { detectOS } from '@/common/js/commonLib'
+ // 判断操作系统
+ document.body.setAttribute('os', detectOS())
ReactDOM.createRoot(document.getElementById('root')).render(
...(略)
)
以上js代码,会在页面HTML的body标签上加上os属性。
在第8.6章节,还通过判断当前Antd的亮暗模式,在body上添加theme-mode属性。
因此最终结果为如下:
例如:在Windows系统中,亮色模式的结果为:
补充滚动条样式,修改src/common/styles/globals.styl
:
html, body, #root
height: 100%
.G-main
padding: 20px
.G-fullpage
display: flex
height: 100%
.G-layout-main
flex: 1
overflow: auto
+ :root
+ // 滚动条样式
+ --theme-scrollbar-color-track: rgb(241,241,241)
+ --theme-scrollbar-color-track-border: rgb(230,230,230)
+ --theme-scrollbar-color-thumb: rgb(193,193,193)
+ --theme-scrollbar-color-thumb-hover: rgb(168,168,168)
+ [theme-mode=dark]
+ --theme-scrollbar-color-track: rgb(44,44,44)
+ --theme-scrollbar-color-track-border: rgb(56,56,56)
+ --theme-scrollbar-color-thumb: rgb(107,107,107)
+ --theme-scrollbar-color-thumb-hover: rgb(115,115,115)
+
+ /* 全局滚动条样式 */
+ // 滚动条厚度
+ body[os=windows] *::-webkit-scrollbar
+ width:14px
+ height:14px
+ // 滚动条(水平+垂直)样式
+ body[os=windows] *::-webkit-scrollbar-track
+ background: var(--theme-scrollbar-color-track)
+ // 滚动条(水平+垂直)hover样式
+ body[os=windows] *::-webkit-scrollbar-track:hover
+ background: var(--theme-scrollbar-color-track)
+ // 滚动条(水平)样式
+ body[os="windows"] *::-webkit-scrollbar-track:horizontal
+ border-top: solid 1px var(--theme-scrollbar-color-track-border)
+ // 滚动条(垂直)样式
+ body[os="windows"] *::-webkit-scrollbar-track:vertical
+ border-left: solid 1px var(--theme-scrollbar-color-track-border)
+ // 滚动滑块(水平+垂直)样式
+ body[os=windows] *::-webkit-scrollbar-thumb
+ background: var(--theme-scrollbar-color-thumb)
+ border-radius: 14px
+ border: 3px solid transparent
+ // 解决滑块边框遮挡滚动条背景边框
+ background-clip: padding-box
+ body[os=windows] *::-webkit-scrollbar-thumb:hover
+ background: var(--theme-scrollbar-color-thumb-hover)
+ // 解决滑块边框遮挡滚动条背景边框
+ background-clip: padding-box
+ // 滚动滑块(水平)样式
+ body[os=windows] *::-webkit-scrollbar-thumb:horizontal
+ // 解决滑块与背景上下边距不统一的问题
+ border-top-width: 4px
+ // 滚动滑块(垂直)样式
+ body[os=windows] *::-webkit-scrollbar-thumb:vertical
+ // 解决滑块与背景左右边距不统一的问题
+ border-left-width: 4px
+ // 滚动条首尾的按钮样式
+ body[os=windows] *::-webkit-scrollbar-button
+ display:none
+ // 水平和垂直滚动条在页面右下角交叉处产生的空白区域样式
+ body[os=windows] *::-webkit-scrollbar-corner
+ background: var(--theme-scrollbar-color-track)
最终呈现的滚动条视觉效果如图所示,基本与macOS的原生滚动条一致。
11 build项目
在build前还可以做一些配置,以下简述几个常用的配置。
11.1 设置静态资源引用路径
默认情况下,build出来的项目,静态资源引用的一级路径都是"/",建议修改成相对路径"./",这样在部署上线的时候不需要太关注访问目录的问题。
修改vite.config.js
:
...(略)
// https://vitejs.dev/config/
export default defineConfig({
+ // 静态资源引用路径,默认为"/"
+ base: './',
...(略)
})
11.2 设置build目录名称及静态资源存放目录(选读)
默认情况下,build出来的项目,将静态文件(js、图片等)都存放在assets目录下。也可以通过配置,改为其他名称(例如:static)。
同样,Vite默认build生成的项目目录名为dist,也可以改为其他名称(例如:build)。
修改vite.config.js:
...(略)
// https://vitejs.dev/config/
export default defineConfig({
+ build: {
+ // build目录名称,默认为"dist"
+ outDir: 'build',
+ // 静态资源存放目录名称,默认为"assets"
+ assetsDir: 'static',
+ },
...(略)
})
11.3 执行build项目
执行以下命令即可build项目:
npm run build
build生成的项目文件位于项目根目录的dist目录中。
12 项目Git源码
本项目已上传至Gitee和GitHub,方便各位下载。
Gitee: gitee.com/betaq/vite-…
GitHub:
结束语
以上就是本次基于Vite的React全家桶教程的全部内容。篇幅较长,确实是花了很长时间精心整理、反复验证、句句斟酌的完整教程,希望能够帮助到你。更多精彩详实的开发教程,欢迎阅读我的微信公众号卧梅又闻花
。
原文:《2025新春版:轻松搞定Vite6+React19全家桶》
更多精品阅读
《2024金秋版:Django5开发与部署保姆级零基础教程(上篇)》
《2024金秋版:Django5开发与部署保姆级零基础教程(下篇)》
《2023金秋版:基于electron-vite构建Vue桌面客户端》
《2023金秋版:基于electron-vite构建React桌面客户端》