【React】入门Day04 —— 项目搭建及登录与表单校验、token 管理、路由鉴权实现

项目搭建

创建项目

javascript 复制代码
# 使用npx创建项目
npx create-react-app my-react-app
# 进入项目目录
cd my-react-app
# 创建项目目录结构
mkdir -p src/{apis,assets,components,pages,store,utils}
touch src/{App.js,index.css,index.js}
  • 使用npx create-react-app创建项目,进入项目目录后通过npm start启动。
  • 调整项目目录结构,包括apisassetscomponentspages等多个文件夹。

使用技术

  • 接入scss预处理器,安装sass工具,创建全局样式文件index.scss

    javascript 复制代码
    # 安装sass工具
    npm i sass -D
    javascript 复制代码
    // 在src/index.scss中设置全局样式
    body {
      font-family: Arial, sans-serif;
      background-color: #f4f4f4;
    }
  • 引入组件库antd,安装后在Login页面测试Button组件。

    javascript 复制代码
    # 安装antd组件库
    npm i antd
    javascript 复制代码
    // 在src/pages/Login/index.jsx中使用Button组件
    import React from 'react';
    import { Button } from 'antd';
    
    const Login = () => {
      return (
        <div>
          <Button type='primary'>登录</Button>
        </div>
      );
    };
    
    export default Login;
  • 使用react-router-dom配置基础路由,创建LayoutLogin组件并配置路由规则。

    javascript 复制代码
    # 安装react-router-dom
    npm i react-router-dom
    javascript 复制代码
    // 在src/router/index.js中配置路由
    import { createBrowserRouter } from 'react-router-dom';
    import Login from '../pages/Login';
    import Layout from '../pages/Layout';
    
    const router = createBrowserRouter([
      {
        path: '/',
        element: <Layout />,
      },
      {
        path: '/login',
        element: <Login />,
      },
    ]);
    
    export default router;
  • 通过craco工具包配置别名路径,在craco.config.js中设置webpack别名,并在jsconfig.json中配置VsCode提示。

    javascript 复制代码
    # 安装craco工具包
    npm i @craco/craco -D
    javascript 复制代码
    // 在craco.config.js中配置别名
    const path = require('path');
    module.exports = {
      webpack: {
        alias: {
          '@': path.resolve(__dirname,'src')
        }
      }
    };
    javascript 复制代码
    // 在package.json中修改scripts命令
    "scripts": {
      "start": "craco start",
      "build": "craco build",
      "test": "craco test",
      "eject": "react-scripts eject"
    }
    javascript 复制代码
    // 在src/router/index.js中使用别名
    import { createBrowserRouter } from 'react-router-dom';
    import Login from '@/pages/Login';
    import Layout from '@/pages/Layout';
    
    const router = createBrowserRouter([
      {
        path: '/',
        element: <Layout />,
      },
      {
        path: '/login',
        element: <Login />,
      },
    ]);
    
    export default router;
    javascript 复制代码
    // 在jsconfig.json中配置VsCode提示
    {
      "compilerOptions": {
        "baseUrl": "./",
        "paths": {
          "@/*": ["src/*"]
        }
      }
    }

功能模块实现

### **登录模块**

*

  #### **基本结构搭建**
  • Login/index.js创建登录页面结构,引入antd组件,使用@/assets路径引入图片,在Login/index.scss中设置样式。
javascript 复制代码
import React from 'react';
import { Card, Form, Input, Button } from 'antd';
import logo from '@/assets/logo.png';
import './index.scss';

const Login = () => {
  return (
    <div className="login">
      <Card className="login-container">
        <img className="login-logo" src={logo} alt="" />
        <Form>
          <Form.Item>
            <Input size="large" placeholder="请输入手机号" />
          </Form.Item>
          <Form.Item>
            <Input size="large" placeholder="请输入验证码" />
          </Form.Item>
          <Form.Item>
            <Button type="primary" htmlType="submit" size="large" block>
              登录
            </Button>
          </Form.Item>
        </Form>
      </Card>
    </div>
  );
};

export default Login;

表单校验实现

  • Form组件设置validateTrigger,为Form.Item组件设置namerules属性进行表单校验。

    javascript 复制代码
    import React from 'react';
    import { Form, Input, Button } from 'antd';
    
    const Login = () => {
      return (
        <Form validateTrigger={['onBlur']}>
          <Form.Item
            name="mobile"
            rules={[
              { required: true, message: '请输入手机号' },
              {
                pattern: /^1[3-9]\d{9}$/,
                message: '手机号码格式不对'
              }
            ]}
          >
            <Input size="large" placeholder="请输入手机号" />
          </Form.Item>
          <Form.Item
            name="code"
            rules={[
              { required: true, message: '请输入验证码' },
            ]}
          >
            <Input size="large" placeholder="请输入验证码" maxLength={6} />
          </Form.Item>
          <Form.Item>
            <Button type="primary" htmlType="submit" size="large" block>
              登录
            </Button>
          </Form.Item>
        </Form>
      );
    };
    
    export default Login;

获取登录表单数据

  • Form组件设置onFinish属性,在点击登录按钮时触发获取表单数据的函数。

    javascript 复制代码
    import React from 'react';
    import { Form, Input, Button } from 'antd';
    
    const Login = () => {
      const onFinish = formValue => {
        console.log(formValue);
      };
    
      return (
        <Form onFinish={onFinish}>
          <Form.Item>
            <Input size="large" placeholder="请输入手机号" />
          </Form.Item>
          <Form.Item>
            <Input size="large" placeholder="请输入验证码" />
          </Form.Item>
          <Form.Item>
            <Button type="primary" htmlType="submit" size="large" block>
              登录
            </Button>
          </Form.Item>
        </Form>
      );
    };
    
    export default Login;

封装 request 工具模块

  • 安装axios,在utils/request.js中创建axios实例,配置baseURL、请求拦截器和响应拦截器。

    javascript 复制代码
    # 安装axios
    npm i axios
    javascript 复制代码
    import axios from 'axios';
    
    const http = axios.create({
      baseURL: 'http://example.com/api',
      timeout: 5000
    });
    
    // 请求拦截器
    http.interceptors.request.use(config => {
      return config;
    }, error => {
      return Promise.reject(error);
    });
    
    // 响应拦截器
    http.interceptors.response.use(response => {
      return response.data;
    }, error => {
      return Promise.reject(error);
    });
    
    export { http };

使用 Redux 管理 token

  • 安装react-redux@reduxjs/toolkit,在store中创建userStore切片,设置token初始状态和setUserInforeducers,封装fetchLogin异步方法。

    javascript 复制代码
    # 安装react-redux和@reduxjs/toolkit
    npm i react-redux @reduxjs/toolkit
    javascript 复制代码
    import { createSlice } from '@reduxjs/toolkit';
    import { http } from '@/utils';
    
    const userStore = createSlice({
      name: 'user',
      initialState: {
        token: ''
      },
      reducers: {
        setUserInfo(state, action) {
          state.token = action.payload;
        }
      }
    });
    
    const { setUserInfo } = userStore.actions;
    const userReducer = userStore.reducer;
    
    const fetchLogin = loginForm => {
      return async dispatch => {
        const res = await http.post('/authorizations', loginForm);
        dispatch(setUserInfo(res.data.token));
      };
    };
    
    export { fetchLogin };
    export default userReducer;

实现登录逻辑

  • Login组件中调用fetchLogin方法,登录成功后跳转到首页并提示。

    javascript 复制代码
    import React from 'react';
    import { message } from 'antd';
    import { useDispatch } from 'react-redux';
    import { fetchLogin } from '@/store/modules/user';
    
    const Login = () => {
      const dispatch = useDispatch();
    
      const onFinish = async formValue => {
        await dispatch(fetchLogin(formValue));
        message.success('登录成功');
      };
    
      return (
        <div>
          <form onSubmit={onFinish}>
            {/* 登录表单字段 */}
          </form>
        </div>
      );
    };
    
    export default Login;

token 持久化

  • 封装setTokengetTokenclearToken方法,在userStoresetUserInfo时将token存入本地。

    javascript 复制代码
    // 在@/utils/token.js中封装存取方法
    const TOKENKEY = 'token_key';
    
    function setToken(token) {
      return localStorage.setItem(TOKENKEY, token);
    }
    
    function getToken() {
      return localStorage.getItem(TOKENKEY);
    }
    
    function clearToken() {
      return localStorage.removeItem(TOKENKEY);
    }
    
    export {
      setToken,
      getToken,
      clearToken
    };
    javascript 复制代码
    // 在userStore中使用token持久化方法
    import { createSlice } from '@reduxjs/toolkit';
    import { http } from '@/utils';
    import { getToken, setToken } from '@/utils/token';
    
    const userStore = createSlice({
      name: 'user',
      initialState: {
        token: getToken() || ''
      },
      reducers: {
        setUserInfo(state, action) {
          state.token = action.payload;
          setToken(state.token);
        }
      }
    });
    
    export default userStore;

请求拦截器注入 token

  • request.js的请求拦截器中,判断是否有token,有则添加到请求头Authorization中。

    javascript 复制代码
    // 在utils/request.js中注入token
    import axios from 'axios';
    
    const http = axios.create({
      baseURL: 'http://example.com/api',
      timeout: 5000
    });
    
    http.interceptors.request.use(config => {
      const token = getToken();
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    }, error => {
      return Promise.reject(error);
    });
    
    http.interceptors.response.use(response => {
      return response.data;
    }, error => {
      return Promise.reject(error);
    });
    
    export { http };

路由鉴权实现

  • components/AuthRoute/index.jsx中创建路由鉴权高阶组件,判断本地是否有token,决定是否重定向到登录页面。

    javascript 复制代码
    import React from 'react';
    import { Navigate } from 'react-router-dom';
    import { getToken } from '@/utils';
    
    const AuthRoute = ({ children }) => {
      const isToken = getToken();
      if (isToken) {
        return <>{children}</>;
      } else {
        return <Navigate to="/login" replace />;
      }
    };
    
    export default AuthRoute;
    javascript 复制代码
    // 在src/router/index.js中使用AuthRoute组件
    import { createBrowserRouter } from 'react-router-dom';
    import Login from '@/pages/Login';
    import Layout from '@/pages/Layout';
    import AuthRoute from '@/components/AuthRoute';
    
    const router = createBrowserRouter([
      {
        path: '/',
        element: <AuthRoute><Layout /></AuthRoute>,
      },
      {
        path: '/login',
        element: <Login />,
      },
    ]);
    
    export default router;

Layout 模块

#### **基本结构和样式 reset**

* 在`pages/Layout/index.js`中使用`antd/Layout`组件创建页面结构,引入`antd`的`Menu`和`Popconfirm`等组件,设置样式并安装`normalize.css`进行样式 reset。

  ```javascript
  import React from 'react';
  import { Layout, Menu, Popconfirm } from 'antd';
  import { HomeOutlined, DiffOutlined, EditOutlined, LogoutOutlined } from '@ant-design/icons';
  import './index.scss';
  import 'normalize.css';

  const { Header, Sider } = Layout;

  const items = [
    {
      label: '首页',
      key: '1',
      icon: <HomeOutlined />,
    },
    {
      label: '文章管理',
      key: '2',
      icon: <DiffOutlined />,
    },
    {
      label: '创建文章',
      key: '3',
      icon: <EditOutlined />,
    },
  ];

  const GeekLayout = () => {
    return (
      <Layout>
        <Header className="header">
          <div className="logo" />
          <div className="user-info">
            <span className="user-name">用户名</span>
            <span className="user-logout">
              <Popconfirm title="是否确认退出?" okText="退出" cancelText="取消">
                <LogoutOutlined /> 退出
              </Popconfirm>
            </span>
          </div>
        </Header>
        <Layout>
          <Sider width={200} className="site-layout-background">
            <Menu
              mode="inline"
              theme="dark"
              defaultSelectedKeys={['1']}
              items={items}
              style={{ height: '100%', borderRight: 0 }}
            ></Menu>
          </Sider>
          <Layout className="layout-content" style={{ padding: 20 }}>
            内容
          </Layout>
        </Layout>
      </Layout>
    );
  };

  export default GeekLayout;
  ```
#### **二级路由配置**

* 在`pages`目录创建`Home`、`Article`、`Publish`页面文件夹,在`router/index.js`中配置嵌套子路由,在`Layout`中配置二级路由出口,使用`Link`修改左侧菜单内容实现路由切换。

  ```javascript
  // 在pages目录创建Home.jsx
  import React from 'react';

  const Home = () => {
    return <div>首页内容</div>;
  };

  export default Home;
  ```

  ```javascript
  // 在pages目录创建Article.jsx
  import React from 'react';

  const Article = () => {
    return <div>文章管理内容</div>;
  };

  export default Article;
  ```

  ```javascript
  // 在pages目录创建Publish.jsx
  import React from 'react';

  const Publish = () => {
    return <div>发布文章内容</div>;
  };

  export default Publish;
  ```

  ```javascript
  // 在src/router/index.js中配置二级路由
  import { createBrowserRouter } from 'react-router-dom';
  import Login from '@/pages/Login';
  import Layout from '@/pages/Layout';
  import Publish from '@/pages/Publish';
  import Article from '@/pages/Article';
  import Home from '@/pages/Home';
  import { AuthRoute } from '@/components/AuthRoute';

  const router = createBrowserRouter([
    {
      path: '/',
      element: (
        <AuthRoute>
          <Layout />
        </AuthRoute>
      ),
      children: [
        {
          index: true,
          element: <Home />,
        },
        {
          path: 'article',
          element: <Article />,
        },
        {
          path: 'publish',
          element: <Publish />,
        },
      ],
    },
    {
      path: '/login',
      element: <Login />,
    },
  ]);

  export default router;
  ```

  ```javascript
  // 在Layout组件中配置二级路由出口
  import React from 'react';
  import { Outlet } from 'react-router-dom';

  const GeekLayout = () => {
    return (
      <Layout className="layout-content" style={{ padding: 20 }}>
        <Outlet />
      </Layout>
    );
  };

  export default GeekLayout;
  ```
#### **路由菜单点击交互实现**

* 为`Menu`组件设置`onClick`属性实现点击菜单跳转路由,通过`useLocation`获取当前路由路径实现菜单反向高亮。

  ```javascript
  import React from 'react';
  import { Outlet, useNavigate } from 'react-router-dom';
  import { HomeOutlined, DiffOutlined, EditOutlined, LogoutOutlined } from '@ant-design/icons';

  const items = [
    {
      label: '首页',
      key: '/',
      icon: <HomeOutlined />,
    },
    {
      label: '文章管理',
      key: '/article',
      icon: <DiffOutlined />,
    },
    {
      label: '创建文章',
      key: '/publish',
      icon: <EditOutlined />,
    },
  ];

  const GeekLayout = () => {
    const navigate = useNavigate();

    const menuClick = route => {
      navigate(route.key);
    };

    return (
      <Layout>
        <Header className="main-header">
          <div className="logo" />
          <div className="user-info">
            <span className="user-name">用户名</span>
            <span className="user-logout">
              <Popconfirm title="是否确认退出?" okText="退出" cancelText="取消">
                <LogoutOutlined /> 退出
              </Popconfirm>
            </span>
          </div>
        </Header>
        <Layout>
          <Sider width={200} className="site-layout-background">
            <Menu
              mode="inline"
              theme="dark"
              selectedKeys={['1']}
              items={items}
              style={{ height: '100%', borderRight: 0 }}
              onClick={menuClick}
            ></Menu>
          </Sider>
          <Layout className="layout-content" style={{ padding: 20 }}>
            <Outlet />
          </Layout>
        </Layout>
      );
  };

  export default GeekLayout;
  ```

  ```javascript
  // 菜单反向高亮实现
  import React from 'react';
  import { Outlet, useLocation } from 'react-router-dom';
  import { HomeOutlined, DiffOutlined, EditOutlined, LogoutOutlined } from '@ant-design/icons';

  const items = [
    {
      label: '首页',
      key: '/',
      icon: <HomeOutlined />,
    },
    {
      label: '文章管理',
      key: '/article',
      icon: <DiffOutlined />,
    },
    {
      label: '创建文章',
      key: '/publish',
      icon: <EditOutlined />,
    },
  ];

  const GeekLayout = () => {
    const location = useLocation();
    const selectedKey = location.pathname;

    return (
      <Layout>
        <Header className="main-header">
          <div className
  ```
#### **展示个人信息**

* 在`store/userStore.js`中编写获取用户信息的逻辑,在`Layout`组件中触发`fetchUserInfo`方法获取信息并渲染用户名。

  ```javascript
  // store/userStore.js
  import { createSlice } from '@reduxjs/toolkit';
  import { http } from '@/utils';
  import { getToken, setToken } from '@/utils';

  const userStore = createSlice({
    name: 'user',
    initialState: {
      token: getToken() || '',
      userInfo: {}
    },
    reducers: {
      setUserToken(state, action) {
        state.token = action.payload;
        setToken(state.token);
      },
      setUserInfo(state, action) {
        state.userInfo = action.payload;
      },
      clearUserInfo(state) {
        state.token = '';
        state.userInfo = {};
        clearToken();
      }
    }
  });

  // 解构出actionCreater
  const { setUserToken, setUserInfo, clearUserInfo } = userStore.actions;
  // 获取reducer函数
  const userReducer = userStore.reducer;

  const fetchLogin = (loginForm) => {
    return async (dispatch) => {
      const res = await http.post('/authorizations', loginForm);
      dispatch(setUserToken(res.data.token));
    };
  };

  const fetchUserInfo = () => {
    return async (dispatch) => {
      const res = await http.get('/user/profile');
      dispatch(setUserInfo(res.data));
    };
  };

  export { fetchLogin, fetchUserInfo, clearUserInfo };
  export default userReducer;
  ```
#### **退出登录实现**

* 为`Popconfirm`添加确认回调事件,在`store/userStore.js`中新增`clearUserInfo`方法删除`token`和用户信息,在回调事件中调用该方法并返回登录页面。

  ```javascript
  // pages/Layout/index.js
  import React, { useEffect } from 'react';
  import { Layout, Menu, Popconfirm } from 'antd';
  import {
    HomeOutlined,
    DiffOutlined,
    EditOutlined,
    LogoutOutlined,
  } from '@ant-design/icons';
  import { useDispatch, useSelector } from 'react-redux';
  import { fetchUserInfo } from '@/store/modules/user';

  const { Header, Sider } = Layout;

  const items = [
    // 菜单配置项
  ];

  const GeekLayout = () => {
    const dispatch = useDispatch();
    const name = useSelector(state => state.user.userInfo.name);

    useEffect(() => {
      dispatch(fetchUserInfo());
    }, [dispatch]);

    const loginOut = () => {
      dispatch(clearUserInfo());
      // 假设这里有合适的导航函数,替换为实际的导航逻辑
      // navigate('/login'); 
    };

    return (
      <Layout>
        <Header className="header">
          <div className="logo" />
          <div className="user-info">
            <span className="user-name">{name}</span>
            <span className="user-logout">
              <Popconfirm
                title="是否确认退出?"
                okText="退出"
                cancelText="取消"
                onConfirm={loginOut}
              >
                <LogoutOutlined /> 退出
              </Popconfirm>
            </span>
          </div>
        </Header>
        <Layout>
          <Sider width={200} className="site-layout-background">
            <Menu
              mode="inline"
              theme="dark"
              defaultSelectedKeys={['1']}
              items={items}
              style={{ height: '100%', borderRight: 0 }}
            ></Menu>
          </Sider>
          <Layout className="layout-content" style={{ padding: 20 }}>
            {/* 页面内容 */}
          </Layout>
        </Layout>
      </Layout>
    );
  };

  export default GeekLayout;
  ```
#### **处理 Token 失效**

* 在`http.interceptors.response`中判断响应状态码为`401`时,清除`token`,跳转到登录页面并刷新页面。

  ```javascript
  // 在http.js(假设是配置axios请求相关的文件)中处理Token失效
  import axios from 'axios';

  const http = axios.create({
    baseURL: 'http://example.com/api',
    timeout: 5000
  });

  http.interceptors.response.use((response) => {
    return response.data;
  }, (error) => {
    if (error.response && error.response.status === 401) {
      // 假设这里有合适的获取和清除token的函数,替换为实际的逻辑
      const token = getToken();
      if (token) {
        clearToken();
      }
      // 假设这里有合适的导航函数,替换为实际的导航逻辑
      // navigate('/login'); 
      window.location.reload();
    }
    return Promise.reject(error);
  });

  export { http };
  ```
相关推荐
2401_857297914 分钟前
招联金融2025秋招倒计时
java·前端·算法·金融·求职招聘
Xerale16 分钟前
Laravel Admin 中的 “Array to String Conversion“ 问题及其解决方法
前端·数据库·笔记·php·laravel
温、33 分钟前
【音频可视化】通过canvas绘制音频波形图
前端·音视频
王可夫1 小时前
JavaScript基础---typeof和instanceof的区别
开发语言·前端·javascript
我的运维人生1 小时前
Apache服务器深度解析与实践应用:构建高效Web服务的基石
服务器·前端·apache·运维开发·技术共享
合合技术团队1 小时前
OCR+PDF解析配套前端工具开源详解!
前端·深度学习·pdf·ocr
霍金的微笑1 小时前
JAVA Web(学习笔记)
java·前端·学习
7_35Durant1 小时前
vue-自定义指令
前端·javascript·vue.js
gurenchang1 小时前
React切换Tab栏并进行锚点滚动
前端·react.js·前端框架