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


学习内容来源: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
prettier 2.8.4
json-server 0.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 在一定程度上可以理解为:类型约束系统


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

猜你喜欢

转载自blog.csdn.net/qq_32682301/article/details/131422250