- 01-页面结构
- 02-获取用户信息并展示
- 03-用户信息-发送请求获取数据
- 04-用户信息-组件分发 action
- 05-用户信息-存储到 redux
- 06-用户信息-展示用户信息
- 07-封装 axios 响应工具类型
- 08-个人信息-页面结构
- 09-个人信息-获取并展示个人信息
- 10-自定义 hooks
- 11-自定义 hooks-分析要封装的代码逻辑
- 12-自定义 hooks-实现自定义 hooks-不考虑类型
- 13-自定义 hooks-为实现的自定义 hooks 添加类型
- 14-自定义 hooks-改造个人信息
- 15-修改昵称-渲染修改昵称组件
- 16-修改昵称-展示修改昵称组件
- 17-修改昵称-隐藏修改昵称组件
- 18-修改昵称-弹出层显示昵称
- 19-修改昵称-提交时拿到昵称回传给父组件
- 20-修改昵称-发送请求
- 21-修改昵称-更新 redux 状态
- 22-修改昵称-完成修改
- 23-修改简介-复用修改昵称弹出层
- 24-修改简介-展示修改简介弹出层
- 25-修改简介-弹出层中展示简介内容
- 25-修改简介-复用提交时的回传逻辑
- 26-修改性别-渲染修改性别弹出层
- 27-修改性别-修改性别弹出层的展示或隐藏
- 28-修改性别-更新数据
- 28-修改头像-展示修改头像弹出层内容
- 29-修改头像-弹窗选择图片
- 30-修改头像-组装修改头像的数据
- 31-修改头像-更新头像
- 32-修改生日-展示日期选择器
- 33-修改生日-更新生日
- 34-退出登录-弹窗确认
- 35-退出登录-功能完成
- 36-封装鉴权路由组件
- 37-登录时跳转到相应页面
- 38-理解无感刷新 token
- 39-实现无感刷新 token-换取新的 token
01-页面结构
目标:能够根据模板搭建个人中心页面结构
步骤:
-
02-获取用户信息并展示
该功能分为以下几步来实现:
发送请求获取用户信息
- 页面分发获取用户信息的 action
- 将用户信息存储到 redux 中
-
03-用户信息-发送请求获取数据
目标:能够发送请求获取用户信息
步骤: 根据接口准备所需类型
- 创建 actions/profile.ts 文件
- 创建获取用户信息的 action
核心代码:
types/data.d.ts 中:
// 我的 - 个人信息
export type User = {
id: string;
name: string;
photo: string;
art_count: number;
follow_count: number;
fans_count: number;
like_count: number;
};
actions/profile.ts 中:
import { http } from ‘@/utils’;
import type { RootThunkAction } from ‘@/types/store’;
import type { User } from ‘@/types/data’;
type UserResponse = {
message: string;
data: User;
};
// 我的页面 - 获取个人信息
export const getUser = (): RootThunkAction => {
return async (dispatch) => {
const res = await http.get
console.log(res);
};
};
04-用户信息-组件分发 action
目标:能够在我的页面中分发获取用户信息的 action
核心代码:
Profile/index.tsx 中:
import { useEffect } from ‘react’;
import { useDispatch } from ‘react-redux’;
import { getUser } from ‘@/store/actions’;
const Profile = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getUser());
}, [dispatch]);
// …
};
05-用户信息-存储到 redux
目标:能够将用户信息保存到 redux 中
步骤:
- 准备保存状态到 redux 的 action 类型
- 在获取个人信息的 action 中将用户信息存储到 redux 中
- 创建 reducers/profile.ts,并完成存储用户信息的功能
- 在 reducers/index.ts 中,将 profile 合并到根 reducer 中
核心代码:
types/store.d.ts 中:
import type { User } from ‘../data’;
// 将 ProfileAction 联合到 redux action 类型中
type RootAction = LoginAction | ProfileAction;
export type ProfileAction = {
type: ‘profile/getUser’;
payload: User;
};
actions/profile.ts 中:
export const getUser = (): RootThunkAction => {
return async (dispatch) => {
const res = await http.get
const { data, message } = res.data;
// 存储 redux 中
// 因为已经有 TS 类型,所以,此处代码都是有提示的
dispatch({ type: ‘profile/getUser’, payload: data });
};
};
reducers/profile.ts 中:
import type { User } from ‘@/types/data’;
import type { ProfileAction } from ‘@/types/store’;
type ProfileState = {
user: User;
};
const initialState = {
user: {},
} as ProfileState;
const profile = (state = initialState, action: ProfileAction): ProfileState => {
switch (action.type) {
case ‘profile/getUser’:
return {
…state,
user: action.payload,
};
default:
return state;
}
};
export default profile;
reducers/index.ts 中:
import profile from ‘./profile’;
const rootReducer = combineReducers({
// …
profile,
});
06-用户信息-展示用户信息
目标:能够展示用户信息
分析说明:
TS 支持索引查询类型(索引访问类型),用来查询属性的类型
type RootState = {
name: string;
};
// T1 的类型是:name 属性的类型 string
type T1 = RootState[‘name’];
步骤:
- 导入 useSelector
- 调用 useSelector 获取 user 状态
- 从 user 对象中解构出用户数据并展示在页面中
核心代码:
import { useSelector } from ‘react-redux’;
import { RootState } from ‘@/types/store’;
const Profile = () => {
// const { user } = useSelector
const { user } = useSelector((state: RootState) => state.profile);
const { photo, name, like_count, follow_count, fans_count, art_count } = user;
// …
// 在 JSX 中渲染从 redux 中拿到的状态数据即可
};
07-封装 axios 响应工具类型
目标:能够封装工具类型统一处理 axios 的响应类型
分析说明:
对于项目接口来说,任何一个接口返回的顶层数据格式都是一样的,都有:
- 都有 message 属性,类型为 string (固定)
- 都有 data 属性,但是具体是什么类型不确定(变化)
// 登录
type LoginResponse = {
message: string;
data: Token;
};
// 用户
type UserResponse = {
message: string;
data: User;
};
问题:TS 类型中,什么类型可以配合多种类型使用,并且可以在使用时指定明确类型?泛型
步骤:
- 在 types/data.d.ts 中,创建泛型工具类型 ApiResponse
- 通过该泛型工具类型,统一处理 axios 响应类型
核心代码:
types/data.d.ts 中:
// 泛型工具类型
type ApiResponse = {
message: string;
data: Data;
};
// 统一处理axios响应类型:
// 登录
export type LoginResponse = ApiResponse
// 用户
export type UserResponse = ApiResponse
actions/login.ts 中:
import type { LoginResponse } from ‘@/types/data’;
actions/profile.ts 中:
import type { UserResponse } from ‘@/types/data’;
08-个人信息-页面结构
目标:能够根据模板展示个人信息页面
步骤:
- 将 Edit 模板拷贝到 Profile 目录中
- 在 App.tsx 中配置个人信息页面的路由
- 在 App.scss 中统一调整 NavBar、List 的样式
核心代码:
App.tsx 中:
import ProfileEdit from ‘./pages/Profile/Edit’;
const App = () => {
return (
// …
);
};
App.scss 中:
.adm-list-default {
border: none;
font-size: 16px;
}
.adm-nav-bar-title {
font-size: 17px;
}
.adm-nav-bar-back-arrow {
font-size: 17px;
}
09-个人信息-获取并展示个人信息
目标:能够获取并展示编辑时的个人信息
步骤:
- 在 types/data.d.ts 中,根据接口准备好返回数据类型
- 在 actions/profile.ts 中,创建获取编辑时个人信息的 action
- 在 types/store.d.ts 中,创建相应的 redux action 类型
- 在 actions 中分发修改 redux 状态的 action
- 在 reducers 中处理该 action,并将状态存储到 redux 中
核心代码:
types/data.d.ts 中:
export type UserProfile = {
id: string;
photo: string;
name: string;
mobile: string;
gender: number;
birthday: string;
intro: string;
};
export type UserProfileResponse = ApiResponse
actions/profile.ts 中:
import type { UserProfileResponse } from ‘@/types/data’;
export const getUserProfile = (): RootThunkAction => {
return async (dispatch) => {
const res = await http.get
dispatch({ type: ‘profile/getUserProfile’, payload: res.data.data });
};
};
types/store/d.ts 中:
import type { UserProfile } from ‘./data’;
export type ProfileAction =
// …
{
type: ‘profile/getUserProfile’;
payload: UserProfile;
};
reducers/profile.ts 中:
// 注意:以下代码为简化代码
import type { UserProfile } from ‘@/types/data’;
type ProfileState = {
// …
userProfile: UserProfile;
};
const initialState = {
// …
userProfile: {},
} as ProfileState;
const profile = (state = initialState, action: ProfileAction): ProfileState => {
switch (action.type) {
// …
case ‘profile/getUserProfile’:
return {
…state,
userProfile: action.payload,
};
}
};
Profile/Edit/index.tsx 中:
import { useEffect } from ‘react’;
import { useDispatch, useSelector } from ‘react-redux’;
import { getUserProfile } from ‘@/store/actions’;
import type { RootState } from ‘@/types/store’;
const ProfileEdit = () => {
const dispatch = useDispatch();
const { userProfile } = useSelector((state: RootState) => state.profile);
useEffect(() => {
dispatch(getUserProfile());
}, [dispatch]);
const { photo, name, intro, gender, birthday } = userProfile;
// JSX 中展示用户信息
return (
// …
extra={
{intro}
}
>
{intro || ‘未填写’}
);
};
10-自定义 hooks
目标:能够知道什么是自定义 hooks
分析说明:
除了使用 React 提供的 hooks 之外,开发者还可以创建自己的 hooks,也就是自定义 hooks
问题:为什么要创建自定义 hooks?
回答:实现状态逻辑复用,也就是将与状态相关的逻辑代码封装到一个函数中,哪个地方用到了,哪个地方调用即可
封装自定义 hook 与封装普通函数的区别:是否包含了状态逻辑,如果需要包含 React 状态逻辑,那么,就得用自定义 hook;而如果封装的内容不涉及状态,此时,就用普通的函数封装即可。
如何理解状态逻辑?可以简单的理解为代码中是否包含 React hooks,比如,useState、useEffect、useRef 等,那么就得使用自定义 hook 来封装
自定义 hooks 的特点:
- 名称必须以 use 开头
- 和内置的 React Hooks 一样,自定义 hook 也是一个函数
- React Hooks(不管是内置的还是自定义的)只能在函数组件或其他自定义 hook 内使用
// 创建自定义 hooks 函数
const useXxx = (params) => {
// …
// 需要复用的状态逻辑代码
// …
return xxx
}
// 使用自定义 hooks 函数
const Hello = () => {
const xxx = useXxx(…)
}
- 说明:自定义 hooks 就是一个函数,可以完全按照对函数的理解,来理解自定义 hooks
- 比如:参数和返回值都可以可选的,可以提供也可以不提供,根据实际的需求来实现即可
总结:
- 自定义 hooks 是函数吗? 是
- 自定义 hooks 的名称有什么约束?必须以 use 开头
-
11-自定义 hooks-分析要封装的代码逻辑
目标:能够分析要封装的代码逻辑找到相同点和不同点
分析说明:
函数封装的基本思想:将相同的逻辑直接拷贝到函数中,不同的数据或逻辑通过函数参数传入。需要返回数据,就通过函数返回值返回
// 不同:导入的 action 函数不同
import { getUser } from ‘@/store/actions’;
import { getUserProfile } from ‘@/store/actions’;
const Profile = () => {
// 不同: 获取到的状态不同
// 注意:虽然,此处两个获取到的都是 ‘profile’,但,实际上其他页面获取到的数据可能不同
const { user } = useSelector(
(state) => state.profile,
);
const { userProfile } = useSelector(
(state) => state.profile,
);
useEffect(() => {
// 不同:分发的 action 函数不同
dispatch(getUser());
dispatch(getUserProfile());
}, [dispatch]);
// 不同:获取到的状态不同
const { photo, name, like_count, follow_count, fans_count, art_count } = user;
const { photo, name, intro, gender, birthday } = userProfile;
// …
};
分析以上【我的页面】和【个人信息页面】中获取数据的逻辑,有两个不同点: 分发的 action 函数不同
- 获取的状态不同
所以,只需要把这两点作为自定义 hooks 的参数即可
而这两个功能最终的目的,都是为了拿到相应的应用状态。因此,把拿到的应用状态,作为自定义 hooks 的返回值即可
// 将不同的内容,作为函数参数传入
const useInitialState = (action, stateName) => {
// 需要复用的状态逻辑代码( 相同的逻辑代码 )
// 最终,该操作需要什么数据,最终就通过 返回值 来返回
return state;
};
// 使用自定义 hooks:
const { userProfile } = useInitialState(getUserProfile, ‘profile’);
const { photo, name, intro, gender, birthday } = userProfile;
const { user } = useInitialState(getUser, ‘profile’);
const { photo, name, like_count, follow_count, fans_count, art_count } = user;
12-自定义 hooks-实现自定义 hooks-不考虑类型
目标:能够通过 JS 代码实现自定义 hooks
步骤:
- 创建 utils/use-initial-state.ts 文件
- 创建 useInitialState 函数(自定义 hook)
- 导入用到的包
- 将相同的逻辑,直接封装到 useInitialState 函数中
- 将不同的地方,作为 useInitialState 函数的参数
- 将拿到的状态作为 useInitialState 函数的返回值
核心代码:
utils/use-initial-state.ts 中:
// 导入用到的包
import { useEffect } from ‘react’;
import { useDispatch, useSelector } from ‘react-redux’;
import type { RootState } from ‘@/types/store’;
// 创建 useInitialState 函数(自定义 hook)
const useInitialState = (action: any, stateName: any) => {
const dispatch = useDispatch();
const state = useSelector((state: RootState) => state[stateName]);
useEffect(() => {
dispatch(action());
}, [dispatch, action]);
return state;
};
export { useInitialState };
Profile/Edit.tsx 中:
import { useInitialState } from ‘@/utils/use-initial-state’;
const ProfileEdti = () => {
const { userProfile } = useInitialState(getUserProfile, ‘profile’);
// …
};
13-自定义 hooks-为实现的自定义 hooks 添加类型
目标:能够为实现的自定义 hooks 添加类型
分析说明:
对于 useInitialState 这个自定义 hook 来说,只需要为参数指定类型即可
- 参数 action:就是一个函数,所以,直接指定为最简单的函数类型即可
- 参数 stateName:
- stateName 表示从 Redux 状态中取出的状态名称,比如,’profile’ 或 ‘login’
- 所以,stateName 应该是 RootState 中的所有状态名称中的任意一个
- 但是,具体是哪一个不确定,只有在使用该函数时才能确定下来
- 问题:如果一个类型不确定,应该是什么 TS 中的什么类型来实现?泛型
// Redux 整个应用,状态的类型
// 目前,Redux 中已经有两个状态:login(登录时的状态)和 profile(我的 - 个人信息)
// RootState => { login: Token; profile: ProfileState; a: B }
type RootState = ReturnType
// 希望,在使用 useInitialState 自定义 hook 的时候,只应该获取到 Redux 中已有的状态,但是,获取哪一个状态时不确定的,只有在调用时才能确定
// 因为 stateName 只能是 Redux 已有状态中的任何一个,所以,stateName 的取值范围:’login’ | ‘profile’ | ‘a’
// 而此处不能直接写死一个联合类型,因为 Redux 中的状态将来是会变化(将来还要继续往 redux 中添加状态)
// 既然不能写死,就得动态获取,也就是 Redux 状态类型 RootState 中有哪些状态,就拿到这些状态的名字即可
// 那也就是要获取到 RootState 对象类型中,所有键的集合: keyof RootState => ‘login’ | ‘profile’ | ‘a’
const useInitialState = (action: () => void, stateName: keyof RootState) {}
// 使用泛型:
// S extends keyof RootState 表示:创建了一个泛型的类型变量叫做:S
// 通过 extends 关键字来给 类型变量S 添加了泛型约束
// 约束:S 的类型应该是 keyof RootState 中的任意一个,也就是:’login’ | ‘profile’ | ‘a’
// const useInitialState = (action: () => void, stateName: S) {}
const useInitialState =
// — 对比以上两种方式的区别 —
// 1 这种方式:在调用该函数时,最终得到的返回值类型:Token | ProfileState | B 也就是将所有可能出现的情况都列举出来了
const useInitialState = (action: () => void, stateName: keyof RootState) {}
// 2 这种方式:在调用该函数时,最终得到的返回值类型:是某一个状态的类型,这个类型由我们传入的 stateName 类决定
// 比如,useInitialState(getUser, ‘profile’) 返回值类型,就是 profile 这个键对应的类型:ProfileState
const useInitialState =
// 调用:
useInitialState(getUser, ‘login’)
useInitialState(getUser, ‘profile’)
useInitialState(getUser, ‘a’)
// 原来讲过的泛型基础:
function id
return value
}
id
// 省略类型不写:
id(10)
核心代码:
utils/use-initial-state.ts 中:
// StateName 是泛型类型变量的名称
// extends 表示要遵循的泛型约束
// keyof RootState 用来获取 Redux 状态中所有的 key,即:所有状态名称中的任意一个
// 将类型变量作为参数 stateName 的类型
// 解释:约束参数 stateName 只能是 RootState 所有状态名称中的任意一个
const useInitialState =
action: () => void,
stateName: StateName,
) => {
const state = useSelector((state: RootState) => state[stateName]);
// …
};
- 完整代码如下:
// 导入用到的包
import { useEffect } from ‘react’;
import { useDispatch, useSelector } from ‘react-redux’;
import type { RootState } from ‘@/types/store’;
// 创建 useInitialState 函数(自定义 hook)
const useInitialState =
action: () => void,
stateName: StateName,
) => {
const dispatch = useDispatch();
const state = useSelector((state: RootState) => state[stateName]);
// const state = useSelector
// state => state[stateName]
// )
useEffect(() => {
dispatch(action());
}, [dispatch, action]);
return state;
};
export { useInitialState };
14-自定义 hooks-改造个人信息
目标:能够使用封装好的自定义 hook 改造获取个人信息功能
步骤:
- 导入自定义 hook
- 调用自定义 hook,并传入相应的参数,验证是否有明确的类型提示
- 通过返回值,拿到对应的状态
核心代码:
Profile/Edit/index.tsx 中:
import { useInitialState } from ‘@/utils/use-initial-state’;
const ProfileEdit = () => {
const { userProfile } = useInitialState(getUserProfile, ‘profile’);
// …
};
15-修改昵称-渲染修改昵称组件
目标:能够根据模板渲染修改昵称组件
步骤:
- 在 Edit 目录中创建 components 文件夹
- 将准备好的修改昵称的模板(EditInput)拷贝到 components 目录中
- 从 antd-mobile 中导入 Popup 组件
- 导入修改昵称组件,在 Popup 组件中渲染
核心代码:
Edit/index.tsx 中:
import { Popup } from ‘antd-mobile’;
import EditInput from ‘./components/EditInput’;
const ProfileEdit = () => {
return (
// …
);
};
16-修改昵称-展示修改昵称组件
目标:能够在点击修改昵称时展示弹出层
步骤:
- 准备控制弹出层展示或隐藏的状态 inputVisible,默认值为 false
- 使用状态 inputVisible 控制 Popup 组件的展示和隐藏
- 给昵称绑定点击事件
- 在点击事件中修改状态 inputVisible 为 true 来展示弹出层
核心代码:
Edit/index.tsx 中:
import { useState } from ‘react’
const ProfileEdit = () => {
const [inputVisible, setInputVisible] = useState(false)
const onInputShow = () => {
setInputVisible(true)
}
return (
// …
昵称
)
}
17-修改昵称-隐藏修改昵称组件
目标:能够在修改昵称组件点击返回时隐藏
分析说明:
Edit 组件和 EditInput 组件之间是父子组件的关系
目标是通过 EditInput 子组件来隐藏弹出层,也就是在子组件中修改父组件中的状态
所以,此处通过子到父的组件通讯来解决
步骤:
- 在 Edit 组件中,创建一个控制弹出层隐藏的函数
- 将该函数作为属性传递给子组件 EditInput
- 在 EditInput 子组件中,通过 props 接收该属性
- 为 EditInput 组件的 props 指定类型
- 在点击导航栏返回按钮时,调用该关闭函数即可
核心代码:
Edit/index.tsx 中:
const ProfileEdit = () => {
const onInputHide = () => {
setInputVisible(false);
};
return (
// …
);
};
Edit/components/EditInput/index.tsx 中:
type Props = {
onClose: () => void;
};
const EditInput = ({ onClose }: Props) => {
return
};
18-修改昵称-弹出层显示昵称
目标:能够在弹出层文本框中展示昵称
步骤:
- 为 EditInput 组件添加 value 属性,用于接收昵称
- 在 Edit 组件中将昵称传递给 EditInput 组件
- 在 EditInput 组件中创建一个状态,默认值为接收到的昵称
- 为 Input 组件设置 value 来展示昵称
- 为 Input 组件设置 onChange 来修改昵称
核心代码:
Edit/index.tsx 中:
Edit/components/EditInput/index.tsx 中:
import { useState } from ‘react’;
type Props = {
value: string;
};
const EditInput = ({ value }: Props) => {
const [inputValue, setInputValue] = useState(value);
return (
// …
);
};
19-修改昵称-提交时拿到昵称回传给父组件
目标:能够在点击提交时拿到昵称并回传给父组件
分析说明:
用户个人信息的状态是在 Edit 父组件中拿到的,所以,修改用户个人信息也应该由 Edit 父组件发起
因此,需要将修改后的昵称回传给 Edit 父组件
步骤:
- 为 EditInput 组件添加 onUpdateName 函数属性
- 在 Edit 组件中为 EditInput 组件传递 onUpdateName 属性
- 为提交按钮绑定点击事件
- 在点击事件中,调用父组件传递过来的 onUpdateName 函数,将昵称回传给父组件
核心代码:
Edit/index.tsx 中:
const ProfileEdit = () => {
const onUpdateName = (value: string) => {
console.log(‘父组件拿到修改后的昵称:’, value);
onInputHide();
};
return (
// …
);
};
Edit/components/EditInput/index.tsx 中:
type Props = {
// …
onUpdateName: (name: string) => void;
};
const EditInput = ({ onUpdateName }: Props) => {
// …
const onSave = () => {
onUpdateName(inputValue);
};
return (
// …
提交
}
>
编辑昵称
);
};
20-修改昵称-发送请求
目标:能够修改用户昵称
分析说明:
- 该接口没有返回值,所以,不需要额外定义接口返回数据的类型
- 不管修改昵称还是简介,都是通过同一个接口来修改的,因此,可以通过传入不同的参数来复用该 action
步骤:
- 创建 updateUserProfile action
- 为该 action 指定参数,参数类型为要修改的用户信息
- 发送请求修改用户数据
核心代码:
ProfileEdit.tsx 中:
// 更新用户昵称
const onUpdateName = (name: string) => {
// 修改昵称
dispatch(updateUserProfile({ name }));
// 修改简介
// dispatch(updateUserProfile({ intro }))
};
actions/profile.ts 中:
import type { UserProfile } from ‘@/types/data’;
export const updateUserProfile = (
// 参数为 UserProfile 中的任意属性,也就是调用该 action 时,可以传入任意的用户信息
// 从而来实现该接口的复用
userProfile: Partial
): RootThunkAction => {
return async (dispatch) => {
await http.patch(‘/user/profile’, userProfile);
};
};
21-修改昵称-更新 redux 状态
目标:能够更新用户昵称状态
步骤:
- 在 types/store.d.ts 中添加更新用户个人资料的 action 类型
- 在 actions 中分发 action 以更新用户昵称状态
- 在 reducers 中更新用户昵称
核心代码:
types/store.d.ts 中:
export type ProfileAction =
// …
{
type: ‘profile/update’;
payload: Partial
};
actions/profile.ts 中:
export const updateUserProfile = (
userProfile: Partial
): RootThunkAction => {
return async (dispatch) => {
await http.patch(‘/user/profile’, userProfile);
// 分发 action 以更新用户昵称
dispatch({ type: ‘profile/update’, payload: userProfile });
};
};
reducers/profile.ts 中:
const profile = (state = initialState, action: ProfileAction): ProfileState => {
// …
case ‘profile/update’:
return {
…state,
userProfile: {
…state.userProfile,
…action.payload
}
}
}
}
22-修改昵称-完成修改
目标:能够实现修改昵称并提示更新成功
核心代码:
Edit/index.tsx 中:
import { useDispatch } from ‘react-redux’;
import { updateUserProfile } from ‘@/store/actions’;
const ProfileEdit = () => {
// …
const onUpdateName = async (value: string) => {
await dispatch(updateUserProfile({ name: value }));
Toast.show({
content: ‘更新成功’,
duration: 1000,
});
// 关闭弹出层
onInputHide();
};
};
23-修改简介-复用修改昵称弹出层
目标:能够复用修改昵称弹出层
分析说明:
修改简介弹出层有两种实现方式:
- 创建一个新的弹出层,也就是参考修改昵称的弹出层重写一遍
- 复用修改昵称的弹出层
此处,我们选择复用修改昵称的弹出层组件。
要复用同一个组件,就需要区分出点击的是昵称还是简介,此处,可以通过状态来区分,修改如下:
// 原来:
const [inputVisible, setInputVisible] = useState(false);
// 现在:
const [inputPopup, setInputPopup] = useState({
// type 属性:用于告诉 EditInput 组件,当前修改的是昵称还是简介
type: ‘’, // ‘name’ or ‘intro’
// 当前值
value: ‘’,
// 展示或隐藏状态
visible: false,
});
// 使用:
// 1 点击修改昵称
setInputPopup({
type: ‘name’,
value: name,
visible: true,
});
步骤:
- 修改控制昵称弹出层状态的结构
- 修改昵称弹出层的展示、隐藏操作
- 修改控制弹出层的 visible 属性值
- 修改传递给 EditInput 组件的 value 属性
- 为 EditInput 组件添加 type 属性,来接收当前展示类型
核心代码:
Edit/index.tsx 中:
type InputPopup = {
type: ‘’ | ‘name’ | ‘intro’;
value: string;
visible: boolean;
};
const ProfileEdit = () => {
const [inputPopup, setInputPopup] = useState
type: ‘’,
value: ‘’,
visible: false,
});
const onInputShow = () => {
setInputPopup({
type: ‘name’,
value: name,
visible: true,
});
};
const onInputHide = () => {
setInputPopup({
type: ‘’,
value: ‘’,
visible: false,
});
};
return (
// …
);
};
EditInput/index.tsx 中:
type Props = {
type: ‘’ | ‘name’ | ‘intro’;
};
const EditInput = ({ type }: Props) => {
// …
};
24-修改简介-展示修改简介弹出层
目标:能够展示修改简介弹出层内容
步骤:
- 给简介绑定点击事件
- 在点击事件中,设置为简介的信息
- 在 EditInput 组件中,根据 type 的类型来展示昵称或简介信息(比如,标题、文本框或富文本框等)
核心代码:
Edit/index.tsx 中:
const ProfileEdit = () => {
const onIntroShow = () => {
setInputPopup({
type: ‘intro’,
value: intro,
visible: true,
});
};
return (
// …
);
};
EditInput/index.tsx 中:
import { TextArea } from ‘antd-mobile’;
type Props = {
type: ‘’ | ‘name’ | ‘intro’;
};
const EditInput = ({ type }: Props) => {
const isName = type === ‘name’;
return (
// …
{isName ? ‘昵称’ : ‘简介’}
{isName ? (
placeholder=”请输入”
value={inputValue}
onChange={setInputValue}
/>
) : (
);
};
25-修改简介-弹出层中展示简介内容
目标:能够在复用修改昵称弹出层后正确展示简介内容
分析说明:
问题:弹出层中昵称和简介之间会相互影响 操作如下:
- 先点击昵称,弹出层中展示昵称内容【正常】
- 关闭弹出层后,再点击简介,此时,弹出层中展示的仍然是昵称内容【Bug】
问题分析: 默认情况下,antd-mobile 中的 Popup 组件在隐藏时,不会销毁所渲染的内容,而是隐藏(display: none)。
对于第一次点击昵称来说,会展示弹出层内容,并第一次执行 EditInput 组件中的代码。EditInput 组件通过 props 接收到 value,然后,交给内部的 useState hook,该 state 的默认值就是 props.value 的值,这一切都是正常的。
接下来,关闭弹出层,Popup 并没有销毁组件内容。所以,当我们再次点击简介时,因为给 EditInput 组件传递了新的属性值,所以,对于 EditInput 组件来说相当于进行了重新渲染。 但是,由于 useState hook 默认值的特点只会在组件第一次渲染时生效。因此,虽然,EditInput 接收到了简介的 value 值,但是对于 useState 来说,这次的 value 值会被忽略。因此,展示在富文本框中的仍然是昵称内容。
// useState 的默认值,只会在组件第一次渲染时生效
// 以后的每次组件更新后的重新渲染,拿到的就是最新的 count 值了
const [count, setCount] = useState(0);
setCount(count + 1);
解决方式:在每次展示 EditInput 组件,让 useState hook 都能正确使用 value 值即可。
- 通过 useEffect 监听 props.value 变化
EditInput/index.tsx 中:
useEffect(() => {
// value 为 null 或 undefined 时,设置为默认值为空字符串
setInputValue(value ?? ‘’);
}, [value]);
- 为 Popup 组件添加 destroyOnClose 属性
Edit/index.tsx 中:
destroyOnClose
>
- 利用特殊的 key 属性
- React 内部 diff 时,首先判断 key 是否相同,key 不同直接重新渲染该组件
25-修改简介-复用提交时的回传逻辑
目标:能够在点击提交时复用修改昵称的回传逻辑
分析说明:
复用时,只需额外的提供当前的 type 类型即可
type Props = {
onUpdateName: (name: string) => void;
};
type Props = {
onUpdateProfile: (type: ‘name’ | ‘intro’, value: string) => void;
};
步骤:
- 修改 EditInput 组件回传数据的函数属性类型
- 修改提交按钮中,调用回传数据函数的参数
- 修改 Edit 组件中传递给 EditInput 的属性
- 修改 Edit 组件中更新用户信息的函数参数
- 关闭弹出层
核心代码:
EditInput/index.tsx 中:
type Props = {
onUpdateProfile: (type: ‘name’ | ‘intro’, value: string) => void;
};
const EditInput = ({ onUpdateProfile }: Props) => {
const onSave = () => {
// 通过该判断,去掉 type 属性中的 ‘’ 类型,解决类型不一致的问题
if (type === ‘’) return;
onUpdateProfile(type, inputValue);
};
// …
};
Edit/index.tsx 中:
const ProfileEdit = () => {
const onUpdateProfile = async (type: ‘name’ | ‘intro’, value: string) => {
await dispatch(updateUserProfile({ [type]: value }));
onInputHide();
};
return (
// …
);
};
26-修改性别-渲染修改性别弹出层
目标:能够显示修改性别弹出层
步骤:
- 将准备好的修改性别的模板(EditList)拷贝到 components 目录中
- 导入修改性别组件,在 Popup 组件中渲染
核心代码:
Edit/index.tsx 中:
import EditList from ‘./components/EditList’;
const ProfileEdit = () => {
return (
// …
);
};
27-修改性别-修改性别弹出层的展示或隐藏
目标:能够控制修改性别弹出层的展示或隐藏
分析说明:
修改性别和修改头像的弹出层内容几乎是一样的,因此,也可以复用同一个弹出层组件
因此,接下来要从复用的角度,设计修改性别弹出层的逻辑(可以参考刚刚实现的修改昵称和简介)
步骤:
- 准备用于控制修改性别弹出层的状态
- 为性别添加点击事件,在点击事件中修改状态进行展示
- 创建隐藏弹出层的控制函数,在点击遮罩时关闭弹出层,并传递给 EditList 组件
- 为 EditList 组件添加 props 类型,并接收隐藏函数
- 为取消按钮添加点击事件来触发隐藏弹出层
核心代码:
Edit/index.tsx 中:
type ListPopup = {
type: ‘’ | ‘gender’ | ‘photo’
visible: boolean
}
const ProfileEdit = () => {
const [listPopup, setListPopup] = useState
type: ‘’,
visible: false
})
const onGenderShow = () => {
setListPopup({
type: ‘gender’,
visible: true
})
}
const onGenderHide = () => {
setListPopup({
type: ‘’,
visible: false
})
}
return (
// …
>
性别
)
}
EditList/index.tsx 中:
type Props = {
onClose: () => void;
};
const EditList = ({ onClose }: Props) => {
return (
取消
);
};
28-修改性别-更新数据
目标:能够实现修改性别功能
步骤:
- 为 EditList 组件添加 type 和 onUpdateProfile 属性及其类型
- 为男/女列表项绑定点击事件
- 在点击事件中拿到当前当前点击项的 value 值
- 调用 onUpdateProfile 回传数据给父组件
- 在父组件 Edit 中,为 EditList 组件指定 onUpdateProfile,值为 onUpdateProfile 函数
- 关闭修改弹出层
核心代码:
EditList/index.tsx 中:
type Props = {
type: ‘’ | ‘gender’ | ‘photo’;
onUpdateProfile: (type: ‘gender’ | ‘photo’, value: string) => void;
};
const EditList = ({ type, onUpdateProfile }: Props) => {
const onItemClick = (value: string) => {
if (type === ‘’) return;
onUpdateProfile(type, value);
};
return (
男
女
);
};
Edit/index.tsx 中:
const ProfileEdit = () => {
const onUpdateProfile = () => {
// …
onGenderHide();
};
return (
// …
// onUpdateProfile 复用修改昵称或简介时的函数
onUpdateProfile={onUpdateProfile}
/>
);
};
28-修改头像-展示修改头像弹出层内容
目标:能够展示修改头像弹出层内容
分析说明:
修改性别和修改头像复用同一个弹出层组件,为了展示不同的内容,可以将弹出层内容,抽象成数据,然后,根据当前传入的 type 类型,来决定渲染哪种数据。
const genderList = [
{ text: ‘男’, value: ‘0’ },
{ text: ‘女’, value: ‘1’ },
];
const photoList = [
{ text: ‘拍照’, value: ‘’ },
{ text: ‘本地选择’, value: ‘’ },
];
// 要渲染的数据为:
const list = type === ‘gender’ ? genderList : photoList;
步骤:
- 为头像列表项绑定点击事件
- 在点击事件中,展示对应弹出层
- 在 EditList 组件中,创建性别和头像对应的列表数据
- 根据当前传入的 type 属性,决定要渲染哪种列表数据
- 根据 list 数组,渲染列表结构
核心代码:
Edit/index.tsx 中:
const ProfileEdit = () => {
const onPhotoShow = () => {
setListPopup({
type: ‘photo’,
visible: true,
});
};
return (
// …
);
};
EditList/index.tsx 中:
const genderList = [
{ text: ‘男’, value: ‘0’ },
{ text: ‘女’, value: ‘1’ },
];
const photoList = [
{ text: ‘拍照’, value: ‘picture’ },
{ text: ‘本地选择’, value: ‘local’ },
];
const EditList = ({ type, onClose, onUpdateProfile }: Props) => {
const list = type === ‘gender’ ? genderList : photoList;
return (
{list.map((item) => (
key={item.text}
onClick={() => {
if (type === ‘’) return;
onUpdateProfile(type, item.value);
}}
>
{item.text}
))}
取消
);
};
29-修改头像-弹窗选择图片
目标:能够在点击拍照或本地选择时弹窗选择图片
分析说明:
修改头像的逻辑与修改性别的其他用户信息的逻辑不同:
- 修改性别的其他用户信息:点击男/女或者提交按钮时,直接发送请求,修改后端数据即可
- 修改头像:点击拍照或本地选择时,弹窗让用户选择图片(不考虑拍照),等用户选择图片后,才会发送请求,修改后台数据
因此,需要单独处理修改头像的逻辑。
此时,就会有一个新的问题:如何在点击拍照或本地选择时,弹窗让用户选择图片?
我们知道,HTML 中的 input[type=file] 标签,可以实现图片选择。所以,此处需要用到 file 标签。
但是,点击项并不是 file,因此,可以转换下思路:在点击拍照或本地选择时,触发 file 的点击即可。
// 假设 file 就是 input[type=file] 对应的 DOM 对象:
file.click();
步骤:
- 在 Edit 组件中,创建一个 input[type=file] 标签,并且设置为 hidden(隐藏该标签)
- 创建一个 ref 对象,来拿到 file 标签
- 在 onUpdateProfile 回调中,判断类型是否为 photo
- 如果是,触发 file 的点击
- 如果不是,继续执行原来的逻辑即可
核心代码:
Edit/index.tsx 中:
const ProfileEdit = () => {
const fileRef = useRef
const onUpdateProfile = async (
type: ‘name’ | ‘intro’ | ‘gender’ | ‘photo’,
value: string
) => {
if (type === ‘photo’) {
fileRef.current?.click()
} else {
// … 原来的逻辑
}
}
return (
// …
)
}
30-修改头像-组装修改头像的数据
目标:能够组装修改头像需要的数据
步骤:
- 创建函数,监听 input[type=file] 选择文件的变化
- 在函数中,创建 FormData 对象
- 根据接口,拿到接口需要规定的参数名,并将选择的文件添加到 FormData 对象中
核心代码:
Edit/index.tsx 中:
const ProfileEdit = () => {
const onChangePhoto = (e: React.ChangeEvent
const file = e.target.files?.[0];
if (!file) return;
const photoData = new FormData();
photoData.append(‘photo’, file);
};
return (
// …
);
};
31-修改头像-更新头像
目标:能够实现更新头像
步骤:
- 在 Edit 组件中,分发修改头像的 action,传入 FormData 对象,并关闭弹出层
- 根据更新用户头像接口,在 types/data.ts 中,添加接口返回数据的类型
- 在 actions 中,创建修改头像的 action,接收到传递过来的 FormData 对象
- 发送请求,更新用户头像
- 分发 action 来修改 redux 中的头像状态
核心代码:
Edit/index.tsx 中:
import { updateUserPhoto } from ‘@/store/actions’;
const ProfileEdit = () => {
// —-
const onChangePhoto = async () => {
// …
await dispatch(updateUserPhoto(photoData));
onGenderHide();
};
};
types/data.d.ts 中:
export type UserPhotoResponse = ApiResponse<{
photo: string;
}>;
actions/profile.ts 中:
export const updateUserPhoto = (data: FormData): RootThunkAction => {
return async (dispatch) => {
const res = await http.patch
dispatch({
type: ‘profile/update’,
payload: {
photo: res.data.data.photo,
},
});
};
};
补充-JS 调用摄像头的资料:
- js 调用摄像头拍照上传图片
html input[type=file] 的 capture 属性
32-修改生日-展示日期选择器
目标:能够在点击生日时展示日期选择器
步骤:创建状态 showBirthday 用来控制日期选择器的展示或隐藏
- 将 showBirthday 设置为日期选择器的 visible 属性
- 给生日绑定点击事件,在点击事件中修改 showBirthday 值为 true 来展示日期选择器
- 为日期选择器设置 value,值为用户的生日值
- 在日期选择器关闭的回调中,隐藏日期选择器
核心代码:
Edit/index.tsx 中:
const ProfileEdit = () => {
const [showBirthday, setShowBirthday] = useState(false)
const onBirthdayShow = () => {
setShowBirthday(true)
}
const onBirthdayHide = () => {
setShowBirthday(false)
}
return (
// …
生日
value={new Date(birthday)}
onCancel={onBirthdayHide}
title=”选择年月日”
min={new Date(1900, 0, 1, 0, 0, 0)}
max={new Date()}
/>
)
}
33-修改生日-更新生日
目标:能够实现更新生日
步骤:
- 创建 onUpdateBirthday 函数,并设置为日期选择器的 onConfirm 属性值
- 通过该函数的参数拿到日期选择器中选择的日期
- 安装 dayjs ,根据接口来格式化选择的日期为 ‘2018-12-20’
- 为更新用户信息的函数 onUpdateProfile 的 type 参数添加 ‘birthday’ 类型
- 复用 onUpdateProfile 函数来更新用户生日
核心代码:
Edit/index.tsx 中:
import dayjs from ‘dayjs’;
const ProfileEdit = () => {
const onUpdateProfile = async (
type: ‘name’ | ‘intro’ | ‘gender’ | ‘photo’ | ‘birthday’,
value: string,
) => {
// …
};
const onUpdateBirthday = (value: Date) => {
const birthday = dayjs(value).format(‘YYYY-MM-DD’);
onUpdateProfile(‘birthday’, birthday);
onBirthdayHide();
};
return (
// …
);
};
34-退出登录-弹窗确认
目标:能够点击退出按钮时弹窗确认是否退出
分析说明:
不需要自定义样式的情况下,使用 Dialog.confirm 来弹窗确认即可
如果需要自定义弹窗按钮的样式,需要使用 Dialog.show 基础方法来实现
步骤:
- 为退出登录按钮绑定点击事件
- 在点击事件中,使用 Dialog 弹窗让用户确认是否退出登录
核心代码:
Edit/index.tsx 中:
const ProfileEdti = () => {
const onLogout = () => {
const handler = Dialog.show({
title: ‘温馨提示’,
content: ‘亲,你确定退出吗?’,
actions: [
[
{
key: ‘cancel’,
text: ‘取消’,
onClick: () => {
handler.close();
},
},
{
key: ‘confirm’,
text: ‘退出’,
style: {
color: ‘var(—adm-color-weak)’,
},
},
],
],
});
};
return (
// …
);
};
35-退出登录-功能完成
目标:能够实现退出功能
步骤:
- 为退出按钮,绑定点击事件,在点击事件中分发退出 action
- 在 types 中,创建退出登录的 action 类型
- 在 actions/login.ts 中,创建退出 action
- 在退出 action 中,分发退出 action,并清理 token
- 在 reducers 中,处理退出 action 清空 token
- 返回登录页面
核心代码:
Edit/index.tsx 中:
const onLogout = () => {
const handler = Dialog.show({
actions: [
[
{
text: ‘退出’,
onClick: () => {
dispatch(logout());
handler.close();
history.replace(‘/login’);
},
},
],
],
});
// …
};
types/data.d.ts 中:
// 登录 action 类型
export type LoginAction =
| {
type: ‘login/token’;
payload: Token;
}
| {
type: ‘login/logout’;
};
actions/login.ts 中:
export const logout = (): RootThunkAction => {
return async (dispatch) => {
dispatch({ type: ‘login/logout’ });
clearToken();
};
};
reducers/login.ts 中:
const login = () => {
switch (action.type) {
// …
case ‘login/logout’:
return initialState;
}
};
36-封装鉴权路由组件
目标:能够封装鉴权路由组件实现登录访问控制功能
步骤:
- 在 components 目录中创建 AuthRoute 路由组件
- 在 AuthRoute 组件中,实现路由的登录访问控制逻辑
- 未登录时,重定向到登录页面,并传递要访问的路由地址
- 登录时,直接渲染要访问的路由
核心代码:
components/AuthRoute.tsx 中:
// AuthRoute 组件的使用,与 路由自己的 Route 组件用法相同
// 也就是说:Route 能够接受什么属性,AuthRoute 组件也能够接受什么属性
//
import { logout } from ‘@/store/actions/login’;
import { isAuth } from ‘@/utils/token’;
import { useDispatch } from ‘react-redux’;
import { Route, Redirect, RouteProps, useLocation } from ‘react-router-dom’;
/
注意:此处的 children 就是
所以,此处,直接返回 children 即可。因为已经渲染过了内容,所以,此处不需要再通过标签来渲染了
/
export const AuthRoute = ({ children, …rest }: RouteProps) => {
const location = useLocation();
const dispatch = useDispatch();
return (
render={() => {
const isLogin = isAuth();
if (isLogin) {
// 登录
return children;
// return
}
dispatch(logout());
// 未登录
return (
pathname: ‘/login’,
state: {
from: location.pathname,
},
}}
/>
);
}}
/>
);
};
App.tsx 中:
import { AuthRoute } from ‘./components/AuthRoute’;
const App = () => {
return (
// …
// 此处,通过 children 来指定要渲染的组件
);
};
pages/Layout/index.tsx 中:
import { AuthRoute } from ‘@/components/AuthRoute’;
const Layout = () => {
// …
return (
// …
);
};
37-登录时跳转到相应页面
目标:能够在登录时根据重定向路径跳转到相应页面
步骤:
- 在 Login 组件中导入 useLocation 来获取路由重定向时传入的 state
- 调用 useLocation hook 时,指定 state 的类型
- 登录完成跳转页面时,判断 state 是否存在
- 如果存在,跳转到 state 指定的页面
- 如果不存在,默认跳转到首页
核心代码:
pages/Login/index.tsx 中:
import { useLocation } from ‘react-router-dom’;
const Login = () => {
// 注意: state 可能是不存在的,所以,类型中要明确包含不存在的情况,即 undefined
const location = useLocation<{ from: string } | undefined>();
const onFinish = async (values: LoginForm) => {
// …
Toast.show({
afterClose: () => {
if (location.state) {
return history.replace(location.state.from);
}
history.replace(‘/home/index’);
},
});
};
};
补充:Route 组件的 component 和 children 的区别
// 1 使用 component 属性来配置路由
// 2 使用 children 属性来配置路由
// 以上两种方式的区别:
// 相同点:都可以用来配置路由
// 不同点:
// 1 component: 该方式,我们只是把要渲染的组件传递给了 Route 组件,是 Route 组件内部来渲染了组件
// 2 children: 该方式,是我们自己手动渲染的组件(
// 对于 Route 组件来说,如果使用了 component 形式,路由会自动将路由信息通过 props 来传递给组件
// 如果使用了 children 形式,路由不会通过 props 来传递路由信息
补充:Route 如何通过 component/children 来为组件传递了路由信息(属性)?
//
const MyRoute = (component: Component) => {
return
};
/
/
const MyRoute = ({ children }: any) => {
// 通过 React.cloneElement() 方法来为 children 传递属性
// 最终,Layout 组件中,就可以通过 props 来接收到传递的数据了
return React.cloneElement(children, {
a: 1,
b: 3,
…路由信息,
});
};
/
/
// 注意:这种写法是错误的,因为 children 不是一个组件名称,所以,不能当做标签来渲染
// const MyRoute = (children: Children) => {
// return
// }
38-理解无感刷新 token
目标:能够理解什么是无感刷新 token
分析说明:
一般情况下,我们用到的移动端的 App(比如,微信)只要登录过一次,一般就不需要再次重新登录,除非很长时间没有使用过 App。 这是如何做到的呢?这就用到我们要讲的无感刷新 token 了。
我们知道,登录时会拿到一个登录成功的标识 token(身份令牌),有了这个令牌就可以进行登录后的操作了,比如,获取个人资料、修改个人信息等等 但是,为了安全,登录标识 token 一般都是有有效期的,比如,咱们的极客园项目中 token 的有效期是 2 个小时。 如果不进行额外的处理,登录 2 小时以后,就得再次登录。但是,这种用户体验不好,特别是移动端(不管是 App 还是 H5)。 相对来说,更好的用户体验是前面提到的微信的那种方式。它的原理简单来说是这样的:
在登录时,同时拿到两个 token:
1 登录成功的令牌:token2 刷新 token 的令牌:refresh_token
刷新 token 的令牌用来:在 token 过期后,换取新的 token(续费),从而实现“永久登录”效果。 这就是所谓的:无感刷新 token。
39-实现无感刷新 token-换取新的 token
目标:能够实现无感刷新 token 实现自动登录
分析说明:
概述:在登录超时或者 token 失效时,也就是服务器接口返回 401,通过 refresh_token 换取新的 token 过程如下(以获取个人资料数据为例):
【1】发送请求获取个人资料数据
【2】接口返回 401,也就是 token 失效了
【3】在响应拦截器中统一处理,换取新的 token
【4】将新的 token 存储到本地缓存中
【5】继续发送获取个人资料的请求,完成数据获取 - 关键点
【6】如果整个过程中出现了任意异常,一般来说就是 refresh_token 也过期了,换取 token 失败,此时,要进行错误处理,也就是: 清除 token,跳转到登录页面
axios 请求拦截过程说明:
1 axios.get()
2 请求拦截器
3 响应代码
4 响应拦截器
以上 4 个步骤的执行顺序:
【1 axios.get()】 —> 【2 请求拦截器】>>> 服务器处理 >>> 【4 响应拦截器】 —> 【3 响应代码】
步骤:
- 使用 try-catch 处理异常,出现异常时,清除 token,清空 redux token,跳转到登录页面
- 判断本地存储中,是否有 refresh_token
- 如果没有,直接跳转到登录页面,让用户登录即可
- 如果有,就使用 refresh_token 通过 axios 发送请求,换取新的 token
- 将新获取到的 token 存储到本地缓存中和 redux 中
- 继续发送原来的请求
核心代码:
utils/http.ts 中:
// 响应拦截器
http.interceptors.response.use(undefined, async (error) => {
// 响应失败时,会执行此处的回调函数
if (!error.response) {
// 网路超时
Toast.show({
content: ‘网络繁忙,请稍后再试’,
duration: 1000,
});
return Promise.reject(error);
}
// 在此处,通过 refresh_token 来换取新的 token
if (error.response.status === 401) {
try {
// …
// 先判断 redux 中是否有 refresh_token
const { refresh_token } = store.getState().login;
if (!refresh_token) {
// console.log(‘refresh_token 没有了’)
// // 本地没有
// Toast.show({
// content: ‘登录超时,请重新登录’,
// duration: 1000,
// afterClose: () => {
// customHistory.push(‘/login’, {
// from: customHistory.location.pathname
// })
// }
// })
// return Promise.reject(error)
// 1 手动抛出异常
// throw new Error(error)
// 2 因为 try-catch 无法直接捕获 Promise 的异常,所以,此处
// 通过 await 等待 Promise 完成。然后,try-catch 就可以
// 捕获到该异常了
await Promise.reject(error);
}
// 有 refresh_token,就用 refresh_token 换取新的 token
// 注意:
// 1 此处需要使用 axios 发请求
// 2 因为使用的是 axios 所以,此处需要指定完整的 接口路径
// 3 对于 put 请求来来说,第 3 个参数才表示配置项,才能够设置 请求头
// 4 此处的请求头用的是 refresh_token 而不是 token
const res = await axios.put(${baseURL}/authorizations
, null, {
headers: {
Authorization: Bearer ${refresh_token}
,
},
});
// console.log(res)
// 使用新拿到的 token 替换本地的 token 以及 redux 中的 token
// 组装所有 token
const tokens = {
// token 是最新的,接口返回的
token: res.data.data.token,
// 因为接口没有返回新的 refresh_token,所以,需要使用原来的
refresh_token,
};
setToken(tokens);
store.dispatch({ type: ‘login/token’, payload: tokens });
// 继续完成原来要执行的操作
// 比如,在获取个人资料时,token 超时了,最终,在拿到最新的 token 后
// 要继续获取个人资料
// console.dir(error)
// 可以通过 error.config 来拿到原来发送的请求的相关信息
// 所以,要执行原来的操作,只需要将 error.config 重新请求一次即可
// 注意:此处,一定要返回 Promise 的结果
return http(error.config);
} catch (e) {
// 如果换取新 token 的过程中,代码出错了,一般就说明 refresh_token 失效了
// 此时,就清空token然后返回登录页面
// 注意:在直接分发 thunk action 时,会报类型错误
// store.dispatch(logout())
// 解决方式:先自己手动分发对象形式的 action 来实现退出
store.dispatch({ type: ‘login/logout’ });
// 手动清理本地的 token
clearToken();
Toast.show({
content: ‘登录超时,请重新登录’,
duration: 1000,
afterClose: () => {
customHistory.push(‘/login’, {
from: customHistory.location.pathname,
});
},
});
return Promise.reject(error);
}
}
});