文章目录
- 一、项目起航:项目初始化与配置
- 二、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 = 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
用来操作类型,在静态代码中使用(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
在一定程度上可以理解为:类型约束系统
部分引用笔记还在草稿阶段,敬请期待。。。