文章目录
- 一、项目起航:项目初始化与配置
- 二、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 |
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 |
具体配置、操作和内容会有差异,“坑”也会有所不同。。。
一、项目起航:项目初始化与配置
- 【实战】 项目起航:项目初始化与配置 —— 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
正常,但是projects
和users
接口401
(A 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
会覆盖前面已有属性- 需要手动区别
get
和post
不同的携参方式axios
和fetch
不同,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
不能在useEffect
的callback
中直接使用,否则会报错: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
即 携带token
的http
函数)- 依赖中只有
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)
- 检验成果:登录即可见
projects
和users
接口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 = nullconst token = auth.getToken()if (token) {// 由于要自定义 token ,这里使用 http 而非 useHttpconst 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
用来操作类型,在静态代码中使用(JS
的 typeof
在代码运行时(runtime
)起作用),最终编译成的 JS
代码不会包含 typeof
字样
Utility Types
(工具类型) 的用法:用泛型的形式传入一个类型(typeName
或 typeof 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
: 如果T
是U
的子类型则返回never
不是则返回T
/*** Exclude from T those types that are assignable to U*/
type Exclude<T, U> = T extends U ? never : T;
keyof
:索引类型查询操作符(对于任何类型 T
,keyof 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
在一定程度上可以理解为:类型约束系统
部分引用笔记还在草稿阶段,敬请期待。。。