React+Antd+Vite+TypeScript 项目实战教程

news2024/10/7 6:47:01

本教程属于react入门教程,课程围绕如何搭建一个项目框架展开,会带你快速了解reactreduxredux-devtoolreact-router-domaxiox这些常见技术的使用方式,教程最后会附上项目源码。

一、创建项目

在搭建项目时,我们通常会使用cli工具来搭建,方便、快捷、高效,一行命令就能生成一个完整的脚手架项目,这里我们使用 vite 来创建一个 react+vite+typescript 项目;Vite官网地址

1、初始化命令

npm init vite
//或者
yarn create vite

2、项目名称

在这里插入图片描述

3、选择框架

只支持Vue3
在这里插入图片描述

4、选择语言

在这里插入图片描述

5、安装依赖并启动

cd react-vite-project
npm install
npm run dev

启动完成后,控制台打印如下:
在这里插入图片描述
打开:http://localhost:5173
在这里插入图片描述

二、目录架构

按照不同的功能,一般会把项目目录安装下面的结构来划分:

项目目录:
├─node_modules		//第三方依赖
├─public			//静态资源(不参与打包)
└─src
    ├─assets		//静态资源
    ├─components	//组件
    ├─config		//配置
    ├─http			//请求方法封装
    ├─layout		//页面布局
    ├─pages			//页面
    ├─routes		//路由
    ├─service		//请求
    ├─store			//状态管理
    └─util			//通用方法
    └─App.css
    └─App.tsx
    └─index.css
    └─main.tsx
    └─vite-env.d.ts
├─.eslinttrc.cjs
├─.gitignore		
├─index.html		//项目页面总入口
├─package.json	
├─tsconfig.json		//ts配置文件
├─tsconfig.node.json
├─vite.config.ts	//vite配置文件

三、支持scss

1、下载

npm i sass -D

2、创建assets/styles/index.scss文件

$red:red;

3、引入index.scss文件

//打开vite.config.ts,添加scss的预编译选项
export default defineConfig({
  ...
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "./assets/styles/index.scss";`
      },
    }
  }
})

4、调用

//1、修改App.css > App.scss
//2、添加scs语法设置字体颜色
h1{
  color:$red;
}
//3、修改引入的文件名
//import './App.css'
修改为:
import './App.scss'

最终效果如下:
在这里插入图片描述

四、useState钩子

1、useState 案例

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

2、同上功能的Class写法

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

官网说明:Using the State Hook

五、路由

1、下载

npm install react-router-dom -S
//npm i @types/react-router-dom	//默认是带代码提示的(非必须)

2、HashRouter路由

main.tsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { HashRouter as Router } from 'react-router-dom';
import './index.css'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <Router>
      <App />
    </Router>
  </React.StrictMode>,
)

App.tsx

import { useState } from 'react'
import { HashRouter, Route, Routes, Link, useNavigate } from "react-router-dom";
import Login from "./pages/login";
import Home from "./pages/home";
import User from "./pages/user";
import './App.scss'

function App() {
  const [count, setCount] = useState(0)
  const navigate=useNavigate();
  return (
      <div className="App">
      {/* 指定跳转的组件,to 用来配置路由地址 */}
      <Link to="/">首页</Link><br />
      <Link to="/user">用户</Link><br />
      <button onClick={() => navigate('/login')}> 登录 </button>
      {/* 路由出口:路由对应的组件会在这里进行渲染 */}
      <Routes>
        {/* 指定路由路径和组件的对应关系:path 代表路径,element 代表对应的组件,它们成对出现 */}
        <Route path='/' element={<Home />}></Route>
        <Route path='/user' element={<User />}></Route>
        <Route path='/login' element={<Login />}></Route>
      </Routes>
    </div>
  )
}

export default App

login.tsx + home.tsx + user.tsx

//login.tsx
function Login() {
    return (
        <div>login页面</div>
    );
}
export default Login;

//home.tsx
function Home() {
    return (
        <div>home页面</div>
    );
}
export default Home;

//user.tsx
function User() {
    return (
        <div>user页面</div>
    );
}
export default User;

这时候,打开http://localhost:5173/,如图:
请添加图片描述

3、BrowserRouter路由

main.tsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import { BrowserRouter as Router } from 'react-router-dom';
import './index.css'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <Router>
      <App />
    </Router>
  </React.StrictMode>,
)

App.tsx

import { useState } from 'react'
import { Route, Routes, Link, useNavigate } from 'react-router-dom';

import Login from "./pages/login";
import Home from "./pages/home";
import User from "./pages/user";
import './App.scss'

function App() {
  const navigate = useNavigate();
  return (
    <div className="App">
      {/* 指定跳转的组件,to 用来配置路由地址 */}
      <Link to="/">首页</Link><br />
      <Link to="/user">用户</Link><br />
      <button onClick={() => navigate('/login')}> 登录 </button>
      {/* 路由出口:路由对应的组件会在这里进行渲染 */}
      <Routes>
        {/* 指定路由路径和组件的对应关系:path 代表路径,element 代表对应的组件,它们成对出现 */}
        <Route path='/' element={<Home />}></Route>
        <Route path='/user' element={<User />}></Route>
        <Route path='/login' element={<Login />}></Route>
      </Routes>
    </div>
  )
}

export default App

这时候,打开http://localhost:5173/,如图

请添加图片描述

4、嵌套路由

App.tsx

import { useState } from 'react'
import { Route, Routes, Link, useNavigate } from 'react-router-dom';

import Login from "./pages/login";
import Home from "./pages/home";
import User from "./pages/user";
import './App.scss'

function App() {
  const navigate = useNavigate();
  return (
    <div className="App">
      {/* 指定跳转的组件,to 用来配置路由地址 */}
      <Link to="/home">首页</Link><br />
      <Link to="/home/user">用户</Link><br />
      <button onClick={() => navigate('/home/login')}> 登录 </button>
      {/* 路由出口:路由对应的组件会在这里进行渲染 */}
      <Routes>
        {/* 指定路由路径和组件的对应关系:path 代表路径,element 代表对应的组件,它们成对出现 */}
        <Route path='/home' element={<Home />}>
          <Route path='user' element={<User />}></Route>
          <Route path='login' element={<Login />}></Route>
        </Route>
      </Routes>
    </div>
  )
}

export default App

home.tsx

import { Outlet } from "react-router-dom";
function Home() {
    return (
        <div>
            <div>home页面</div>
            <Outlet />
        </div>
    );
}

export default Home;

页面显示如下
请添加图片描述

5、默认子级路由

在嵌套路由下的子级路由,如果我们要设置一个默认路由,那么只要在路由上添加index属性。

<Routes>
    <Route path='/home' element={<Home />}>
        <Route path='user' element={<User />}></Route>
        <Route index element={<Login />}></Route>
    </Route>
</Routes>

6、重定向

import { Navigate } from 'react-router-dom';

<Route path='/' element={<Navigate to="/layout" />}></Route>

在做权限验证的时候我们可以使用重定向,如果已经登录则进入主页,没有登录则进入登录页面。

7、useRoutes路由配置

上面的路由我们都是在App.tsx中手动写,不太方便,实际项目中我们的路由会在配置文件中统一配置,这时候我们可以使用useRoutes来实现路由配置;

App.tsx

import GetRoutes from "@/routes/index";
function App() {
  return (
    <GetRoutes></GetRoutes>
  )
}
export default App

/routes/index.tsx

import { useRoutes, Navigate, RouteObject } from "react-router-dom";

import Layout from "@/layout/index";
import Login from "@/pages/login";
import Home from "@/pages/home";
import User from "@/pages/user";

export const router_item: Array<object> = [
    { path: "/", label: "首页", element: <Navigate to="/layout/home" /> },
    {
        path: "/layout",
        label: "控制台",
        element: <Layout />,
        children: [
            {
                path: "home",
                label: "首页",
                element: <Home />
            },
            {
                path: "login",
                label: "登录页",
                element: <Login />,
            },
            {
                path: "user",
                label: "用户页",
                element: <User />
            },
        ],
    },
];

function GetRoutes() {
    const routes: RouteObject[] = useRoutes(router_item);

    return routes;
}

export default GetRoutes;

实际效果如下图:
请添加图片描述

8、路由懒加载

有些页面比较大,我们可以使用懒加载,来提升页面加载性能,避免页面卡顿;react官网提供了路由懒加载的完整实例:react路由懒加载。懒加载主要借助lazysuspense组件来实现。

lazy 能够让你在组件第一次被渲染之前延迟加载组件的代码。<Suspense> 允许您显示临时组件(一般是一个loading状态),直到其子项完成加载。

实例演示:

import { lazy } from 'react';
const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));

<Suspense fallback={<Loading />}>
  <h2>Preview</h2>
  <MarkdownPreview />
 </Suspense>

MarkdownPreview.js是需要加载的组件,使用lazy()生成一个LazyExoticComponent对象,然后包裹在suspense组件中,fallback={<Loading />}中的Loading是一个组件,显示加载过程中的页面。


知道了上述的使用方式,我们可以这样在项目中来使用懒加载。

  1. 封装lazyLoad(),用于传入lazy()生成的LazyExoticComponent对象,返回一个suspense组件
  2. 修改路由中组件引入方式,改成lazyLoad(lazy(() => import("@/pages/home")))

具体代码如下:

/routes/index.tsx(请查看home、user组件的引入方式)

import { useRoutes, Navigate, RouteObject } from "react-router-dom";
import { lazy } from "react";
//页面
import Layout from "@/layout/index";
import Login from "@/pages/login";
import Error404 from "@/pages/error/404";

//公共
import lazyLoad from "./lazyLoad";

// 添加一个固定的延迟时间,以便你可以看到加载状态
function delayForDemo(promise: Promise<any>) {
    return new Promise(resolve => {
        setTimeout(resolve, 2000);
    }).then(() => promise);
}

export const router_item: Array<object> = [
    {
        path: "/",
        key: "/",
        label: "首页",
        hidden: true,
        element: <Navigate to="/login" />
    },
    {
        path: "/login",
        key: "login",
        label: "登录",
        hidden: true,
        element: <Login />,
        meta: {
            noAuth: true    //不需要检验
        }
    },
    {
        path: "/layout",
        key: "layout",
        label: "控制台",
        element: <Layout />,
        children: [
            {
                path: "home",
                key: "home",
                label: "首页",
                element: lazyLoad(lazy(() => delayForDemo(import("@/pages/home")))) //故意延迟2s,这里是延迟加载
            },
            {
                path: "user",
                key: "user",
                label: "用户",
                element: lazyLoad(lazy(() => import("@/pages/user"))),	//这里是延迟加载
                children: [
                    {
                        path: "home1",
                        key: "home1",
                        label: "首页1",
                        element: lazyLoad(lazy(() => import("@/pages/home"))),//这里是延迟加载
                    },
                    {
                        path: "user1",
                        key: "user1",
                        label: "用户1",
                        element: lazyLoad(lazy(() => import("@/pages/user"))),//这里是延迟加载
                    },
                ],
            },
        ],
    },
    {
        path: "/404",
        hidden: true,
        element: <Error404 />,
        meta: {
            noAuth: true    //不需要检验
        }
    },
    {
        path: "*",
        hidden: true,
        element: <Navigate to="/404" />
    },
];

function GetRoutes() {
    const routes: RouteObject[] = useRoutes(router_item);

    return routes;
}

export default GetRoutes;

/routes/lazyLoad.tsx

import { LazyExoticComponent, Suspense } from "react";
import Spinner from "@/components/spinner";
/**
 * 实现路由懒加载
 * @param Comp 懒加载组件
 * @returns 
 */
function lazyLoad(Comp: LazyExoticComponent<() => JSX.Element>) {
    return (
        <Suspense fallback={<Spinner />}>
            <Comp />
        </Suspense>
    );
}

export default lazyLoad;

/compoments/spinner.tsx

function Spinner() {
    return (
        <>
            loading...
        </>
    );
}

export default Spinner;

由于home加载留有2s的延迟,我们看下主页加载效果,能看到loading…加载状态,如图:
请添加图片描述

六、布局

1、框架

在这里插入图片描述

2、代码

/layout/index.tsx代码如下:

import "./index.scss";
import { Outlet } from "react-router-dom";
import Aside from "./aside"

function Layout() {
    return (
        <section id="container">
            <aside>
                <Aside></Aside>
            </aside>
            <section>
                <header>header</header>
                <main>
                    <Outlet></Outlet>
                </main>
            </section>
        </section>
    );
}

export default Layout;

七、菜单

由于前面我们已经设置了路由,在生成菜单的时候可以使用路由的数据来直接生成,结构一致,不过需要补充keylabelhidden属性;

生产菜单时我们需要考虑多层级菜单、刷新后自动展开上次选中的菜单;

1、安装antd

npm i antd --save

2、代码

路由数据如下:

/routes/index.tsx

import { useRoutes, Navigate, RouteObject } from "react-router-dom";

import Layout from "@/layout/index";
import Login from "@/pages/login";
import Home from "@/pages/home";
import User from "@/pages/user";

export const router_item: Array<object> = [
    {
        path: "/",
        key: "/",
        label: "首页",
        hidden: true,
        element: <Navigate to="/layout/home" />
    },
    {
        path: "/layout",
        key: "layout",
        label: "控制台",
        element: <Layout />,
        children: [
            {
                path: "login",
                key: "login",
                label: "登录",
                element: <Login />,
            },
            {
                path: "home",
                key: "home",
                label: "首页",
                element: <Home />
            },
            {
                path: "user",
                key: "user",
                label: "用户",
                element: <User />,
                children: [
                    {
                        path: "home1",
                        key: "home1",
                        label: "首页1",
                        element: <Home />
                    },
                    {
                        path: "user1",
                        key: "user1",
                        label: "用户1",
                        element: <User />
                    },
                ],
            },
        ],
    },
];

function GetRoutes() {
    const routes: RouteObject[] = useRoutes(router_item);

    return routes;
}

export default GetRoutes;

aside.tsx

// react hook
import { useState } from "react";
import { router_item } from "@/routes/index";
import { Menu } from "antd";
import { useNavigate } from "react-router-dom";

function aside() {
    const navigate = useNavigate();

    //菜单
    const [routes] = useState<any[]>(router_item);

    //打开和选中
    const defaultOpenKeys = (localStorage.getItem("openKeys") || "")?.split(",");
    const defaultSelectKeys = localStorage.getItem("selectKeys") || "";
    const [selectKeys, setSelectKeys] = useState<string[]>([defaultSelectKeys]);
    const [openKeys, setOpenKeys] = useState<string[]>(defaultOpenKeys);
    //点击菜单
    const menuHandler = (e: any) => {
        let path = "/" + e.keyPath.reverse().join("/");
        path = path.replace("//", "/");
        navigate(path);

        // 缓存打开和选中的keys
        const selectKeys = e.key;
        e.keyPath.pop();
        const openKeys = e.keyPath.join(",");
        setSelectKeys(selectKeys);
        setOpenKeys(openKeys);
        localStorage.setItem("selectKeys", selectKeys);
        localStorage.setItem("openKeys", openKeys);
    }

    return (
        <>
            <Menu
                mode="inline"
                theme="dark"
                defaultOpenKeys={openKeys}
                defaultSelectedKeys={selectKeys}
                items={routes}
                onClick={menuHandler}></Menu>
        </>
    );
}

export default aside;

3、菜单效果

请添加图片描述

八、登录页

这是一个最简单的登录页,交互很简单,输入用户名和密码,验证通过,点击登录,登录成功则跳转到内部控制台页面,不成功则给出错误提示。

1、安装@ant-design/icons

npm i @ant-design/icons --save

2、代码

/pages/login/index.tsx

import "./index.scss";
import reactIcon from "@/assets/react.svg";

import { Button, Checkbox, Form, Input, message } from 'antd';
import { UserOutlined, LockOutlined } from "@ant-design/icons";
import { useNavigate } from "react-router-dom";

const styles = {
    login: {
        background: `linear-gradient(blue, pink)`,
        width: "100vw",
        height: "100vh",
    }
};
interface loginData {
    username: string;
    password: string;
    remember: string;
}

function Login() {
    const navigate = useNavigate();
    const [messageApi, contextHolder] = message.useMessage();

    const onFinish = (values: loginData) => {
        messageApi.open({
            type: 'loading',
            content: '正在登录..',
            duration: 0,
        });

        setTimeout(() => {
            messageApi.destroy();
            //默认请求延时
            if (values.username == "admin" && values.password == "123") {
                messageApi.open({
                    type: 'success',
                    content: '登录成功!',
                    onClose() {
                        navigate("/layout/home");
                    }
                });
                return;
            } else {
                messageApi.open({
                    type: 'error',
                    content: '登录失败!',
                });
            }
        }, 500);
    };

    const onFinishFailed = (errorInfo: any) => {
        // console.log('Failed:', errorInfo);
    };
    return (
        <div className="login_container" style={styles.login}>
            {contextHolder}
            <div className="title_big">Ant Design后台管理系统 <img src={reactIcon} style={{ marginLeft: "5px" }} /></div>
            <div className="login_panel">
                <Form
                    name="basic"
                    labelCol={{ span: 0 }}
                    wrapperCol={{ span: 24 }}
                    style={{ maxWidth: 600 }}
                    initialValues={{ remember: true }}
                    onFinish={onFinish}
                    onFinishFailed={onFinishFailed}
                    autoComplete="off"
                >
                    <Form.Item
                        name="username"
                        rules={[
                            { required: true, message: '请输入用户名!' },
                            ({ getFieldValue }) => ({
                                validator(_, value) {
                                    if (value === "admin") {
                                        return Promise.resolve();
                                    }
                                    return Promise.reject(new Error('用户名不存在!'));
                                },
                            }),
                        ]}
                    >
                        <Input prefix={<UserOutlined />} placeholder="用户名" autoComplete="off" />
                    </Form.Item>

                    <Form.Item
                        name="password"
                        rules={[{ required: true, message: '请输入密码!' }]}
                    >
                        <Input.Password prefix={<LockOutlined />} placeholder="密码" />
                    </Form.Item>

                    <Form.Item wrapperCol={{ span: 24 }} name="remember" valuePropName="checked">
                        <Checkbox>记住密码</Checkbox>
                        {/* <Button type="link" htmlType="button" style={{ float: "right" }}>
                            忘记密码?
                        </Button> */}
                    </Form.Item>

                    <Form.Item wrapperCol={{ span: 24 }}>
                        <Button type="primary" htmlType="submit" block>
                            登录
                        </Button>
                    </Form.Item>
                </Form>
            </div>
        </div>
    );
}

export default Login;

pages/login/index.scss

.login_container {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;

    .title_big {
        width: 400px;
        height: 50px;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 2rem;
        // letter-spacing: 0.1rem;
        margin-bottom: 1rem;
        background: #fff;
        border-radius: 5px;
        position: relative;
        color: #0052b6;
    }

    .login_panel {
        width: 400px;
        background-color: white;
        padding: 15px;
        box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
        border-radius: 5px;

        .title {
            height: 40px;
            line-height: 40px;
            font-size: 1.5rem;
            border-bottom: 1px solid #eee;
            margin-bottom: 10px;
        }
    }
}

登录页面用到了ANTD中的Form表单表单自定义验证消息提示navigate跳转

登录成功效果:
请添加图片描述

登录失败效果:
请添加图片描述

九、权限校验

实际项目在进入某个页面中前,是需要做校验的:token校验、权限校验、登录状态校验等等。

1、token校验

在进入除login页面以外的页面时,我们要对页面进行拦截,并进行token校验,判断token是否存在,存在则进入;不存在则跳转到登录页面进行登录;

/components/AutoRouter.tsx

//这个组件用于拦截判断token是否存在。
import { useLocation, Navigate } from "react-router-dom";
function AutoRouter(props: { children: JSX.Element }) {
    const { pathname } = useLocation();
    //校验
    if (pathname.startsWith("/login")) {
        return props.children;
    }
    const token = localStorage.getItem("token");
    if (token) {
        // 1、存在token,则进入主页
        return props.children;
    } else {
        // 2、如果不存在token,则进入登录页
        return <Navigate to="/login" />
    }
}

export default AutoRouter;

App.tsx

//AutoRouter作为父组件,包裹所有子组件
import GetRoutes from "@/routes/index";
import AutoRouter from "@/components/autoRouter";

function App() {
  return (
    <AutoRouter>
      <GetRoutes></GetRoutes>
    </AutoRouter>
  )
}

export default App

/pages/login/index.tsx

//修改onFinish回调方法,加入token设置逻辑
const onFinish = (values: loginData) => {
        ...
        setTimeout(() => {
            //默认请求延时
            if (values.username == "admin" && values.password == "123") {
                localStorage.setItem("token","djalkdjadjlasj3123123");    //----登录成功后,设置token
               ...
                return;
            } else {
                ...
            }
        }, 500);
    };

token丢失效果演示:
请添加图片描述

2、路由权限配置化

上面我们在进入login页面时,在autoRoute页面中针对login路径不做权限校验,但是正常项目中大量的页面需要免校验,如果没有都要这么来写,使用不是很方便。可以将是否要检验,直接写在路由清单中。然后在页面跳转时,拦截目标页面对应的路由配置,查看是否声明需要做校验。

之前autoRoute.tsx中关于login页面免校验的代码:

function AutoRouter(props: { children: JSX.Element }) {
    const { pathname } = useLocation();
    //校验
    if (pathname.startsWith("/login")) {
        return props.children;//-----手动适配,不是很合理
    }
    ...
}

我们可以改成下面这种形式:

//login路由配置noAuth
    {
        path: "/login",
        key: "login",
        label: "登录",
        hidden: true,
        element: <Login />,
        meta: {
            noAuth: true    //不需要检验
        }
    },
    
//autoRoute.tsx中的拦截逻辑
function AutoRouter(props: { children: JSX.Element }) {
    const { pathname } = useLocation();
    const token = localStorage.getItem("token");
    //1、获取当前路径对应的路由配置
    const route = matchRoute(pathname, router_item);
    //2、如果noAuth为true,则直接跳过校验
    if (route && route.meta && route.meta.noAuth) {
        // 路由配置noAuth,则不需要校验
        return props.children;
    }
    if (token) {
        // 1、存在token,则进入主页
        return props.children;
    } else {
        // 2、如果不存在token,则进入登录页
        return <Navigate to="/login" />
    }
}

/util/util.ts

export interface MetaProp {
    title: string;
    key: string;
    noAuth: boolean;
}
export interface RouteObject {
    children?: RouteObject[];
    element?: React.ReactNode;
    path?: string;
    meta?: Partial<MetaProp>
}

/**
 * 获取路径对应的路由配置,没有则返回null
 * @param path 路由完整路径
 * @param routes 路由数组
 * @returns 路由配置项
 */
export function matchRoute(path: string, routes: RouteObject[] = []): any {
    const pathArr = path.split("/");
    pathArr.shift();
    const curPath = pathArr.shift();
    let result: any = null;
    for (let i = 0; i < routes.length; i++) {
        const item = routes[i];
        if ([curPath, `/${curPath}`].includes(item.path)) {
            if (!pathArr.length) {
                return item;
            }
            if (item.children) {
                const res = matchRoute(`/${pathArr.join("/")}`, item.children);
                if (res) {
                    return res;
                }
            }
        }
    }
    return result;
}

这样,我们如果要对某个页面做免校验设置,直接在/routes/index.tsx对应的路由配置中加上meta.noAuth:true就行,例如:404页面、忘记密码、登录页都可以加上。

十、Redux状态管理

1、redux

类似于Vue中的Vuex,react中可以使用Redux来做状态管理。
请添加图片描述
下面我们尝试创建一个redux状态实例,然后在页面中获取或者更新状态值,页面上的值也会相应改变。

创建 /redux/index.ts

import { legacy_createStore as createStote } from "redux";

//定义数据
const initState = {
    count: 0,
    name: "IT飞牛"
};

// 关联action
function countReducer(state = initState, action: any) {
    switch (action.type) {
        case "ADD_COUNT":
            return { ...state, count: state.count + action.number };
        case "UPDATE_NAME":
            return { ...state, name: action.name };
        default:
            return state;
    }
}
const store = createStote(countReducer);

export default store;

修改 /pages/home.tsx 代码,点击按钮修改name值

import { Outlet } from "react-router-dom";
import store from "@/redux";
function Home() {

    const updateCount = () => {
        store.dispatch({ type: "UPDATE_NAME", name: "IT飞牛,前端行业的一个小学生!" });
    }
    return (
        <>
            <button onClick={updateCount}>修改name</button>
            <Outlet />
        </>
    );
}

export default Home;

修改 /layout/index.tsx代码,监听store的变化,并同步修改name,显示到页面

import "./index.scss";
import { Outlet } from "react-router-dom";
import Aside from "./aside"
import store from "@/redux";
import { useState } from "react";

function Layout() {
    const [name, setName] = useState<string>(store.getState().name);    //获取store.name作为默认值
    store.subscribe(() => {
        const store_data = store.getState();
        setName(store_data.name);   //更新store.name值
    });
    return (
        <section id="container">
            <aside>
                <Aside></Aside>
            </aside>
            <section>
                <header>header-{name}</header>  {/* store.name显示到页面 */}

                <main>
                    <Outlet></Outlet>
                </main>
            </section>
        </section>
    );
}

export default Layout;

最终效果:

请添加图片描述

2、action拆分

项目中的状态数据往往很多,可能每个模块都会有一批状态,这时候我们就需要对状态进行拆分管理,这里我们可以借助combineReducers来实现。

combineReducers作用:

将值是不同 reducer 函数的对象转换为单个 reducer 函数。 它将调用每个子 reducer,并将它们的结果收集到一个状态对象中,其键对应于传递的 reducer 函数的键。

具体步骤:

  1. 准备countReducernameReducer两个reducer
  2. 使用combineReducers将上述两个reducer合并成mixReducer
  3. 使用mixReducer创建一个store实例
  4. 更新:store.dispatch()
  5. 取值:store.getState().nameReducer[state值]

具体代码如下:

/redux/action.ts

//定义数据
const initState1 = {
  count: 0,
};

export function countReducer(state = initState1, action: any) {
  switch (action.type) {
    case "ADD_COUNT":
      return { ...state, count: state.count + action.number };
    default:
      return state;
  }
}

const initState2 = {
  name: "IT飞牛",
};

export function nameReducer(state = initState2, action: any) {
  switch (action.type) {
    case "UPDATE_NAME":
      return { ...state, name: action.name };
    default:
      return state;
  }
}

/redux/index.ts

import {
  legacy_createStore as createStote,
  combineReducers,
} from "redux";
import { countReducer, nameReducer } from "./action";

const reducer = combineReducers({ countReducer, nameReducer });//合并reducer

const store = createStote(reducer);

export default store;

/pages/home.tsx 代码不变,主要代码如下:

    //更新state值
    store.dispatch({
      type: "UPDATE_NAME",
      name: "IT飞牛,前端行业的一个小学生!",
    });

/page/layout.tsx中取值代码微调:

//老代码
const [name, setName] = useState<string>(store.getState().name);
store.subscribe(() => {
    const store_data = store.getState();
    setName(store_data.name);
});

//新代码
const [name, setName] = useState<string>(store.getState().nameReducer.name); //获取store.name作为默认值
store.subscribe(() => {
    const store_data = store.getState();
    setName(store_data.nameReducer.name); //更新store.name值
});

这时候,store中的数据的结构就已经按照不同reducer模块做了划分,具体如下:
请添加图片描述

3、redux-devtool调试工具

redux-devtool是一个chrome浏览器插件,安装后可以直接在浏览器中查看redux状态的变化。

  1. 安装插件

    进入谷歌应用商店,搜索redux-devtool进行安装

请添加图片描述

  1. 项目安装第三方依赖
npm i redux-thunk redux-devtools-extension --save-dev
  1. 调整/redux/indx.tsx代码,createStote创建store时,使用thunk中间件

    import {
      legacy_createStore as createStote,
      applyMiddleware,
      combineReducers,
    } from "redux";
    import thunk from "redux-thunk";
    import { composeWithDevTools } from "redux-devtools-extension";
    import { countReducer, nameReducer } from "./action";
    
    const reducer = combineReducers({ countReducer, nameReducer });
    
    const store = createStote(reducer, composeWithDevTools(applyMiddleware(thunk)));
    
    export default store;
    

最终效果:
请添加图片描述

十一、axios封装

1、安装

npm i axios --save

2、封装axios

创建/util/request.ts,代码如下:

import axios, { AxiosInstance } from "axios";
import { HttpError } from "./HttpError";

const isDev = process.env.NODE_ENV === "development";
const baseUrl = isDev ? "/api" : "/";
export const request: AxiosInstance = axios.create({
  baseURL: baseUrl,
  timeout: 30000,
  headers: {
    "Content-Type": "application/json;charset=utf-8",
  },
});

request.interceptors.request.use(
  (req) => {
    req.headers.authorization =
      "Bearer " + localStorage.getItem("ACCESS_TOKEN");
    return req;
  },
  (err) => {
    // err.message
    throw err;
  }
);

request.interceptors.response.use(
  (res) => {
    const { code } = res.data;
    if (code === 401) {
      // window.location.replace("/");
    }
    if (code != "200") {
      // Message.error(res.data.message)
      throw new HttpError(
        res.data?.message || "网络错误!",
        Number(res.data.status)
      );
    }
    return res.data;
  },
  (err) => {
    const { response } = err;
    const { code } = response.data;
    if (code === 401) {
      // window.location.replace("/");
    }
    // err.message
    throw err;
  }
);

export const http = {
  post<T>(url: string, data?: any, config?: any) {
    return request.post(url, data, config) as Promise<T>; //使用范型,代码提示更简便
  },
  get<T>(url: string, config?: any) {
    return request.get(url, config) as Promise<T>; //使用范型,代码提示更简便
  },
};

class HttpError extends Error {
    code: number;
    constructor(message: string, code: number) {
        super(message);
        this.code = code;
    }
}

3、定义请求

创建api/homeApi.ts,代码如下:

import { request, http } from "@/util/request";

//写法1
export function getInfo1() {
  return http.post<InfoRes>("/api/common/getInfo", {
    url: "/api/common/getInfo",
    method: "GET",
  });
}

//写法2
export function getInfo2(): Promise<InfoRes> {
  return request.request({
    url: "/api/common/getInfo",
    method: "GET",
  });
}

interface InfoRes {
  token: string;
}

4、发起请求

import { getInfo1, getInfo2 } from "@/api/homeApi";

const res1 = await getInfo1();
const res2 = await getInfo2();

5、Api拦截器补充说明

在实际项目中,前端发起的请求会失败,失败的原因有两类:http状态码异常、业务异常,那么我们可以在request.ts中对这两类失败做初步的统一处理。

例如针对http状态码异常,常见的4xx5xx错误,我们统一拦截,直接给出类似**服务器/网络错误,请联系管理员!**这样的message错误消息提示。

如果http状态码正常,但是业务状态码code异常,我们同样也可以做统一拦截,按照需求对业务异常做前置处理。

十二、跨域配置

1、跨域的解决方案

跨域的解决方法常见的有本地代理、CORS、服务器反向代理等等;

2、配置vite本地代理

打开vite.config.ts文件:

  server: {
    // 是否自动打开浏览器
    open: true,
    // 服务器主机名,如果允许外部访问,可设置为"0.0.0.0"
    host: '0.0.0.0',
    // 服务器端口号
    port: 3000,
    // 代理
    proxy: {
      '/v1/apigateway': {
        target: `http://12.12.12.12/`,
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/v1_redirect\/apigateway/, '')
      },
    }
  }

本地代理的原理和服务器反向代理类似,就是在本地使用Express启动一个http服务,然后所有的本地请求都会经过本地的这个Express服务转发到实际api服务器上,这样就可以绕过浏览器的同源策略。

如上配置,我们在页面中发起一个请求http://localhost/v1/apigateway/getInfo,那么经过本地代理处理后,实际转发到api服务器的地址是http://12.12.12.12/v1_redirect/apigateway/getInfo

vite更多跨域配置请查看:server.proxy

常见问题

1、找不到模块“react-router-dom”

在安装完react-router-dom之后,App.tsx中引入react-router-dom时,提示如下错误:

找不到模块“react-router-dom”。你的意思是要将 "moduleResolution" 选项设置为 "node",还是要将别名添加到 "paths" 选项中?ts(2792)

其实在react-router中,是有自带的声明文件的,如图:
在这里插入图片描述

解决方案

修改tsconfig.json,将target修改为es5

{
  "compilerOptions": {
  	"target": "es5",
  }
}

2、useNavigate报错

在使用编程式导航时,控制台报错如下:

caught Error: useNavigate() may be used only in the context of a <Router> component.

其实就是需要在使用useNavigate时,确保事件出发的dom节点外层被Router节点包裹。

修改main.tsx如下

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import {BrowserRouter as Router} from 'react-router-dom';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
     <Router>//------添加
       <App />
     </Router>
  </React.StrictMode>
);

3、删除无效的dom节点

有时候dom层级结构太复杂,我们需要清除某个不需要的dom节点,那么正常只要直接删除div标签就可以;

那么jsx语法中也可以使用空标签来实现<></>,同样不会渲染这个div节点。

项目源码

react-vite-project

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/508190.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

day38_JDBC

今日内容 上课同步视频:CuteN饕餮的个人空间_哔哩哔哩_bilibili 同步笔记沐沐霸的博客_CSDN博客-Java2301 零、 复习昨日 一、数据库连接池 二、反射 三、封装DBUtil 零、 复习昨日 SQL注入 预处理语句 String sql "select * from user where id ?"; PreparedStat…

Spring Boot访问数据库

SpringBoot访问数据库时&#xff0c;我们可以使用以下三种&#xff0c;JdbcTemplate、Spring Boot Data Jpa、mybatis。 JdbcTemplate是Spring自己提供的&#xff0c;但是其操作不方便&#xff0c;甚至有些繁琐&#xff0c;在实际应用中也是很少会使用&#xff0c;所以这里就不…

使用MobaXterm在Windows系统远程Ubuntu系统

使用场景 通常大部分办公软件都安装在Windows系统上&#xff0c;使用Windows系统办公更便捷&#xff0c;并且大多办公软件在Ubuntu系统上不兼容&#xff0c;即使进行处理&#xff0c;使用体验也不好&#xff0c;下面针对在同一局域网使用Windows远程Ubnuntu进行ssh、VNC和打开…

Nature -- 空间表观组学与转录组学联合分析,揭示基因表达的精准调控

为充分理解组织中单细胞的基因表达调控&#xff0c;需要在单细胞水平以空间分辨率的方式集成不同分子信息的不同层面&#xff0c;包括表观基因组和转录组数据。虽然单细胞多组学方法可以捕捉来自不同分子层的信息&#xff0c;但是空间组学方法到目前为止主要局限于一层分子。张…

MATLAB的无人机遥感数据预处理与农林植被性状估算实践

在新一轮互联网信息技术大发展的现今&#xff0c;无人机、大数据、人工智能、物联网等新兴技术在各行各业都处于大爆发的前夜。为了将人工智能方法引入农业生产领域。首先在种植、养护等生产作业环节&#xff0c;逐步摆脱人力依赖&#xff1b;在施肥灌溉环节构建智慧节能系统&a…

ASP.NET core WebApi Cors跨域解决

前言 我用了最新版的Asp.net webapi &#xff0c;在csdn上面搜跨域如何解决的时候&#xff0c;发现csdn上面对于.NET技术讨论不是很多。没办法&#xff0c;只能面向官方文档和GitHub编程了。 项目类型确认 NuGet包引入 前面两个已经放弃维护了&#xff0c;我们就不用了。用最…

使用Actor-Critic的DDPG强化学习算法控制双关节机械臂

在本文中&#xff0c;我们将介绍在 Reacher 环境中训练智能代理控制双关节机械臂&#xff0c;这是一种使用 Unity ML-Agents 工具包开发的基于 Unity 的模拟程序。 我们的目标是高精度的到达目标位置&#xff0c;所以这里我们可以使用专为连续状态和动作空间设计的最先进的Deep…

【uni-app】errMsg : navigateTo:fail can not navigateTo a tabbar page报错解决方案

文章目录 前言一、报错二、解决方案更改api 总结 前言 大家好&#xff0c;今天在进行uni-app项目开发时&#xff0c;在进行页面跳转的时候报了一个错误&#xff0c;一开始觉得只是个小报错就没有仔细看这个报错&#xff0c;直接就到页面检查看是不是跳转没写好&#xff0c;但是…

Spring IOC:详解【依赖注入数值问题 依赖注入方式】

编译软件&#xff1a;IntelliJ IDEA 2019.2.4 x64 操作系统&#xff1a;win10 x64 位 家庭版 Maven版本&#xff1a;apache-maven-3.6.3 Mybatis版本&#xff1a;3.5.6 spring版本&#xff1a;5.3.1 文章目录 Spring系列专栏文章目录一、Spring依赖注入数值问题1.1 字面量数值问…

计算机网络 | 广播与组播

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和…

Jetpack之livedata原理分析

1.LiveData是什么&#xff1f; 只有在生命周期处于started和resumed时。livedata才会更新观察者 2.Livedata的各种使用方式 1.更新数据 class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceSta…

Python数据分析实战【十四】:python的三种排序方法:sort、sorted、sort_values案例学习【文末源码地址】

文章目录 一、List.sort()排序案例一&#xff1a;按照列表中的元素进行排序案例二&#xff1a;按照销售额数据进行排列 二、sorted()排序案例一&#xff1a;sorted()对列表进行排序案例二&#xff1a;sorted()对字典进行排序案例三&#xff1a;sorted()对列表中的字典元素排序 …

【AI大模型】国产AI技术再创新高,讯飞星火认知大模型中文能力已经超越ChatGPT?

文章目录 前言SparkDesk讯飞星火认知大模型简介语言理解知识问答逻辑推理数学题解答代码理解与编写亲自体验写在最后 前言 5月6日&#xff0c;讯飞星火认知大模型成果发布会在安徽合肥举行。科大讯飞董事长刘庆峰、研究院院长刘聪发布讯飞星火认知大模型&#xff0c;现场实测大…

(一)ArcGIS空间数据的转换与处理——投影变换

ArcGIS空间数据的转换与处理——投影变换 原始数据往往由于在数据结构、数据组织、数据表达等方面与用户需求不一致而要进行转换与处理。本节主要介绍 ArGIS 中数据的投影变换内容。 目录 ArcGIS空间数据的转换与处理——投影变换 1.概述2.定义投影3.投影变换3.1栅格数据的投…

Python数据分析实战【十四】:Python的三种排序方法:sort()、sorted()和sort_values()【文末源码地址】

文章目录 一、List.sort()排序案例一&#xff1a;按照列表中的元素进行排序案例二&#xff1a;按照销售额数据进行排列 二、sorted()排序案例一&#xff1a;sorted()对列表进行排序案例二&#xff1a;sorted()对字典进行排序案例三&#xff1a;sorted()对列表中的字典元素排序 …

计算机网络 | 基于TCP的C/S模型代码实现

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和…

QT QGraphicsView 提升到 QChartView报错 解决方案

QT QGraphicsView 提升到 QChartView报错 解决方案 本文主要描述, 使用QT提供的QChartView来绘制图表,提升QGraphicsView控件继承QChartView后,然后将QGraphicsView提升到我们自己写的类,怎么才能确保提升后编译不报错. [问题描述] 使用QGraphicsView显示图表的时候,我们需要将…

基于Leaflet的乡镇行政区划在WebGIS中的可视化工具实践

前言 在构建WebGIS的应用系统中&#xff0c;通常会遇到以下的建设需求。功能点如下&#xff1a; 实现影像地图的展示&#xff0c;可以放大、缩小和浏览地图。地图的拖拽范围需要控制在合理的经纬度范围内。在影像地图侧边实现某乡镇级行政区的信息展示&#xff0c;包括名称&…

Java中的深拷贝和浅拷贝

目录 &#x1f34e;引出拷贝 &#x1f34e;浅拷贝 &#x1f34e;深拷贝 &#x1f34e;总结 引出拷贝 现在有一个学生类和书包类&#xff0c;在学生类中有引用类型的书包变量&#xff1a; class SchoolBag {private String brand; //书包的品牌private int size; //书…

使用Vue+vue-router+路由守卫实现路由鉴权功能实战

目录 一、本节介绍和上节回顾 1. 上节介绍 2. Vue + SpringBoot前后端分离项目实战的目录