本教程属于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

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐