• 前端实战|React18极客园——登陆模块(token持久化、路由拦截、mobx、封装axios)


    在这里插入图片描述

    欢迎来到我的博客
    📔博主是一名大学在读本科生,主要学习方向是前端。
    🍭目前已经更新了【Vue】、【React–从基础到实战】、【TypeScript】等等系列专栏
    🛠目前正在学习的是🔥 R e a c t 框架 React框架 React框架🔥,中间穿插了一些基础知识的回顾
    🌈博客主页👉codeMak1r.小新的博客

    本文被专栏【React–从基础到实战】收录
    🕹坚持创作✏️,一起学习📖,码出未来👨🏻‍💻!

    最近在学习React过程中,找到了一个实战小项目,在这里与大家分享。
    本文遵循项目开发流程,逐步完善各个需求
    前文——《项目前置准备》

    登陆模块

    1.基本结构模块

    本节目标: 能够使用antd搭建基础布局

    实现步骤

    1. 在 Login/index.js 中创建登录页面基本结构
    2. 在 Login 目录中创建 index.scss 文件,指定组件样式
    3. 将 logo.png 和 login.png 拷贝到 assets 目录中

    代码实现

    pages/Login/index.js

    import { Card } 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="" />
            {/* 登录表单 */}
          </Card>
        </div>
      )
    }
    
    export default Login
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    pages/Login/index.scss

    .login {
      width: 100%;
      height: 100%;
      position: absolute;
      left: 0;
      top: 0;
      background: center/cover url('~@/assets/login.png');
      
      .login-logo {
        width: 200px;
        height: 60px;
        display: block;
        margin: 0 auto 20px;
      }
      
      .login-container {
        width: 440px;
        height: 360px;
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
        box-shadow: 0 0 50px rgb(0 0 0 / 10%);
      }
      
      .login-checkbox-label {
        color: #1890ff;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    2. 创建表单结构

    本节目标: 能够使用antd的Form组件创建登录表单

    实现步骤

    1. 打开 antd Form 组件文档
    2. 找到代码演示的第一个示例(基本使用),点击<>(显示代码),并拷贝代码到组件中
    3. 分析 Form 组件基本结构
    4. 调整 Form 组件结构和样式

    代码实现

    pages/Login/index.js

    import { Form, Input, Button, Checkbox } from 'antd'
    const Login = () => {
      return (
        <Form>
          <Form.Item>
            <Input size="large" placeholder="请输入手机号" />
          </Form.Item>
          <Form.Item>
            <Input size="large" placeholder="请输入验证码" />
          </Form.Item>
          <Form.Item>
            <Checkbox className="login-checkbox-label">
              我已阅读并同意「用户协议」和「隐私条款」
            </Checkbox>
          </Form.Item>
    
          <Form.Item>
            <!-- 渲染Button组件为submit按钮 -->
            <Button type="primary" htmlType="submit" size="large" block>
              登录
            </Button>
          </Form.Item>
        </Form>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    3. 表单校验实现

    本节目标: 能够为手机号和密码添加表单校验

    实现步骤

    1. 为 Form 组件添加 validateTrigger 属性,指定校验触发时机的集合
    2. 为 Form.Item 组件添加 name 属性,这样表单校验才会生效
    3. 为 Form.Item 组件添加 rules 属性,用来添加表单校验

    代码实现

    page/Login/index.js

    const Login = () => {
      return (
        <Form validateTrigger={['onBlur', 'onChange']}>
          <Form.Item
            name="mobile"
            rules={[
              {
                pattern: /^1[3-9]\d{9}$/,
                message: '手机号码格式不对',
                validateTrigger: 'onBlur'
              },
              { required: true, message: '请输入手机号' }
            ]}
          >
            <Input size="large" placeholder="请输入手机号" />
          </Form.Item>
          <Form.Item
            name="code"
            rules={[
              { len: 6, message: '验证码6个字符', validateTrigger: 'onBlur' },
              { required: true, message: '请输入验证码' }
            ]}
          >
            <Input size="large" placeholder="请输入验证码" maxLength={6} />
          </Form.Item>
          <Form.Item name="remember" valuePropName="checked">
            <Checkbox className="login-checkbox-label">
              我已阅读并同意「用户协议」和「隐私条款」
            </Checkbox>
          </Form.Item>
    
          <Form.Item>
            <Button type="primary" htmlType="submit" size="large" block>
              登录
            </Button>
          </Form.Item>
        </Form>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    4. 获取登录表单数据

    本节目标: 能够拿到登录表单中用户的手机号码和验证码

    实现步骤

    1. 为 Form 组件添加 onFinish 属性,该事件会在点击登录按钮时触发
    2. 创建 onFinish 函数,通过函数参数 values 拿到表单值
    3. Form 组件添加 initialValues 属性,来初始化表单值

    代码实现

    pages/Login/index.js

    // 点击登录按钮时触发 参数values即是表单输入数据
    const onFinish = values => {
      console.log(values)
    }
    
    <Form
      onFinish={ onFinish }
      initialValues={{
        mobile: '13911111111',
        code: '246810',
        remember: true
      }}
    >...</Form>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    5. 封装http工具模块

    本节目标: 封装axios,简化操作

    实现步骤

    1. 创建 utils/http.js 文件
    2. 创建 axios 实例,配置 baseURL,请求拦截器,响应拦截器
    3. 在 utils/index.js 中,统一导出 http

    代码实现

    utils/http.js

    import axios from 'axios'
    
    const http = axios.create({
      baseURL: 'http://geek.itheima.net/v1_0',
      timeout: 5000
    })
    // 添加请求拦截器
    http.interceptors.request.use((config)=> {
        return config
      }, (error)=> {
        return Promise.reject(error)
    })
    
    // 添加响应拦截器
    http.interceptors.response.use((response)=> {
        // 2xx 范围内的状态码都会触发该函数。
        // 对响应数据做点什么
        return response
      }, (error)=> {
        // 超出 2xx 范围的状态码都会触发该函数。
        // 对响应错误做点什么
        return Promise.reject(error)
    })
    
    export { http }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    utils/index.js

    import { http } from './http'
    export {  http }
    
    • 1
    • 2

    6. 配置登录Mobx

    本节目标: 基于mobx封装管理用户登录的store

    store/login.Store.js

    // 登录模块
    import { makeAutoObservable } from "mobx"
    import { http } from '@/utils'
    
    class LoginStore {
      token = ''
      constructor() {
        makeAutoObservable(this)
      }
      // 登录
      login = async ({ mobile, code }) => {
        const res = await http.post('http://geek.itheima.net/v1_0/authorizations', {
          mobile,
          code
        })
        this.token = res.data.token
      }
    }
    export default LoginStore
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    store/index.js

    import React from "react"
    import LoginStore from './login.Store'
    
    class RootStore {
      // 组合模块
      constructor() {
        this.loginStore = new LoginStore()
      }
    }
    // 导入useStore方法供组件使用数据
    const StoresContext = React.createContext(new RootStore())
    export const useStore = () => React.useContext(StoresContext)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    7. 实现登录逻辑

    本节目标: 在表单校验通过之后通过封装好的store调用登录接口

    实现步骤

    1. 使用useStore方法得到loginStore实例对象
    2. 在校验通过之后,调用loginStore中的login函数
    3. 登录成功之后跳转到首页

    代码实现

    import { useStore } from '@/store'
    const onFinish = async (values) => {
        // 存储登录成功的token
        try {
          await loginStore.setToken(values)
          navigate('/', { replace: true })
          message.success('At Your Service, Sir!', 2)
        } catch (error) {
          message.error(error.response?.data?.message || '登录失败')
        }
      };
      const onFinishFailed = (errorInfo) => {
        const [name] = errorInfo.errorFields[0].name
        if (name === "captcha") message.error('登录失败,请检查验证码是否有误!', 2);
        if (name === "tel") message.error('登录失败,请检查手机号是否有误!', 2);
      }
      return (...)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    8. token持久化

    封装工具函数

    本节目标: 能够统一处理 token 的持久化相关操作,确保刷新后 token 不丢失。

    实现步骤

    1. 创建 utils/token.js 文件
    2. 分别提供 getToken/setToken/clearToken/isAuth 四个工具函数并导出
    3. 创建 utils/index.js 文件,统一导出 token.js 中的所有内容,来简化工具函数的导入
    4. 将登录操作中用到 token 的地方,替换为该工具函数

    代码实现

    utils/token.js

    const TOKEN_KEY = 'geek_pc'
    
    const getToken = () => localStorage.getItem(TOKEN_KEY)
    const setToken = token => localStorage.setItem(TOKEN_KEY, token)
    const clearToken = () => localStorage.removeItem(TOKEN_KEY)
    
    export { getToken, setToken, clearToken }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    持久化设置

    本节目标: 使用token函数持久化配置

    实现步骤

    1. 拿到token的时候一式两份,存本地一份
    2. 初始化的时候优先从本地取,取不到再初始化为控制

    代码实现

    store/login.Store.js

    // 登录模块
    import { makeAutoObservable } from "mobx"
    import { setToken, getToken, clearToken, http } from '@/utils'
    
    class LoginStore {
      // 这里哦!!
      token = getToken() || ''
      constructor() {
        makeAutoObservable(this)
      }
      // 登录
      login = async ({ mobile, code }) => {
        const res = await http.post('http://geek.itheima.net/v1_0/authorizations', {
          mobile,
          code
        })
        this.token = res.data.token
        // 还有这里哦!!
        setToken(res.data.token)
      }
     
    }
    export default LoginStore
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    9. axios请求拦截器注入token

    《Vue/React项目实现axios请求拦截器注入token》

    本节目标: 把token通过请求拦截器注入到请求头中

    img

    拼接方式:config.headers.Authorization = Bearer ${token}}

    utils/http.js

    http.interceptors.request.use(config => {
      const token = getToken('pc-key')
      if (token) {
        config.headers.Authorization = `Bearer ${token}`
      }
      return config
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    第一次发起请求,是登录请求,此时,localStorage中没有token,getToken获取不到,不走下面这个if函数体,直接return config;

    后面再发请求时,由于已经登录了,此时,localStorage中有token,getToken获取到了,走if中的函数体,在发起请求前自动进行预处理,追加一个token,以便于访问需要权限的页面

    为请求头对象(headers)中添加token验证的自定义字段(Authorization),作用是为了让需要验证才能使用的API能够使用(请求头中携带了token值则可通过验证)

    在最后必须return config

    10. 路由导航守卫

    【Vue/React实现路由鉴权/导航守卫/路由拦截(react-router v6)】

    本节目标: 能够实现未登录时访问拦截并跳转到登录页面(路由鉴权实现)

    实现思路

    自己封装 AuthRoute 路由鉴权高阶组件,实现未登录拦截,并跳转到登录页面

    思路为:判断本地是否有token,如果有,就返回子组件,否则就重定向到登录Login

    实现步骤

    1. 在 components 目录中,创建 AuthRoute/index.js 文件
    2. 判断是否登录
    3. 登录时,直接渲染相应页面组件
    4. 未登录时,重定向到登录页面
    5. 将需要鉴权的页面路由配置,替换为 AuthRoute 组件渲染

    代码实现

    components/AuthRoute/index.js

    // 路由鉴权
    // 1. 判断token是否存在
    // 2. 如果存在 直接正常渲染
    // 3. 如果不存在 重定向到登录路由
    
    import { Navigate } from "react-router-dom";
    import { getToken } from "@/utils";
    // 高阶组件:把一个组件当成另外一个组件的参数传入 然后通过一定的判断 返回新的组件
    // 这里的AuthRoute就是一个高阶组件
    
    function AuthRoute({ children }) {
      // 获取token
      const tokenStr = getToken()
      // 如果token存在 直接正常渲染
      if (tokenStr) {
        return <>{children}</>
      }
      // 如果token不存在,重定向到登录路由
      else {
        return <Navigate to='/login' replace />
      }
    }
    {/*
        
     登录:<>  
     非登录:
    */ }
    export { AuthRoute }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    注:utils工具函数getToken如下

    // 从localstorage中取token
    const getToken = () => {
    return window.localStorage.getItem(key)
    }
    
    • 1
    • 2
    • 3
    • 4

    src/routes/index.js路由表文件

    import Layout from "@/pages/Layout";
    import Login from "@/pages/Login";
    import { AuthRoute } from "@/components/AuthRoute";
    
    // eslint-disable-next-line
    export default [
      // 不需要鉴权的组件Login
      {
        path: "/login",
        element: <Login />
      },
      // 需要鉴权的组件Layout
      {
        path: "/",
        element: <AuthRoute>
          <Layout />
        </AuthRoute>
      }
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    下篇文章:Layout布局模块的实现
    专栏订阅入口【React–从基础到实战】

  • 相关阅读:
    智能合约漏洞,Euler Finance 价值 1.95 亿美元漏洞事件分析
    5 个冷门但非常实用的 Kubectl 使用技巧,99% 的人都不知道
    为什么在springboot中使用thymeleaf无法实现网络请求
    linux系统打补丁
    深入理解Linux系统IO调用接口,文件描述符,语言缓冲区
    v-bind指令:设置元素的属性
    超级计算/先进计算的十大用途
    flink篇——Time和watermark机制
    golang开发 深入理解 context
    全球农业经济论坛 丰收节贸促会-万祥军:产业链解决中国方案
  • 原文地址:https://blog.csdn.net/Svik_zy/article/details/126634019