当前位置: 首页 > news >正文

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

项目搭建

  • 创建项目

    # 使用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
      # 安装sass工具
      npm i sass -D
      // 在src/index.scss中设置全局样式
      body {font-family: Arial, sans-serif;background-color: #f4f4f4;
      }
    • 引入组件库antd,安装后在Login页面测试Button组件。
      # 安装antd组件库
      npm i antd
      // 在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组件并配置路由规则。
      # 安装react-router-dom
      npm i react-router-dom
      // 在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提示。
      # 安装craco工具包
      npm i @craco/craco -D
      // 在craco.config.js中配置别名
      const path = require('path');
      module.exports = {webpack: {alias: {'@': path.resolve(__dirname,'src')}}
      };
      // 在package.json中修改scripts命令
      "scripts": {"start": "craco start","build": "craco build","test": "craco test","eject": "react-scripts eject"
      }
      // 在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;
      // 在jsconfig.json中配置VsCode提示
      {"compilerOptions": {"baseUrl": "./","paths": {"@/*": ["src/*"]}}
      }

  • 功能模块实现

    • 登录模块

      • 基本结构搭建

  •         在Login/index.js创建登录页面结构,引入antd组件,使用@/assets路径引入图片,在Login/index.scss中设置样式。
  • 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属性进行表单校验。
      import React from 'react';
      import { Form, Input, Button } from 'antd';const Login = () => {return (<Form validateTrigger={['onBlur']}><Form.Itemname="mobile"rules={[{ required: true, message: '请输入手机号' },{pattern: /^1[3-9]\d{9}$/,message: '手机号码格式不对'}]}><Input size="large" placeholder="请输入手机号" /></Form.Item><Form.Itemname="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属性,在点击登录按钮时触发获取表单数据的函数。
      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、请求拦截器和响应拦截器。
      # 安装axios
      npm i axios
      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异步方法。
      # 安装react-redux和@reduxjs/toolkit
      npm i react-redux @reduxjs/toolkit
      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方法,登录成功后跳转到首页并提示。
      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存入本地。
      // 在@/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
      };
      // 在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中。
      // 在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,决定是否重定向到登录页面。
      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;
      // 在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组件创建页面结构,引入antdMenuPopconfirm等组件,设置样式并安装normalize.css进行样式 reset。
        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"><Menumode="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目录创建HomeArticlePublish页面文件夹,在router/index.js中配置嵌套子路由,在Layout中配置二级路由出口,使用Link修改左侧菜单内容实现路由切换。
        // 在pages目录创建Home.jsx
        import React from 'react';const Home = () => {return <div>首页内容</div>;
        };export default Home;
        // 在pages目录创建Article.jsx
        import React from 'react';const Article = () => {return <div>文章管理内容</div>;
        };export default Article;
        // 在pages目录创建Publish.jsx
        import React from 'react';const Publish = () => {return <div>发布文章内容</div>;
        };export default Publish;
        // 在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;
        // 在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获取当前路由路径实现菜单反向高亮。
        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"><Menumode="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;
        // 菜单反向高亮实现
        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方法获取信息并渲染用户名。
        // 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和用户信息,在回调事件中调用该方法并返回登录页面。
        // 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"><Popconfirmtitle="是否确认退出?"okText="退出"cancelText="取消"onConfirm={loginOut}><LogoutOutlined /> 退出</Popconfirm></span></div></Header><Layout><Sider width={200} className="site-layout-background"><Menumode="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,跳转到登录页面并刷新页面。
        // 在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 };

http://www.mrgr.cn/news/42568.html

相关文章:

  • 可以提高 Java 代码开发效率的工具类(持续更新)
  • 【2022工业图像异常检测文献】CFLOW-AD: 通过条件归一化流实现实时无监督定位异常检测
  • 深入理解Dubbo源码核心原理-Part3
  • (C语言贪吃蛇)15.贪吃蛇吃食物
  • fiddler抓包17_简单接口测试(Composer请求编辑)
  • fastAPI教程:进阶操作
  • 创建django项目,编译类型选择Custom environment后,却没有manage.py文件,无法启动项目?
  • 古典舞在线互动:SpringBoot平台设计与功能实现
  • 论文翻译 | Language Models are Few-Shot Learners 语言模型是少样本学习者(下)
  • LeetCode题练习与总结:整数转换英文表示--273
  • 知识图谱入门——8:KG开发常见数据格式:OWL、RDF、XML、GraphML、JSON、CSV。
  • 36 指针与 const 的多种搭配:指向常量的指针、常量指针、指向常量的常量指针、指针到指针的常量(涉及双重指针)
  • 插入排序:直接插入排序、希尔排序
  • Java高效编程(15):最小化类与成员的可见性
  • 5G NR coreset 简介
  • (C语言贪吃蛇)16.贪吃蛇食物位置随机(完结撒花)
  • Ubuntu18.04配置OpenPCDet并运行demo过程记录
  • Chromium 硬件加速开关c++
  • Redis: 集群高可用之MOVED转向和ASK转向解决方案
  • [云] DockerCoins 练习笔记