2025新春版:轻松搞定Vite6+React19全家桶

3,623 阅读44分钟

封面_掘金.png

原创声明-掘金.png

最近真是赶上了大版本升级高峰期,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.x4.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.3Header组件中实现主题色切换
• 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/

1.3_安装并运行项目.png

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官方说明:

cn.vitejs.dev/guide/asset…

运行项目,执行:

npm run dev

页面效果如下:

1.4_精简项目.png

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官方说明:

cn.vitejs.dev/guide/featu…

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组件正常显示出来了。

4.1_安装Ant Design.png

※注: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 兼容

ant.design/docs/react/…

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,涉及以下知识点:

  1. 页面路由跳转
  2. 二级路由
  3. 登录状态验证(路由守卫)
  4. 主题色换肤
  5. 亮色/暗色模式

特例是:登录页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'

运行效果如下:

5.1_构建Login页面-1.png

输入账号和密码,然后点击登录,可以看到登录按钮变成了loading状态。在调试工具中可以看到提交的账号和密码数据。

5.1_构建Login页面-2.png

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的&lt;Card
                        /&gt;组件进行作为最外层,以便适用主题换肤。
                    </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.3_构建Home页面.png

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'

运行效果如下:

5.4_构建Users页面.png

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页面效果:

6.3_引入路由-1-home.png

嵌入Entry的Users页面效果:

6.3_引入路由-2-users.png

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

6.4_路由跳转:构建左侧导航Sider组件-1-home.png

Users页面:http://localhost:3000/#/users

6.4_路由跳转:构建左侧导航Sider组件-2-users.png

左侧导航收起状态:

6.4_路由跳转:构建左侧导航Sider组件-3-收起状态.png

现在看上去还挺丑的,别着急,后续章节会讲解Antd主题色及暗色模式,会再次修改Sider组件样式的。

7 React Developer Tools浏览器插件

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

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

7_React Developer Tools浏览器插件-1.png

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

7_React Developer Tools浏览器插件-2.png

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点:

  1. 使用useDispatch来派发对store的修改,使用方式就是dispatch(setDark(false))setDark是在theme的reducer(src/store/slices/theme.jsx)中定义好的方法。
  2. 使用useModal来调用Antd的Modal组件,contextHolder随意放置在组件任何位置即可(建议放置组件内部的最下面)。
  3. 使用useNavigate来实现路由跳转。
  4. 使用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 }}>
                    ...(略)

运行效果如下:

8.7_store的变更-1.png

点击“月亮”小图标按钮后,即可切换到暗色模式:

8.7_store的变更-2.png

细心的你应该会发现,左侧导航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主题编辑器

ant-design.antgroup.com/theme-edito…

再来看看Sider的效果,舒服多了:

8.8_自建组件使用Antd的主题色.png

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

8.9.2_创建自定义SVG图标Icon组件-1.png

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

8.9.2_创建自定义SVG图标Icon组件-2.png

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

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

  1. 检查svg代码中是否有class以及与颜色相关的fill、stroke等属性,如有,必须连带属性一起删除。像p-id这种自定义的属性也可以删除。

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

  3. 确保<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

运行效果:

8.9.3_在Header组件中实现主题色切换-1.png

点击主题色对话框里的颜色,主题色切换就会立即生效了。

8.9.3_在Header组件中实现主题色切换-2.png

刷新页面或者重新打开网页也会保留上次的主题色。这是因为主题色在localStorage和Redux的store中都同步保存了。

8.10 安装Redux调试浏览器插件

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

8.10_安装Redux调试浏览器插件-1.png

在Redux DevTools插件中,可以清晰地看到Redux数据的状态变化,非常便于调试。

8.10_安装Redux调试浏览器插件-2.png

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()
        })
}

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

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

  1. 通过apiReqs把项目所有API进行统一管理。
  2. 通过apiRequest方法,实现了统一的token验证、登录状态失效报错以及请求错误报错等业务逻辑。
  3. 通过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>组件的右上角。

9.4_发起API请求:实现登录功能-1.png

通过调试工具查看localStorage,可以看到登录信息已正确保存。

9.4_发起API请求:实现登录功能-2.png

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反向代理设置,请参阅官方说明:

cn.vite.dev/config/serv…

一定记得要把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}"',
+               },
+           }),
+       ],
    })

运行项目,页面效果如下:

10.3_使用ESLint 9实时检查代码.png

可以看到,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系统中,系统默认的滚动条样式显得有点“出戏”。

10.4_针对Windows系统的页面滚动条样式优化-1.png

可以通过自定义样式,并与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系统中,亮色模式的结果为:

10.4_针对Windows系统的页面滚动条样式优化-2.png

补充滚动条样式,修改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的原生滚动条一致。

10.4_针对Windows系统的页面滚动条样式优化-3.png

10.4_针对Windows系统的页面滚动条样式优化-4.png

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:

github.com/Yuezi32/vit…

结束语

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

原文:《2025新春版:轻松搞定Vite6+React19全家桶》

更多精品阅读

《2024金秋版:Django5开发与部署保姆级零基础教程(上篇)》

《2024金秋版:Django5开发与部署保姆级零基础教程(下篇)》

《2024新春版:零基础Docker全栈开发部署速通攻略》

《2023金秋版:基于electron-vite构建Vue桌面客户端》

《2023金秋版:基于electron-vite构建React桌面客户端》

《2023金秋版:基于Vite4+Vue3的Chrome插件开发教程》

《2023金秋版:基于Vite4+React的Chrome插件开发教程》