React+Antd+Vite+TypeScript 项目实战教程(一)

news2024/11/15 9:17:50

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

一、创建项目

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

  1. 初始化命令
npm init vite
//或者
yarn create vite
  1. 项目名称
    在这里插入图片描述
  2. 选择框架
    只支持Vue3
    在这里插入图片描述
  3. 选择语言
    在这里插入图片描述
  4. 安装依赖并启动
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
  1. 创建assets/styles/index.scss文件
$red:red;
  1. 引入index.scss文件
//打开vite.config.ts,添加scss的预编译选项
export default defineConfig({
  ...
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "./assets/styles/index.scss";`
      },
    }
  }
})
  1. 调用
//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/510436.html

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

相关文章

QT界面开发杂记(五)

QString转char* QString("name").toStdString().c_str() c_str()没有‘\0’结尾可能导致一些错误可以使用以下方法解决&#xff1a; QString xmlPath "path"; const char cXmlName[1024] {0}&#xff1b; memcpy((void*)cXmlName,xmlPath.toStdStri…

目标检测 pytorch复现CenterNet目标检测项目

目标检测 pytorch复现CenterNet目标检测项目 1、项目创新点2、CenterNet网络结构3、CenterNet的模型计算流程如下&#xff1a;4、详细实现原理4.1、heatmap(热力图)理解和生成4.1.1 heatmap生成4.1.2 heatmap高斯函数半径的确定 4.1.3 CenterNet中生成高斯核的部分代码进行解析…

关于hashmap,希望能够帮到你

文章目录 前言介绍hashmap前先说一下关于的map知识 一、Map的概念和场景1.map的概念2.模型1. 纯 key 模型2. Key-Value 模型 二、Map的使用1.关于Map的使用2. 关于Map.Entry<K, V>的说明3. Map 的常用方法说明 三.hashmap1.方法构造2 冲突-概念3. 冲突-避免-哈希函数设计…

深入学习MYSQL-数据检索

前言 由于大部分基础知识都已经学过了&#xff0c;这里只把觉得应该记录一下的知识点做个笔记。然后以下笔记和sql均来自书籍(MYSQL必会知识) LIKE模糊查询 通配符% 相当于是查询一jet开头后面任意的数据 select prod_id,prod_name from products where prod_name like jet…

GRPC 程序在 Kubernetes 中的负载均衡

本文的背景使用的是 kratos 框架。 背景 众所周知 grpc 底层使用 http2 协议&#xff0c;而 http2 是一个长链接多路复用的。在正常情况下客服端与服务端一对一不会需要负载均衡手段&#xff1b;但是当服务上云之后为了保障服务的可用性所以我们服务端一般是多副本&#xff0…

用chatgpt实现 java导出excel复杂表。

记录一次使用chatgpt解决实际问题的&#xff0c;需求是在页面添加一个订单导出excel的功能&#xff0c;订单编号、订单明细&#xff0c;相同订单编号合并单元格&#xff0c;模板如下 表头表尾不用说&#xff0c; 主要是表格内容部分&#xff0c;左边是订单编号&#xff0c;右边…

clickhouse linux 离线安装

1. 下载离线安装包&#xff0c;下四个包&#xff0c;版本号要一致, (在此下的是20.8.3.18-1.el7.x86_64版) clickhouse-server, clickhouse-client, clickhouse-common-static, clickhouse-server-common Altinity/clickhouse - Packages packagecloudBrowse pa…

人工智能基础部分15-自然语言处理中的数据处理上采样、下采样、负采样是什么?

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下人工智能基础部分15-自然语言处理中的数据处理上采样、下采样、负采样是什么&#xff1f;在自然语言处理中&#xff0c;上采样、下采样、负采样都是用于处理数据不平衡问题的技术&#xff0c;目的是为了优化模型的训…

《记录》chariles配置

Charles配置 1、下载&#xff1a;官网下载-傻瓜式安装 2、windows配置解析pc端的https包 http包解析是配好的&#xff0c;需要自行配置https 2.1、下载证书 1、如图点击 之后下一步点完成就行。 2.2、代理设置 这里勾选其实勾不勾都行&#xff0c;默认不改也行。我是看了…

DC域控服务器与辅助DC域控服务器之间的数据同步以及创建域组织机构和域用户

本篇主要是处理DC域控服务器与辅助DC域控服务器之间的数据同步关系&#xff0c;DC域控服务器与辅助DC域控服务器的创建可以参考上篇文章 验证DC域控服务器与辅助DC域控服务器之间的数据同步关系&#xff0c;分别在辅助DC域控服务器DC上面新建一个用户&#xff0c;看看再对应的…

一名【合格】前端工程师的自检清单

1.JavaScript规定了几种语言类型&#xff1f; 基本数据类型&#xff1a;number、string、boolean、null、undefined、symbol(es6) 对象引用类型&#xff1a;Array、Function、Object、RegExp、Error、Date 2.JavaScript对象的底层数据结构是什么&#xff1f; JavaScript 对象…

甄云科技对话格瑞德,探讨高复杂度采购事业的数“智”解决之道

在由甄云科技主办的客户高层访谈节目“甄知访谈”中&#xff0c;本期我们走进山东格瑞德集团&#xff0c;一起来分享格瑞德的采购数字化转型之路。由甄云科技总裁姚一鸣对话格瑞德集团有限公司供应链总经理徐涵先生。 山东格瑞德集团成立于 1993 年&#xff0c;是一家围绕人工…

[pgrx开发postgresql数据库扩展]7.返回序列的函数编写(2)表序列

前文再续&#xff0c;书接上一回。 上一节我们简单说了利用SetOfIterator返回一个srf&#xff08;Set Returning Functions&#xff09;&#xff0c;但是很多情况下&#xff0c;一个单值序列并不能很好的满足我们的需求&#xff0c;所以今天我们来说另外一个作用更广泛的srf&a…

您有一条群邀请消息:施耐德电气“绿色智能制造创赢计划”第四季正式启动!

4月26日&#xff0c;由工业和信息化部国际经济技术合作中心与施耐德电气共同主办的“2023绿色智能制造创赢计划”第四季正式启动 。 这一计划为拥有**技术专长与发展潜力的中小企业提供联合共创平台&#xff0c;帮助企业加速突破工业场景中的关键痛点&#xff0c;孵化和落地更…

GDB调试-从安装到使用

1、GDB简介 gdb 工具是 GNU 项目调试器&#xff0c;基于命令行。和其他的调试器一样&#xff0c;我们可以使用 gdb 来一行行的运行程序、单步执行、跳入/跳出函数、设置断点、查看变量等等&#xff0c;它是 UNIX/LINUX 操作系统下强大的程序调试工具。对于一般的Linux桌面系统(…

Nginx:worker_processes、worker_connections设置

转自&#xff1a;Nginx&#xff1a;worker_processes、worker_connections设置_worker_connections设置多少_it_zhenxiaobai的博客-CSDN博客 worker_processes与worker_connections 设置好合适大小&#xff0c;可以提升 nginx 处理性能&#xff0c;非常重要。 原作者的话&…

【PSO-LSTM】基于PSO优化LSTM网络的电力负荷预测(Python代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

eSIM证书要求-涉及规范SGP.22-SGP.26-2

subjectPublicKeyInfo 证书链中所有证书的subjectPublicKeyInfo中的OID都是一样的 CRL Distribution Point 证书吊销列表分发点 (CRL Distribution Point &#xff0c;简称 CDP) 是含在数字证书中的一个可以共各种应用软件自动下载的最新的 CRL 的位置信息。一个 CDP 通常出现…

egg.js + mysql + windows 踩坑全纪录

资料&#xff1a; egg.js文档&#xff08;https://www.eggjs.org/zh-CN/intro/quickstart&#xff09; 背景&#xff1a;前面的都很简单&#xff0c;按照官方文档配置即可&#xff0c;全部调通以后&#xff0c;开始接触数据库mysql 因为米有后台开发背景&#xff0c;所以需要从…

从IDC数据库安全报告,看OceanBase安全能力

欢迎访问 OceanBase 官网获取更多信息&#xff1a;https://www.oceanbase.com/ 作为数据的承载工具&#xff0c;数据库自身安全能力对于数据安全至关重要。数据库软件诞生至今&#xff0c;经过了几十年的发展和演进&#xff0c;已经成为 IT 系统中不可或缺的关键技术。但是随着…