【实战】 JWT、用户认证与异步请求(下) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(五)

news2025/1/13 11:38:08

文章目录

    • 一、项目起航:项目初始化与配置
    • 二、React 与 Hook 应用:实现项目列表
    • 三、TS 应用:JS神助攻 - 强类型
    • 四、JWT、用户认证与异步请求
      • 1~5
      • 6.用useAuth切换登录与非登录状态
      • 7.用fetch抽象通用HTTP请求方法,增强通用性
      • 8.用useHttp管理JWT和登录状态,保持登录状态
      • 9.TS的联合类型、Partial和Omit介绍
      • 10.TS 的 Utility Types-Pick、Exclude、Partial 和 Omit 实现


学习内容来源:React + React Hook + TS 最佳实践-慕课网


相对原教程,我在学习开始时(2023.03)采用的是当前最新版本:

版本
react & react-dom^18.2.0
react-router & react-router-dom^6.11.2
antd^4.24.8
@commitlint/cli & @commitlint/config-conventional^17.4.4
eslint-config-prettier^8.6.0
husky^8.0.3
lint-staged^13.1.2
prettier2.8.4
json-server0.17.2
craco-less^2.0.0
@craco/craco^7.1.0
qs^6.11.0
dayjs^1.11.7
react-helmet^6.1.0
@types/react-helmet^6.1.6
react-query^6.1.0
@welldone-software/why-did-you-render^7.0.1
@emotion/react & @emotion/styled^11.10.6

具体配置、操作和内容会有差异,“坑”也会有所不同。。。


一、项目起航:项目初始化与配置

  • 【实战】 项目起航:项目初始化与配置 —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(一)

二、React 与 Hook 应用:实现项目列表

  • 【实战】 React 与 Hook 应用:实现项目列表 —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(二)

三、TS 应用:JS神助攻 - 强类型

  • 【实战】 TS 应用:JS神助攻 - 强类型 —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(三)

四、JWT、用户认证与异步请求

1~5

  • 【实战】 JWT、用户认证与异步请求(上) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(四)

6.用useAuth切换登录与非登录状态

登录态 页面和 非登录态 页面分别整合(过程稀碎。。):

  • 新建文件夹及下面文件:unauthenticated-app
  • index.tsx
import { useState } from "react";
import { Login } from "./login";
import { Register } from "./register";

export const UnauthenticatedApp = () => {
  const [isRegister, setIsRegister] = useState(false);
  return (
    <div>
      {isRegister ? <Register /> : <Login />}
      <button onClick={() => setIsRegister(!isRegister)}>
        切换到{isRegister ? "登录" : "注册"}
      </button>
    </div>
  );
};
  • login.tsx(把 src\screens\login\index.tsx 剪切并更名)
import { useAuth } from "context/auth-context";
import { FormEvent } from "react";

export const Login = () => {
  const { login, user } = useAuth();
  // HTMLFormElement extends Element (子类型继承性兼容所有父类型)(鸭子类型:duck typing: 面向接口编程 而非 面向对象编程)
  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const username = (event.currentTarget.elements[0] as HTMLFormElement).value;
    const password = (event.currentTarget.elements[1] as HTMLFormElement).value;
    login({ username, password });
  };
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="username">用户名</label>
        <input type="text" id="username" />
      </div>
      <div>
        <label htmlFor="password">密码</label>
        <input type="password" id="password" />
      </div>
      <button type="submit">登录</button>
    </form>
  );
};
  • register.tsx(把 src\screens\login\index.tsx 剪切并更名,代码中 login 相关改为 register
import { useAuth } from "context/auth-context";
import { FormEvent } from "react";

export const Register = () => {
  const { register, user } = useAuth();
  // HTMLFormElement extends Element (子类型继承性兼容所有父类型)(鸭子类型:duck typing: 面向接口编程 而非 面向对象编程)
  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const username = (event.currentTarget.elements[0] as HTMLFormElement).value;
    const password = (event.currentTarget.elements[1] as HTMLFormElement).value;
    register({ username, password });
  };
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="username">用户名</label>
        <input type="text" id="username" />
      </div>
      <div>
        <label htmlFor="password">密码</label>
        <input type="password" id="password" />
      </div>
      <button type="submit">注册</button>
    </form>
  );
};
  • 删掉目录:src\screens\login
  • 新建文件:authenticated-app.tsx
import { useAuth } from "context/auth-context";
import { ProjectList } from "screens/ProjectList";

export const AuthenticatedApp = () => {
  const { logout } = useAuth();
  return (
    <div>
      <button onClick={logout}>登出</button>
      <ProjectList />
    </div>
  );
};
  • 修改 src\App.tsx(根据是否可以获取到 user 信息,决定展示 登录态 还是 非登录态 页面)
import { AuthenticatedApp } from "authenticated-app";
import { useAuth } from "context/auth-context";
import { UnauthenticatedApp } from "unauthenticated-app";
import "./App.css";

function App() {
  const { user } = useAuth();

  return (
    <div className="App">
      {user ? <AuthenticatedApp /> : <UnauthenticatedApp />}
    </div>
  );
}

export default App;

查看页面,尝试功能:

  • 切换登录/注册,正常
  • 登录:login 正常,但是 projectsusers 接口 401A token must be provided
  • F12 控制台查看 __auth_provider_token__ (Application - Storage - Local Storage - http://localhost:3000):

在这里插入图片描述

  • 注册:正常,默认直接登录(同登录,存储 user

7.用fetch抽象通用HTTP请求方法,增强通用性

  • 新建:src\utils\http.ts
import qs from "qs";
import * as auth from 'auth-provider'

const apiUrl = process.env.REACT_APP_API_URL;

interface HttpConfig extends RequestInit {
  data?: object,
  token?: string
}

export const http = async (funcPath: string, { data, token, headers, ...customConfig }: HttpConfig) => {
  const httpConfig = {
    method: 'GET',
    headers: {
      Authorization: token ? `Bearer ${token}` : '',
      'Content-Type': data ? 'application/json' : ''
    },
    ...customConfig
  }

  if (httpConfig.method.toUpperCase() === 'GET') {
    funcPath += `?${qs.stringify(data)}`
  } else {
    httpConfig.body = JSON.stringify(data || {})
  }

  // axios 和 fetch 不同,axios 会在 状态码 不为 2xx 时,自动抛出异常,fetch 需要 手动处理
  return window.fetch(`${apiUrl}/${funcPath}`, httpConfig).then(async res => {
    if (res.status === 401) {
      // 自动退出 并 重载页面
      await auth.logout()
      window.location.reload()
      return Promise.reject({message: '请重新登录!'})
    }
    const data = await res.json()
    if (res.ok) {
      return data
    } else {
      return Promise.reject(data)
    }
  })
}
  • 类型定义思路:按住 Ctrl ,点进 fetch,可见:fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;,因此第二个参数即为 RequestInit 类型,但由于有自定义入参,因此自定义个继承 RequestInit 的类型
  • customConfig 会覆盖前面已有属性
  • 需要手动区别 getpost 不同的携参方式
  • axiosfetch 不同,axios 会在 状态码 不为 2xx 时,自动抛出异常,fetch 需要 手动处理
  • 留心 Authorization (授权)不要写成 Authentication (认证),否则后面会报401,且很难找出问题所在

8.用useHttp管理JWT和登录状态,保持登录状态

  • 为了使请求接口时能够自动携带 token 定义 useHttp: src\utils\http.ts
...
export const http = async (
  funcPath: string,
  { data, token, headers, ...customConfig }: HttpConfig = {} // 参数有 默认值 会自动变为 可选参数
) => {...}
...
export const useHttp = () => {
  const { user } = useAuth()
  // TODO 学习 TS 操作符
  return (...[funcPath, customConfig]: Parameters<typeof http>) => http(funcPath, { ...customConfig, token: user?.token })
}
  • 函数定义时参数设定 默认值,该参数即为 可选参数
  • 参数可以解构赋值后使用 rest 操作符降维,实现多参
  • Parameters 操作符可以将函数入参类型复用
  • src\screens\ProjectList\index.tsx 中使用 useHttp(部分原有代码省略):
...
import { useHttp } from "utils/http";

export const ProjectList = () => {
  ...
  const client = useHttp()

  useEffect(() => {
    // React Hook "useHttp" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.
    client('projects', { data: cleanObject(lastParam)}).then(setList)
    // React Hook useEffect has a missing dependency: 'client'. Either include it or remove the dependency array.
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [lastParam]); 

  useMount(() => client('users').then(setUsers));

  return (...);
};
  • useHttp 不能在 useEffectcallback 中直接使用,否则会报错:React Hook "useHttp" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.,建议如代码中所示使用(client 即 携带 tokenhttp 函数)
  • 依赖中只有 lastParam ,会警告:React Hook useEffect has a missing dependency: 'client'. Either include it or remove the dependency array.,但是添加 client 会无法通过相等检查并导致无限的重新渲染循环。(当前代码中最优解是添加 eslint 注释,其他可参考但不适用:https://www.cnblogs.com/chuckQu/p/16608977.html)
  • 检验成果:登录即可见 projectsusers 接口 200,即正常携带 token,但是当前页面刷新就会退出登录(user 初始值为 null),接下来优化初始化 user(src\context\auth-context.tsx):
...
import { http } from "utils/http";
import { useMount } from "utils";

interface AuthForm {...}

const initUser = async () => {
  let user = null
  const token = auth.getToken()
  if (token) {
    // 由于要自定义 token ,这里使用 http 而非 useHttp
    const data = await http('me', { token })
    user = data.user
  }
  return user
}
...
export const AuthProvider = ({ children }: { children: ReactNode }) => {
  ...
  useMount(() => initUser().then(setUser))
  return (...);
};
...

思路分析:定义 initUser ,并在 AuthProvider 组件 挂载时调用,以确保只要在 localStorage 中存在 token(未登出或清除),即可获取并通过预设接口 me 拿到 user ,完成初始化

至此为止,注册登录系统(功能)闭环

9.TS的联合类型、Partial和Omit介绍

联合类型

type1 | type2

交叉类型

type1 & type2

类型别名

type typeName = typeValue

类型别名在很多情况下可以和 interface 互换,但是两种情况例外:

  • typeValue 涉及交叉/联合类型
  • typeValue 涉及 Utility Types (工具类型)

TS 中的 typeof 用来操作类型,在静态代码中使用(JStypeof 在代码运行时(runtime)起作用),最终编译成的 JS 代码不会包含 typeof 字样

Utility Types(工具类型) 的用法:用泛型的形式传入一个类型(typeNametypeof functionName)然后进行类型操作

常用 Utility Types

  • Partial:将每个子类型转换为可选类型
/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};
  • Omit:删除父类型中的指定子类型并返回新类型
/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

案例:

type Person = {
  name: string,
  age: number,
  job: {
    salary: number
  }
}

const CustomPerson: Partial<Person> = {}
const OnlyJobPerson: Omit<Person, 'name' | 'age'> = { job: { salary: 3000 } }

10.TS 的 Utility Types-Pick、Exclude、Partial 和 Omit 实现

  • Pick:经过 泛型约束 生成一个新类型(理解为子类型?)
/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
  • Exclude: 如果 TU 的子类型则返回 never 不是则返回 T
/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

keyof:索引类型查询操作符(对于任何类型 Tkeyof T的结果为 T 上已知的公共属性名的联合。)

let man: keyof Person
// 相当于 let man: 'name' | 'age' | 'job'
// keyof Man === 'name' | 'age' | 'job' // true ???

T[K]:索引访问操作符(需要确保类型变量 K extends keyof T

in:遍历

extends:泛型约束

TS 在一定程度上可以理解为:类型约束系统


部分引用笔记还在草稿阶段,敬请期待。。。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/693735.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AR宇航员互动体验软件:虚拟与现实叠加增强体验感

随着科技的不断发展&#xff0c;人们对太空探索的兴趣和热情也越来越高涨。为了满足人们对太空探索的渴望&#xff0c;广州华锐互动研发了宇航员AR模拟体验软件&#xff0c;这种软件可以让用户身临其境地体验太空探索的过程&#xff0c;提供一种全新的、令人兴奋的太空探索新体…

css基础知识十一:CSS3新增了哪些新特性?

一、是什么 css&#xff0c;即层叠样式表&#xff08;Cascading Style Sheets&#xff09;的简称&#xff0c;是一种标记语言&#xff0c;由浏览器解释执行用来使页面变得更为美观 css3是css的最新标准&#xff0c;是向后兼容的&#xff0c;CSS1/2的特性在CSS3 里都是可以使用…

图解CNN中的卷积(卷积运算、池化、Padding、多通道的卷积)

文章目录 卷积操作池化Padding对多通道&#xff08;channels&#xff09;图片的卷积套上激活函数是什么样的参考&#xff1a; 卷积层是深度学习神经网络中经常使用的一种层。它通过卷积运算来提取输入的特征&#xff0c;常用于图像、语音等信号处理任务中。 卷积层有以下几个参…

rocketmq-spring-boot-starter支持SpringBoot 1.x(spring-context 4.x)版本

1 问题说明 由于历史原因&#xff0c;项目使用的是SpringBoot1.x版本&#xff0c;而且由于种种原因&#xff0c;不能升级。在项目开发迭代过程中&#xff0c;决定使用RocketMQ作为消息中间件&#xff0c;因为是SpringBoot项目&#xff0c;理所应当的引入了rocketmq-spring-boo…

简单聊聊数字孪生与GIS融合的必要性

随着科技的不断发展和应用的不断深入&#xff0c;数字孪生和GIS在各自领域中展现出巨大的潜力。然而&#xff0c;更引人注目的是&#xff0c;数字孪生和GIS的融合将为许多行业带来全新的机遇和变革。在本文中&#xff0c;我们将探讨数字孪生和GIS融合的必要性&#xff0c;以及它…

2023ty计网期末综合题满分冲刺版

1. 假设有段1km长的CSMA/CD网络链路的数据传输率为1Gb/s。设信号在此链路媒介上的传播速度为2x105 km/s&#xff0c;求使用此协议的最短数据帧长度。 &#xff08;1&#xff09;传播时延&#xff1a;1/2000005微秒&#xff0c; &#xff08;2&#xff09;往返时延&#xff1a…

List合并的操作

List合并的操作 1.addAll方法 List list1new ArrayList();List list2new ArrayList();for (int i 0; i < 10; i) {list1.add(i*2);list2.add(i*21);}System.out.println(list1);//方法1&#xff1a;addAlllist1.addAll(list2);System.out.println(list1); 2.Stream操作 L…

ATTCK(二)之ATTCK的发展历史

ATT&CK的发展历史 MITRE公司 MITRE是美国NIST标准化组织选择的专注于网络安全的组织&#xff0c;由美国联邦政府资助。很多安全标准都MITRE制定的&#xff0c;比如有名的漏洞CVE编号规则以及威胁情报格式STIX。所以ATT&CK非常有影响力&#xff0c;而且未来能成为一个公…

MySQL----MHA高可用

文章目录 一、MHA理论1.1什么是 MHA1.2MHA 的组成1.3MHA 的特点 二、MHA的一主两从部署实验设计故障修复步骤&#xff1a; 一、MHA理论 1.1什么是 MHA MHA&#xff08;MasterHigh Availability&#xff09;是一套优秀的MySQL高可用环境下故障切换和主从复制的软件。 MHA 的出…

Java 日志重点梳理

大家好&#xff01;我是今越。重点梳理一下在 Java 程序开发中关于日志管理的知识点&#xff0c;以及在 Spring Boot 框架中该如何使用日志。 在 Java 中&#xff0c;日志框架主要分为两大类&#xff1a;日志门面和日志实现。 日志门面 日志门面定义了一组日志的接口规范&am…

LangChain-Agent自定义Tools类 ——输入参数篇(二)

给自定义函数传入输入参数&#xff0c;分别有single-input 参数函数案例和multi-input 参数函数案例&#xff1a; from langchain.agents import Tool from langchain.tools import BaseTool from math import pi from typing import Union from math import pi from typing …

StringBuffer和正则表达式

StringBuffe 获取int类型的最大值和最小值 System.out.println(Integer.MAX_VALUE);//int类型的最大值 System.out.println(Integer.MIN_VALUE);//int类型的最小值输出结果 Integer和String相互转换 Integer i1 new Integer(100); System.out.println(i1);Integer i2 new…

08 | 事务到底是隔离的还是不隔离的?

以下内容出自《MySQL 实战 45 讲》 08 | 事务到底是隔离的还是不隔离的&#xff1f; 事务启动时机 事务启动时机&#xff1a; begin/start transaction 命令并不是一个事务的起点&#xff0c;在执行到它们之后的第一个操作 InnoDB 表的语句&#xff0c;事务才真正启动。如果想…

Gradio的Button组件介绍

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

从0到1精通自动化测试,pytest自动化测试框架,配置文件pytest.ini(十三)

一、前言 pytest配置文件可以改变pytest的运行方式&#xff0c;它是一个固定的文件pytest.ini文件&#xff0c;读取配置信息&#xff0c;按指定的方式去运行 二、ini配置文件 pytest里面有些文件是非test文件pytest.ini pytest的主配置文件&#xff0c;可以改变pytest的默认…

SpringBoot多环境启动

文章目录 多环境启动多环境启动基本格式多环境启动命令格式多环境启动的兼容性 多环境启动 多环境启动基本格式 我们在开发中和上线后的环境中, 有些配置文件的值是不相同的, 但是当项目上线后我们肯定是不能修改配置文件的, 于是我们需要针对不同的环境进行不同的配置 例如下…

【C语言之区分sizeof 和 strlen】

C语言之区分sizeof 和 strlen 详解C语言sizeof 和 strlen1、单目操作符1.1、详解sizeof单目操作符1.2、sizeof代码部分1.2.1、sizeof例程11.2.2、sizeof例程21.2.3、sizeof例程3 2、详解strlen函数2.1、strlen代码部分2.1.1、strlen例程1 3、区别strlen函数和sizeof操作符3.1、…

数据库—属性闭包

属性闭包 要理解属性闭包先理解以下概念 U属性集合&#xff0c;比如一个表A有这些属性**{a,b,c,d,e}**F函数依赖集 这个就是由已知的一些函数依赖组成的集合&#xff0c;比如&#xff1a; F {a->b,b->c,c->d,ab->e} //F函数依赖集R(U&#xff0c;F)表示一个关系模…

linux工程管理工具make

linux工程管理工具make 一、make 工具的功能二、makefile 文件三、makefile 的规则Makefile 介绍一、Makefile 的规则二、一个示例三、其他例子makefiletest.cprocess.cprocess.h截图 一、make 工具的功能 1、主要负责一个软件工程中多个源代码的自动编译工作 2、还能进行环境…

Spring boot装载模板代码工程实践问题之二

Spring boot装载模板代码工程实践问题解决方案 替代方案解决方案及解释 Spring boot装载模板代码工程中&#xff0c;后续有自定注解的要求&#xff0c;在本地运行无恙&#xff0c;打成jar启动后&#xff0c;自定义注解会无效。 替代方案 在测试compiler.getTask多种参数后&…