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

该文详细介绍了使用React、Hook和TypeScript开发项目的过程,包括项目初始化、React组件实现、强类型应用、JWT和用户认证的处理,以及异步请求的管理。重点讨论了如何用useAuth切换登录状态,使用fetch抽象HTTP请求方法,并通过useHttp管理JWT保持登录状态。此外,还探讨了TS的联合类型、Partial和Omit等UtilityTypes的应用。
摘要由CSDN通过智能技术生成


学习内容来源: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

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


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

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

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

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

1~5

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 在一定程度上可以理解为:类型约束系统


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

  • 50
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 91
    评论
评论 91
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序边界

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值