【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 };
  ```
相关推荐
掘金者阿豪1 小时前
把业务数据变成共享仪表盘:Metabase可视化与远程访问实践
前端·后端
kyriewen1 小时前
折腾了半年 AI 编程工作流,最后发现效率瓶颈是桌上那块屏幕
前端·javascript·ai编程
蜗牛前端2 小时前
codex 全流程开发上线的高颜值礼簿小程序
前端·微信小程序
大龄秃头程序员2 小时前
我在图文流 App 里落地双层缓存、弱网降级与 OOM 治理
前端
老王以为2 小时前
React Renderer 分离的多平台架构
前端·react native·react.js
hunterandroid2 小时前
Kotlin Coroutines 与 Flow:让异步任务更清晰
前端
Bigger3 小时前
从零搭建 AI 代码审查服务:一份前端也能看懂的 Python 学习笔记
前端·ci/cd·ai编程
lichenyang4533 小时前
JSAPI、NAPI、Biz、Imp:ASCF Demo 如何真正调用系统能力和 C++ 能力
前端
lichenyang4534 小时前
IPC、JSVM、UIThread、libuv:ASCF 架构图里最容易混的几个词
前端
用户059540174464 小时前
Redis记忆存储故障恢复测试踩坑实录:手动测试让我漏掉了2个一致性Bug
前端·css