React安装使用教程

React+Ant Design+router+axios安装完整教程

官网:React Native 中文网 · 使用React来编写原生应用的框架

一,安装

  1. npx create-react-app my-app

  2. npm start

  3. npm eject 暴露项目优先提交代码

    git add .

    git commit -m "搭建项目"

    4.yarn add node-sass --dev 和 yarn add less less-loader --dev

    5.修改配置config/webpack 打包文件 在75行左右

​ 添加代码

复制代码
const lessRegex = /\.less$/;
const lessModuleRegex = /\.module\.less$/;

  //配置 less
{
  test: lessRegex,
  exclude: lessModuleRegex,
  use: getStyleLoaders(
      {
        importLoaders: 3,
        sourceMap: isEnvProduction
            ? shouldUseSourceMap
            : isEnvDevelopment,
        modules: {
          mode: 'icss',
        },
      },
      'less-loader'
  ),
  sideEffects: true,
},
{
  test: lessModuleRegex,
  use: getStyleLoaders(
      {
        importLoaders: 3,
        sourceMap: isEnvProduction
            ? shouldUseSourceMap
            : isEnvDevelopment,
        modules: {
          mode: 'local',
          getLocalIdent: getCSSModuleLocalIdent,
        },
      },
      'less-loader'
  ),
},

其实就把上面sass配置代码复制一遍,改成less。按照以上操作后,项目已支持Less。

6.接下来安装Stylus

yarn add stylus stylus-loader --dev

复制代码
//stylus
const stylusRegex = /\.styl$/;
const stylusModuleRegex = /\.module\.styl$/;

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

复制代码
//配置 stylus
            {
              test: stylusRegex,
              exclude: stylusModuleRegex,
              use: getStyleLoaders(
                  {
                    importLoaders: 3,
                    sourceMap: isEnvProduction
                        ? shouldUseSourceMap
                        : isEnvDevelopment,
                    modules: {
                      mode: 'icss',
                    },
                  },
                  'stylus-loader'
              ),
              sideEffects: true,
            },
            {
              test:stylusModuleRegex,
              use: getStyleLoaders(
                  {
                    importLoaders: 3,
                    sourceMap: isEnvProduction
                        ? shouldUseSourceMap
                        : isEnvDevelopment,
                    modules: {
                      mode: 'local',
                      getLocalIdent: getCSSModuleLocalIdent,
                    },
                  },
                  'stylus-loader'
              ),
            },

6.设置路径别名(避免使用相对路径的麻烦)

检索:alias

//config/webpack.config.js

复制代码
//设置绝对路径
'@': path.join(__dirname, '..', 'src')

若使用绝对路径,在pakage.json配置

复制代码
"homepage": "./",

"name": "react-demo",
"version": "0.1.0",
"private": true,
"homepage": "./",

二,项目模块构建

复制代码
├─ /config               <-- webpack配置目录

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

1.设置styles样式(我使用的是webstrom 记得安装插件stylus)

​ global样式

复制代码
html, body, #root
  height: 100%
/*清浮动*/
.clearfix:after
  content: "."
  display: block
  height: 0
  clear: both
  visibility: hidden
.clearfix
  display:block

frame导入样式

复制代码
@import "./global.styl"
@import "./reset.styl"

在index.js 入口文件中导入文件预处理样式

复制代码
import React from 'react';
import ReactDOM from 'react-dom/client';
import '@/common/styles/frame.styl'
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <App />
);

三,安装Ant Design

官网:Ant Design - 一套企业级 UI 设计语言和 React 组件库

1.安装

yarn add antd

2.设置Antd为中文语言

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

四,安装路由

官网:React Router 主页 | React Router7 中文文档

1.安装(备注:安装时需要根据自己的node版本选择版本,默认是安装最新的,最新的需要node环境是20的)

备注:yarn add react-router-dom 只是针对项目内注册是组件跳转使用

yarn add react-router-dom

Hooks API

  1. useNavigate()

    作用:编程式导航,返回

    复制代码
    const navigate = useNavigate();
    navigate("/path", { state: { data } });  // 支持相对路径和状态传递
  2. useParams()

    作用:获取动态路由参数(如)

    复制代码
    /user/:id
  3. useSearchParams()

    作用:获取和操作 URL 查询参数

    复制代码
    Jsconst [searchParams, setSearchParams] = useSearchParams();
    const id = searchParams.get("id");
  4. useLocation()

    作用:获取当前路由的location

    对象(包含pathname、search、state)

2.在router/index.js 文件下添加路由信息

复制代码
import { createHashRouter, Navigate } from 'react-router-dom'
import Home from "../pages/home";
import Login from "../pages/login";
export const routes = createHashRouter([
    {
        path: '/',
        element:<Navigate to="/login" />,
        children: []
    },
    {
        path: '/login',
        element: <Login />,
        children: []
    },
    {
        path: '/home',
        element: <Home />,
    },
    {
        path: '*',
        element: <Navigate to="/login" />
    }
]);

3.在src/index.js 下引入路由,并删除App.js文件

复制代码
import React from 'react';
import ReactDOM from 'react-dom/client';
// 全局样式
import '@/common/styles/frame.styl'
// 引入Ant Design中文语言包
import zhCN from 'antd/locale/zh_CN'
// 引入路由配置
import {ConfigProvider} from "antd";
import {RouterProvider} from "react-router-dom";
import {routes} from "../src/router/index";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
   <ConfigProvider locale={zhCN}>
       <RouterProvider router={routes}></RouterProvider>
   </ConfigProvider>
);

4.实现跳转

在login界面进行引入路由

复制代码
import {useNavigate} from "react-router-dom";

使用完整示例

复制代码
import { Button, Input } from 'antd'
import imgLogo from './logo.png'
import './login.styl'
import {useNavigate} from "react-router-dom";
function Login() {
    const navigate = useNavigate()
    return (
        <div className="content-body">
            <img src={imgLogo} alt="" className="logo" />
            <div className="ipt-con">
                <Input placeholder="账号" />
            </div>
            <div className="ipt-con">
                <Input.Password placeholder="密码" />
            </div>
            <div className="ipt-con">
                <Button type="primary" block={true} onClick={()=>{navigate("/home")}}>
                    登录
                </Button>
            </div>
        </div>
    )
}
export default Login

5.安装非组件内跳转路由,以上跳转,只是针对在组件内进行跳转,一下安装是非React组件内跳转

yarn add history@4.10.1

6.安装完成后在src/router/hisRouter.js 写一个goto方法

复制代码
import { createHashHistory } from 'history'

let history = createHashHistory()

export const goto = (path) => {
    history.push(path)
}

7.在src/pages/home/index.js里调用goto方法

复制代码
import {Button, theme} from "antd";
import {goto} from "../../router/hisRouter";
import {Content} from "antd/es/layout/layout";
function admin(){
    // 获取Design Token
    const token = theme.useToken().token || {};
    const contentStyle = {
        textAlign: 'center',
        minHeight: '100%',
        lineHeight: '120px',
        color: '#fff',
        backgroundColor: token.colorBgContainer,
    };

    return (
        <Content style={contentStyle}>
            <div>
                <span style={{color:token.colorText}}>admin</span>
                <Button onClick={()=>{goto('/entry/home')}}>返回</Button>
            </div>
        </Content>
    )
}

export default admin

五,创建自定义SVG图标Icon组件(无需求跳过)

1.安装图标

yarn add @ant-design/icons

2.在src/components/extraIcons/index.js文件添加代码

复制代码
import Icon from '@ant-design/icons'
 //特别注意
//https://www.iconfont.cn/
//检查svg代码中是否有class以及与颜色相关的fill、stroke等属性,如有,必须连带属性一起删除。
//确保标签中有fill="currentColor",否则图标的颜色将不能改变。
//确保标签中width和height属性的值为1em,否则图标的大小将不能改变。
const SunSvg = () => (
    <svg t="1743404892026"
         className="icon"
         viewBox="0 0 1024 1024"
         version="1.1"
         xmlns="http://www.w3.org/2000/svg"
         p-id="3408"
         width="1em"
         height="1em"
         fill="currentColor"
    >
        <path
            d="M344.189719 297.542353l-57.889397-57.889397-48.231443 48.232466 57.889397 57.889397L344.189719 297.542353zM254.129654 480.812217l-96.462886 0L157.666768 545.103411l96.462886 0L254.129654 480.812217zM543.518311 162.503932l-64.291194 0 0 93.214915 64.291194 0L543.518311 162.503932zM784.677572 287.885422l-48.231443-48.232466-57.89042 57.889397 45.031568 45.027474L784.677572 287.885422zM678.555709 728.42137l57.89042 57.841302 45.07557-44.982449-57.934423-57.885304L678.555709 728.42137zM768.614751 545.103411l96.464932 0 0-64.291194-96.464932 0L768.614751 545.103411zM511.397785 320.009018c-106.116747 0-192.926795 86.855073-192.926795 192.927818 0 106.113677 86.810048 192.923725 192.926795 192.923725 106.11777 0 192.923725-86.810048 192.923725-192.923725C704.32151 406.864091 617.515555 320.009018 511.397785 320.009018M479.227117 863.459791l64.291194 0 0-93.259941-64.291194 0L479.227117 863.459791zM238.068879 738.030205l48.231443 48.231443 57.889397-57.841302-44.982449-45.027474L238.068879 738.030205z"
             p-id="3409"></path>
    </svg>
)
 
const MoonSvg = () => (
    // 这里粘贴"月亮"图标的SVG代码
)
 
const ThemeSvg = () => (
    // 这里粘贴"主题色"图标的SVG代码
)
 
export const SunOutlined = (props) => <Icon component={SunSvg} {...props} />
export const MoonOutlined = (props) => <Icon component={MoonSvg} {...props} />
export const ThemeOutlined = (props) => <Icon component={ThemeSvg} {...props} />

3.在Header组件下编写代码index.js 和 header.stly

复制代码
import { Button, Card } from 'antd'
import { MoonOutlined, ThemeOutlined } from '@/components/extraIcons'
import './header.styl'

function Header() {
    return (
        <Card className="M-header">
            <div className="header-wrapper">
                <div className="logo-con">Header</div>
                <div className="opt-con">
                    <Button icon={<MoonOutlined />} shape="circle"></Button>
                    <Button icon={<ThemeOutlined />} shape="circle"></Button>
                </div>
            </div>
        </Card>
    )
}

export default Header

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

4.代码测试,在home 界面里面引入使用

复制代码
import Header from "../../components/header";

import { useNavigate } from 'react-router-dom'
import { Button } from 'antd'
import { goto } from '../../api/index'
import './home.styl'
import Header from "../../components/header";
function Home() {
    // 创建路由钩子
    const navigate = useNavigate()
    return (
        <div className="P-home">
            <Header />
            <h1>Home Page</h1>
            <div className="ipt-con">
                <Button onClick={()=>{goto('/login')}}>组件外跳转</Button>
            </div>
            <div className="ipt-con">
                <Button type="primary" onClick={()=>{navigate('/login')}}>返回登录</Button>
            </div>
        </div>
    )
}

export default Home

六,父子传值

1.在header/index.js 下添加代码

复制代码
import { Button, Card } from 'antd'
import { MoonOutlined, ThemeOutlined } from '@/components/extraIcons'
import './header.styl'

function Header(props) {

    //接收父组件传的值
    const {title,info} =props
   if (info){
       info()
   }
   
    return (
        <Card className="M-header">
            <div className="header-wrapper">
                <div className="logo-con">Header{title }</div>
                <div className="opt-con">
                    <Button icon={<MoonOutlined />} shape="circle"></Button>
                    <Button icon={<ThemeOutlined />} shape="circle"></Button>
                </div>
            </div>
        </Card>
    )
}

export default Header

2.在home/index.js 里面添加代码,并测试运行代码

复制代码
<Header title='测试' info={()=>{console.log("接受了数据")}} />

七,二级动态路由的配置

1.创建二级路由的框架页面

src/pages/entry/index.js 和entry.styl

复制代码
import { Outlet } from 'react-router-dom'
import Header from '../../components/header'
import './entry.styl'
function Entry() {
    return (
        <div className="M-entry">
            <Header />
            <div className="main-container">
                <Outlet />
            </div>
        </div>
    )
}
export default Entry

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

2.在src/pages下添加一个admin 参考对比界面

3.配置路由页面完整测试代码

复制代码
import { createHashRouter, Navigate } from 'react-router-dom'
import Home from "../pages/home";
import Login from "../pages/login";
import Admin from "../pages/admin";
import Entry from "../pages/entry";
export const routes = createHashRouter([
    {
        path: '/login',
        element: <Login />,
    },
    {
        index: true,
        element: <Navigate to="/login" />,
    },
    {
        path: '/entry/*',
        element: <Entry />,
        children: [
            { path: 'home', element: <Home /> },
            { path: 'admin', element: <Admin /> },
            { index: true, element: <Navigate to="home" /> },
            { path: '*', element: <Navigate to="/404" /> }
        ]
    },
    {
        path: '*',
        element: <Navigate to="/404" />
    }
]);

八,安装Redux及Redux Toolkit

Redux 中文文档

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。

Redux Toolkit (也称为 "RTK" ) 是我们官方推荐的编写 Redux 逻辑的方法。@reduxjs/toolkit 包封装了核心的 redux 包,包含我们认为构建 Redux 应用所必须的 API 方法和常用依赖。 Redux Toolkit 集成了我们建议的最佳实践,简化了大部分 Redux 任务,阻止了常见错误,并让编写 Redux 应用程序变得更容易。

1.安装

yarn add @reduxjs/toolkit react-redux

2.在全局配置文件src/globalConfig.js里面配置信息(用来配置主题)

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

3.创建用于主题换肤的store分库,src/store/slices/theme.js

复制代码
import { createSlice } from '@reduxjs/toolkit'
import { globalConfig } from '../../globalConfig'

// 先从localStorage里获取主题配置
const sessionTheme = JSON.parse(window.localStorage.getItem(globalConfig.SESSION_LOGIN_THEME))

// 如果localStorage里没有主题配置,则使用globalConfig里的初始化配置
const initTheme =  sessionTheme?sessionTheme: globalConfig.initTheme


export const themeSlice = createSlice({
    // store分库名称
    name: 'theme',
    // store分库初始值
    initialState:{
        dark: initTheme.dark,
        colorPrimary: initTheme.colorPrimary
    },
    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

4.创建store总库src/index.js

复制代码
import {configureStore} from "@reduxjs/toolkit";
import {themeSlice} from "./slices/theme";


export const store = configureStore({
    reducer: {
        //主题换肤分库
        theme: themeSlice.reducer,
    }
})

5.引入store库,src/index.js

复制代码
//引入store
import {store} from "../src/store/index";
import {Provider} from "react-redux";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <Provider store={store}>
        <ConfigProvider locale={zhCN}>
            <RouterProvider router={routes}></RouterProvider>
        </ConfigProvider>
    </Provider>
);

6.在header头里面配置主题信息和调用store方法,修改后的header/index.js

复制代码
import { Button, Card } from 'antd'
//导入图标
import { MoonOutlined, ThemeOutlined,SunOutlined } from '../extraIcons/index'
import './header.styl'
import { useSelector, useDispatch } from 'react-redux'
import { setDark } from '../../store/slices/theme'
//导入store主题抛出的方法
function Header({title,info}) {
    //如果传info方法就执行调用,这里是模拟演示,实际使用时,info方法是从父组件传入的
    //父组件传入的info方法是在父组件中定义的,父组件中定义的info方法是在父组件中定义的,父组件中定义的info方法是在父组件中定义的
    if (info){info()}
    //获取store中theme的dispatch方法
    const dispatch = useDispatch()
    //获取store中theme的状态
    const {dark} = useSelector((state)=>state.theme)
    //图标主题切换
    const darkChange=()=>{
        return dark ?
            <Button icon={<SunOutlined />}
                    shape="circle"
                    onClick={()=>{dispatch(setDark(false))}}>
            </Button> :
            <Button icon={<MoonOutlined />}
                    shape="circle"
                    onClick={()=>{dispatch(setDark(true))}}>
            </Button>
    }
    //返回头部
    return (
        <Card className="M-header">
            <div className="header-wrapper">
                <div className="logo-con">Header{title }</div>
                <div className="opt-con">
                    {darkChange()}
                    <Button icon={<ThemeOutlined />} shape="circle"></Button>
                </div>
            </div>
        </Card>
    )
}

export default Header

7.修改src/entry/index.js页面设置主题的切换完整示例

复制代码
import {Outlet, useLocation} from 'react-router-dom'
import Header from '../../components/header'
import './entry.styl'
import { useSelector, useDispatch } from 'react-redux'
//获取Ant Design的主题、
import { ConfigProvider, theme } from 'antd'
//darkAlgorithm为暗色主题,defaultAlgorithm为亮色(默认)主题
const {darkAlgorithm, defaultAlgorithm} = theme;

function Entry() {
    const mate = useLocation();
    //获取store中theme的状态
    const globalTheme = useSelector((state) => state.theme);
    const  antdTheme = {
        algorithm: globalTheme.dark ? darkAlgorithm : defaultAlgorithm,
    }
    return (
        <ConfigProvider theme={antdTheme}>
        <div className="M-entry">
            <Header title={mate.pathname} />
            <div className="main-container">
                <Outlet />
            </div>
        </div>
        </ConfigProvider>
    )
}
export default Entry

备注:在以上的主题切换中,页面中的"admin Page"始终是白色,并没有跟随换肤。这是因为它并没有包裹在Antd的组件中。而Header组件能够换肤是因为其外层用了Antd的<Card>组件。所以在开发过程中,建议尽量使用Antd组件。

8.以上的换肤是针对使用Antd组件的换肤,可能也会遇到自行开发的组件也要换肤,非Ant Design组件的主题换肤。src/pages/admin/index.js

复制代码
import {Button, theme} from "antd";
import {goto} from "../../api";
import {Content} from "antd/es/layout/layout";
function admin(){
    // 获取Design Token
    const token = theme.useToken().token || {};
    const contentStyle = {
        textAlign: 'center',
        minHeight: '100%',
        lineHeight: '120px',
        color: '#fff',
        backgroundColor: token.colorBgContainer,
    };

    return (
        <Content style={contentStyle}>
            <div>
                <span style={{color:token.colorText}}>admin</span>
                <Button onClick={()=>{goto('/entry/home')}}>返回</Button>
            </div>
        </Content>
    )
}

export default admin

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

定制主题 - Ant Design

9.创建主题色选择对话框组件,新建src/components/themeModal/index.js

复制代码
import { Modal } from 'antd'
import { useSelector, useDispatch } from 'react-redux'
import { CheckCircleFilled } from '@ant-design/icons'
import { setColorPrimary } from '@/store/slices/theme'
import { globalConfig } from '@/globalConfig'
import './themeModal.styl'
function ThemeModal({ open = false, onClose = (val) => {} }) { // 为 open 和 onClose 添加默认值
    const dispatch = useDispatch()
    // 从 store 中获取 theme
    const theme = useSelector(state => state.theme)
    //获取主题色列表
    const themeColorList = globalConfig.customColorPrimarys || [];
    // 渲染主题色列表
    const renderThemeColorList = () => {
      return  themeColorList.map((item, index) => {
            return (
                <div className="theme-color"
                     style={{ backgroundColor: item.toString() }}
                     key={index}
                     onClick={() =>{dispatch(setColorPrimary(item));onClose(false)}}>
                    {
                        theme.colorPrimary === item && (
                            <CheckCircleFilled
                                style={{
                                    fontSize: 28,
                                    color: '#fff',
                                }}/>
                        )
                    }
                </div>
            )
        })
    }
    return (
        <Modal
            className="M-themeModal"
            open={open}
            title="主题色"
            onCancel={() => onClose(false)} // 确保 onClose 存在时调用
            maskClosable={false}
            footer={null} // 添加 footer 为 null,避免默认按钮干扰
        >
            <div className="colors-con">
                {renderThemeColorList()}
            </div>
        </Modal>
    )
}

export default ThemeModal

10.修改header/index.js 代码

复制代码
const [isModalOpen, setIsModalOpen] = useState(false) // 添加状态管理

<Card className="M-header">
    <div className="header-wrapper">
        <div className="logo-con">Header{title}</div>
        <div className="opt-con">
            {darkChange()}
            <Button
                icon={<ThemeOutlined />}
                shape="circle"
                onClick={() => setIsModalOpen(true)} // 绑定点击事件
            />
        </div>
    </div>
    <ThemeModal
        open={isModalOpen}
        onClose={() => setIsModalOpen(false)} // 关闭弹窗
    />
</Card>

11.设置修改后的主题颜色,修改src/pages/entry/index.js

复制代码
//获取自定义的颜色
const customColor = globalTheme.colorPrimary || "";
//如果自定义的颜色存在,就将其设置为主题色示例
if (customColor){
    antdTheme.token = {
        colorPrimary: customColor,
    }
}

九,生产/开发环境变量配置

1.安装dotenv-cli插件

yarn add dotenv-cli -g

2.在根目录下新建.env.dev

复制代码
# 开发环境

#代理前缀
REACT_APP_BASE = '/api'
#接口前缀
REACT_APP_BASE_URL = 'http://localhost:8089'
#websocket前缀 
REACT_APP_BASE_WSS = 'wss://localhost:8089'

同样在根目录下新建.env.pro

复制代码
# 生产环境

#代理前缀
REACT_APP_BASE = '/api'
#接口前缀
REACT_APP_BASE_URL = 'http://localhost:8089'
#websocket前缀 
REACT_APP_BASE_WSS = 'wss://localhost:8089'

3.配置package.json文件

复制代码
  "scripts": {
    "dev": "dotenv -e .local.single.env -e .env.dev node scripts/start.js ",
    "build": "dotenv -e .local.single.env -e .env.pro node scripts/build.js ",
    "test": "node scripts/test.js",
    "preview": "dotenv -e .local.single.env -e .env.pro node scripts/start.js "
  },

十,安装axios和请求封装

1.安装axios

yarn add axios

2.构建请求封装src/api/Api.js 构建请求体封装

复制代码
import axios from 'axios';
// 从环境变量中获取baseUrl和base
const {REACT_APP_BASE_URL,REACT_APP_BASE} = process.env;



// 创建axios实例
const instance = axios.create({
    baseURL: REACT_APP_BASE_URL + REACT_APP_BASE,
    timeout: 10000,
    headers: {
        'Content-Type': 'application/json'
    }
});

// 请求拦截器
instance.interceptors.request.use(config => {
    const token = localStorage.getItem('authToken');
    if (token) {
        config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
}, error => {
    return Promise.reject(error);
});

// 响应拦截器
instance.interceptors.response.use(response => {
    if (response.status === 200) {
        return response.data;
    }
    return Promise.reject(response);
}, error => {
    return Promise.reject(error);
});

/**
 * get参数转换
 */
const queryChangeFun = result => {
    let queryString = Object.keys(result)
        .map(key => `${key}=${result[key]}`)
        .join('&');
    return queryString.length >= 2 ? '?' + queryString : '';
};

// 封装通用请求方法
const askApi = (method = 'get', url = "", params = {}, query = {}, headers = {}) => {
    const config = {
        method: method.toLowerCase(), // 统一转换为小写
        url,
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            ...headers // 合并自定义headers
        }
    };

    // 根据请求方法处理参数
    if (method.toLowerCase() === 'get') {
        // config.params = query; // get请求使用params
        config.url = url + queryChangeFun(query);
    } else {
        config.data = params; // 其他请求使用data
    }
    return instance(config)
};

export default askApi;

2.在src/interface/userApi.js创建一个模拟用户请求接口的封装

复制代码
/**
 * 用户相关接口请求
 */

import askApi from "../api/Api";

/**
 * 登录接口
 */
export const loginApi = (param) => askApi('post', '/login',param,{},{});

3.在登录界面掉用接口,示例

复制代码
function Login() {
    const navigate = useNavigate();
    const loginFun = () => {
        loginApi({
            username: 'admin',
            password: 'admin'
        }).then(res => {
            console.log(res)
            navigate("/entry")
        }).catch(err => {
            navigate("/login")
        })

    }
    return (
        <div className="content-body">
            <img src={imgLogo} alt="" className="logo" />
            <div className="ipt-con">
                <Input placeholder="账号" />
            </div>
            <div className="ipt-con">
                <Input.Password placeholder="密码" />
            </div>
            <div className="ipt-con">
                <Button type="primary" block={true} onClick={()=>{loginFun()}}>
                    登录
                </Button>
            </div>
        </div>
    )
}

十一,安装Mock.js

​ 作用:生成随机数据,拦截 Ajax 请求

1.安装:

yarn add mockjs

2.新建文件src/mock/index.js

复制代码
import Mock from 'mockjs'

const {REACT_APP_BASE_URL,REACT_APP_BASE} =process.env;
const url = REACT_APP_BASE_URL+REACT_APP_BASE
// 设置延迟时间
Mock.setup({
    timeout: '200-400'
});
if (process.env.NODE_ENV === 'development') {
    Mock.mock(url+'/login','post', function (val) {
        console.log(val)
        return {
            code: 200,
            msg: '登录成功',
            data: {
                loginUid: 1000,
                username: 'admin',
                password: 'admin@123456',
                token: "dnwihweh0w0183971030183971030",
            },
        }
    })
}

3.在src/index.js 下引入方法,正式发布需要移除

复制代码
//引入mock数据
import './mock/index'

4.优化登录界面和调用接口

安装第三方背景插件,可跳过: yarn add @splinetool/react-spline @splinetool/runtime

复制代码
import {Button, Checkbox, Form, Input, message} from 'antd'
import './login.styl'
import {useNavigate} from "react-router-dom";
import {loginApi} from "../../interface/userApi";
import {useMsg} from "../../common/utils/toolUtil";
import Spline from "@splinetool/react-spline";
function Login() {
    const {isMsg,msgTitle} = useMsg();
    const navigate = useNavigate();
    const onFinish = values => {
        loginApi(values).then(res => {
            isMsg(res)
            if (res.code === 200) {
                localStorage.setItem('token', res.data.token);
                localStorage.setItem('userName', res.data.userName);
                localStorage.setItem('userId', res.data.userId);
                navigate("/entry")
            }
        }).catch(err => {
            localStorage.removeItem('token');
        })
    };
    return (
        <main>
            <div className="spline">
                <Spline scene="https://prod.spline.design/zaHcDRWYBdPkoutI/scene.splinecode"/>
            </div>
            <div className="content-body"  >
                {msgTitle}
                <div className='card-body'>
                   <div className='title'>
                       <span className='svg'>
                           <svg t="1743563430303" className="icon" viewBox="0 0 1024 1024" version="1.1"
                                xmlns="http://www.w3.org/2000/svg" p-id="5310" width="25" height="25"><path
                               d="M512 512m-91.264 0a91.264 91.264 0 1 0 182.528 0 91.264 91.264 0 1 0-182.528 0Z"
                               fill="#000000" p-id="5311"></path><path
                               d="M256.341333 693.546667l-20.138666-5.12C86.101333 650.496 0 586.112 0 511.829333s86.101333-138.666667 236.202667-176.597333l20.138666-5.077333 5.674667 19.968a1003.946667 1003.946667 0 0 0 58.154667 152.661333l4.309333 9.088-4.309333 9.088a994.432 994.432 0 0 0-58.154667 152.661333l-5.674667 19.925334zM226.858667 381.866667c-114.090667 32.042667-184.106667 81.066667-184.106667 129.962666 0 48.853333 70.016 97.877333 184.106667 129.962667a1064.533333 1064.533333 0 0 1 50.432-129.962667A1056.085333 1056.085333 0 0 1 226.858667 381.866667z m540.8 311.68l-5.674667-20.010667a996.565333 996.565333 0 0 0-58.197333-152.618667l-4.309334-9.088 4.309334-9.088a999.253333 999.253333 0 0 0 58.197333-152.661333l5.674667-19.968 20.181333 5.077333c150.058667 37.930667 236.16 102.314667 236.16 176.64s-86.101333 138.666667-236.16 176.597334l-20.181333 5.12z m-20.949334-181.717334c20.48 44.330667 37.418667 87.893333 50.432 129.962667 114.133333-32.085333 184.106667-81.109333 184.106667-129.962667 0-48.896-70.016-97.877333-184.106667-129.962666a1057.621333 1057.621333 0 0 1-50.432 129.962666z"
                               fill="#000000" p-id="5312"></path><path
                               d="M226.56 381.653333l-5.674667-19.925333C178.688 212.992 191.488 106.410667 256 69.205333c63.274667-36.522667 164.864 6.613333 271.317333 115.882667l14.506667 14.890667-14.506667 14.890666a1004.885333 1004.885333 0 0 0-103.338666 126.592l-5.76 8.234667-10.026667 0.853333a1009.365333 1009.365333 0 0 0-161.493333 26.026667l-20.138667 5.077333z m80.896-282.88c-11.434667 0-21.546667 2.474667-30.08 7.381334-42.410667 24.448-49.92 109.44-20.693333 224.128a1071.872 1071.872 0 0 1 137.941333-21.376 1060.138667 1060.138667 0 0 1 87.552-108.544c-66.56-64.810667-129.578667-101.589333-174.72-101.589334z m409.130667 868.778667c-0.042667 0-0.042667 0 0 0-60.8 0-138.88-45.781333-219.904-128.981333l-14.506667-14.890667 14.506667-14.890667a1003.946667 1003.946667 0 0 0 103.296-126.634666l5.76-8.234667 9.984-0.853333a1008.213333 1008.213333 0 0 0 161.578666-25.984l20.138667-5.077334 5.717333 19.968c42.112 148.650667 29.354667 255.274667-35.157333 292.437334a101.546667 101.546667 0 0 1-51.413333 13.141333z m-174.762667-144.256c66.56 64.810667 129.578667 101.589333 174.72 101.589333h0.042667c11.392 0 21.546667-2.474667 30.037333-7.381333 42.410667-24.448 49.962667-109.482667 20.693333-224.170667a1067.52 1067.52 0 0 1-137.984 21.376 1052.757333 1052.757333 0 0 1-87.509333 108.586667z"
                               fill="#000000" p-id="5313"></path><path
                               d="M797.44 381.653333l-20.138667-5.077333a1001.770667 1001.770667 0 0 0-161.578666-26.026667l-9.984-0.853333-5.76-8.234667a998.997333 998.997333 0 0 0-103.296-126.592l-14.506667-14.890666 14.506667-14.890667C603.093333 75.861333 704.64 32.725333 768 69.205333c64.512 37.205333 77.312 143.786667 35.157333 292.48l-5.717333 19.968zM629.333333 308.906667c48.725333 4.437333 95.018667 11.648 137.984 21.376 29.269333-114.688 21.717333-199.68-20.693333-224.128-42.154667-24.362667-121.386667 12.970667-204.8 94.208A1060.224 1060.224 0 0 1 629.333333 308.906667zM307.456 967.552A101.546667 101.546667 0 0 1 256 954.410667c-64.512-37.162667-77.312-143.744-35.114667-292.437334l5.632-19.968 20.138667 5.077334c49.28 12.416 103.637333 21.162667 161.493333 25.984l10.026667 0.853333 5.717333 8.234667a1006.762667 1006.762667 0 0 0 103.338667 126.634666l14.506667 14.890667-14.506667 14.890667c-80.981333 83.2-159.061333 128.981333-219.776 128.981333z m-50.773333-274.218667c-29.269333 114.688-21.717333 199.722667 20.693333 224.170667 42.112 24.021333 121.301333-13.013333 204.8-94.208a1066.581333 1066.581333 0 0 1-87.552-108.586667 1065.642667 1065.642667 0 0 1-137.941333-21.376z"
                               fill="#000000" p-id="5314"></path><path
                               d="M512 720.128c-35.114667 0-71.210667-1.536-107.349333-4.522667l-10.026667-0.853333-5.76-8.234667a1296.554667 1296.554667 0 0 1-57.6-90.538666 1295.104 1295.104 0 0 1-49.749333-95.061334l-4.266667-9.088 4.266667-9.088a1292.8 1292.8 0 0 1 49.749333-95.061333c17.664-30.549333 37.077333-61.013333 57.6-90.538667l5.76-8.234666 10.026667-0.853334a1270.826667 1270.826667 0 0 1 214.741333 0l9.984 0.853334 5.717333 8.234666a1280.256 1280.256 0 0 1 107.392 185.6l4.309334 9.088-4.309334 9.088a1262.933333 1262.933333 0 0 1-107.392 185.6l-5.717333 8.234667-9.984 0.853333c-36.138667 2.986667-72.277333 4.522667-107.392 4.522667z m-93.738667-46.250667c63.146667 4.736 124.330667 4.736 187.52 0a1237.589333 1237.589333 0 0 0 93.696-162.048 1219.626667 1219.626667 0 0 0-93.738666-162.048 1238.656 1238.656 0 0 0-187.477334 0 1215.018667 1215.018667 0 0 0-93.738666 162.048 1242.197333 1242.197333 0 0 0 93.738666 162.048z"
                               fill="#000000" p-id="5315"></path></svg>
                       </span>
                       <span>测试管理系统</span>
                   </div>
                    <Form
                        className='formBody'
                        name="basic"
                        layout="vertical"
                        initialValues={{ remember: true }}
                        onFinish={onFinish}
                        autoComplete="off"
                        size='large'
                    >
                        <Form.Item
                            label="账户"
                            name="userName"
                            rules={[{ required: true, message: '账户不能为空!' }]}
                        >
                            <Input />
                        </Form.Item>

                        <Form.Item
                            label="密码"
                            name="password"
                            rules={[{ required: true, message: '密码不能为空!' }]}
                        >
                            <Input.Password />
                        </Form.Item>

                        <div className='noUserName'>
                            <span>还没有账户?</span>
                        </div>

                        <Form.Item>
                            <Button className='formBtn' type="primary" htmlType="submit">
                                登录
                            </Button>
                        </Form.Item>
                    </Form>
                </div>
            </div>
        </main>
    )
}

export default Login

5.封装两个工具类方法/src/comon/utils/toolUtil

复制代码
/**
 * 工具类
 */

import { message } from 'antd';
/**
 * 判空
 */
export const isEmpty = (val) => {
    if (val === undefined) return true;
    if (val === null) return true;
    if (val === '') return true;
    if (val.length === 0) return true;
    if (typeof val === 'object') {
        return Object.keys(val).length === 0;
    }
    if(typeof val === 'number') {
        return val === 0;
    }
    if (typeof val === 'string') {
        return val === '0';
    }
    if (typeof val === 'boolean') {
        return val;
    }
    if (typeof val === 'undefined'){
        return false;
    }
}

/**
 * 消息提示
 */
export const useMsg = () => {
    const [messageApi, contextHolder] = message.useMessage();
    const isMsg = ({ code, msg }) => {
        let type = 'info';
        if (code === 200) type = 'success';
        else if (code >= 400 && code < 500) type = 'warning';
        else if (code >= 500) type = 'error';
        messageApi[type](msg);
    };
    let msgTitle = contextHolder
    return { isMsg, msgTitle };
};

十二,全局守卫

1.在router/index.js 里面来个简单示例

复制代码
// 白名单路径
const WHITE_LIST = ['/login']

// 路由守卫组件
function AuthRoute({ children }) {
    const location = useLocation();
    // 替换为实际的登录状态检查,例如从redux、context或localStorage获取
    const isLogin = localStorage.getItem('token') !== null;

    // 如果在白名单中直接放行
    if (WHITE_LIST.includes(location.pathname)) {
        return children;
    }

    // 不在白名单但已登录,放行
    if (isLogin) {
        return children;
    }

    // 否则重定向到登录页,并携带来源路径以便登录后跳转
    return <Navigate to="/login" state={{ from: location }} replace />;
}

完整示例:

复制代码
import {createHashRouter, Navigate, useLocation} from 'react-router-dom'
import Home from "../pages/home";
import Login from "../pages/login";
import Admin from "../pages/admin";
import Entry from "../pages/entry";


// 白名单路径
const WHITE_LIST = ['/login']


export const routes = createHashRouter([
    {
        path: '/login',
        element: <Login />,
    },
    {
        index: true,
        element: <Navigate to="/login" />,
    },
    {
        path: '/entry/*',
        meta: { auth: true },
        element: <AuthRoute><Entry /></AuthRoute> ,
        children: [
            { path: 'home', element: <Home /> },
            { path: 'admin', element: <Admin /> },
            { index: true, element: <Navigate to="home" /> },
            { path: '*', element: <Navigate to="/404" /> }
        ]
    },
    {
        path: '*',
        element: <Navigate to="/404" />
    }
]);

// 路由守卫组件
function AuthRoute({ children }) {
    const location = useLocation();
    // 替换为实际的登录状态检查,例如从redux、context或localStorage获取
    const isLogin = localStorage.getItem('token') !== null;

    // 如果在白名单中直接放行
    if (WHITE_LIST.includes(location.pathname)) {
        return children;
    }

    // 不在白名单但已登录,放行
    if (isLogin) {
        return children;
    }

    // 否则重定向到登录页,并携带来源路径以便登录后跳转
    return <Navigate to="/login" state={{ from: location }} replace />;
}

十三,设置反向代理

1.安装(备注:有需求可设置,无需求跳过)

yarn add http-proxy-middleware@latest --save

2.在src/setupProxy.js

复制代码
const { createProxyMiddleware } = require('http-proxy-middleware')
const {REACT_APP_BASE_URL,REACT_APP_BASE}=process.env
/**
 * 配置代理
 */
module.exports = function (app) {
    app.use(
        '/api-net',
        createProxyMiddleware({
            target: REACT_APP_BASE_URL+REACT_APP_BASE,
            changeOrigin: true,
            pathRewrite: {
                '^/api-net': ''
            },
        })
    )
}

true },

element: ,

children: [

{ path: 'home', element: },

{ path: 'admin', element: },

{ index: true, element: },

{ path: '', element: }
]
},
{
path: '
',

element:

}

]);

// 路由守卫组件

function AuthRoute({ children }) {

const location = useLocation();

// 替换为实际的登录状态检查,例如从redux、context或localStorage获取

const isLogin = localStorage.getItem('token') !== null;

复制代码
// 如果在白名单中直接放行
if (WHITE_LIST.includes(location.pathname)) {
    return children;
}

// 不在白名单但已登录,放行
if (isLogin) {
    return children;
}

// 否则重定向到登录页,并携带来源路径以便登录后跳转
return <Navigate to="/login" state={{ from: location }} replace />;

}

复制代码
# 十三,设置反向代理

1.安装(备注:有需求可设置,无需求跳过)

yarn add http-proxy-middleware@latest --save

2.在src/setupProxy.js

const { createProxyMiddleware } = require('http-proxy-middleware')

const {REACT_APP_BASE_URL,REACT_APP_BASE}=process.env

/**

  • 配置代理
    */
    module.exports = function (app) {
    app.use(
    '/api-net',
    createProxyMiddleware({
    target: REACT_APP_BASE_URL+REACT_APP_BASE,
    changeOrigin: true,
    pathRewrite: {
    '^/api-net': ''
    },
    })
    )
    }
复制代码
相关推荐
G_G#2 分钟前
纯前端js插件实现同一浏览器控制只允许打开一个标签,处理session变更问题
前端·javascript·浏览器标签页通信·只允许一个标签页
@大迁世界18 分钟前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路27 分钟前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug30 分钟前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu1213832 分钟前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中1 小时前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路1 小时前
GDAL 实现矢量合并
前端
hxjhnct1 小时前
React useContext的缺陷
前端·react.js·前端框架
前端 贾公子1 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端
菩提小狗1 小时前
Sqlmap双击运行脚本,双击直接打开。
前端·笔记·安全·web安全