01-页面结构

目标:能够根据模板搭建个人中心页面结构
步骤

  1. 根据模板修改 Profile 页面结构

    02-获取用户信息并展示

    该功能分为以下几步来实现:

  2. 发送请求获取用户信息

  3. 页面分发获取用户信息的 action
  4. 将用户信息存储到 redux 中
  5. 获取用户信息并展示

    03-用户信息-发送请求获取数据

    目标:能够发送请求获取用户信息
    步骤

  6. 根据接口准备所需类型

  7. 创建 actions/profile.ts 文件
  8. 创建获取用户信息的 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(‘/user’);
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 中
步骤

  1. 准备保存状态到 redux 的 action 类型
  2. 在获取个人信息的 action 中将用户信息存储到 redux 中
  3. 创建 reducers/profile.ts,并完成存储用户信息的功能
  4. 在 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(‘/user’);
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’];
步骤

  1. 导入 useSelector
  2. 调用 useSelector 获取 user 状态
  3. 从 user 对象中解构出用户数据并展示在页面中

核心代码
import { useSelector } from ‘react-redux’;
import { RootState } from ‘@/types/store’;

const Profile = () => {
// const { user } = useSelector(state => state.profile)
const { user } = useSelector((state: RootState) => state.profile);

const { photo, name, like_count, follow_count, fans_count, art_count } = user;

// …
// 在 JSX 中渲染从 redux 中拿到的状态数据即可
};

07-封装 axios 响应工具类型

目标:能够封装工具类型统一处理 axios 的响应类型
分析说明
对于项目接口来说,任何一个接口返回的顶层数据格式都是一样的,都有:

  1. 都有 message 属性,类型为 string (固定)
  2. 都有 data 属性,但是具体是什么类型不确定(变化)

// 登录
type LoginResponse = {
message: string;
data: Token;
};
// 用户
type UserResponse = {
message: string;
data: User;
};
问题:TS 类型中,什么类型可以配合多种类型使用,并且可以在使用时指定明确类型?泛型
步骤

  1. 在 types/data.d.ts 中,创建泛型工具类型 ApiResponse
  2. 通过该泛型工具类型,统一处理 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-个人信息-页面结构

目标:能够根据模板展示个人信息页面
步骤

  1. 将 Edit 模板拷贝到 Profile 目录中
  2. 在 App.tsx 中配置个人信息页面的路由
  3. 在 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-个人信息-获取并展示个人信息

目标:能够获取并展示编辑时的个人信息
步骤

  1. 在 types/data.d.ts 中,根据接口准备好返回数据类型
  2. 在 actions/profile.ts 中,创建获取编辑时个人信息的 action
  3. 在 types/store.d.ts 中,创建相应的 redux action 类型
  4. 在 actions 中分发修改 redux 状态的 action
  5. 在 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(‘/user/profile’);
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 (
// …
arrow
extra={
{intro}
}
>
{intro || ‘未填写’}

);
};

10-自定义 hooks

目标:能够知道什么是自定义 hooks
分析说明
除了使用 React 提供的 hooks 之外,开发者还可以创建自己的 hooks,也就是自定义 hooks
问题:为什么要创建自定义 hooks?
回答:实现状态逻辑复用,也就是将与状态相关的逻辑代码封装到一个函数中,哪个地方用到了,哪个地方调用即可
封装自定义 hook 与封装普通函数的区别:是否包含了状态逻辑,如果需要包含 React 状态逻辑,那么,就得用自定义 hook;而如果封装的内容不涉及状态,此时,就用普通的函数封装即可。
如何理解状态逻辑?可以简单的理解为代码中是否包含 React hooks,比如,useState、useEffect、useRef 等,那么就得使用自定义 hook 来封装
自定义 hooks 的特点:

  1. 名称必须以 use 开头
  2. 和内置的 React Hooks 一样,自定义 hook 也是一个函数
  • React Hooks(不管是内置的还是自定义的)只能在函数组件或其他自定义 hook 内使用

// 创建自定义 hooks 函数
const useXxx = (params) => {
// …
// 需要复用的状态逻辑代码
// …

return xxx
}

// 使用自定义 hooks 函数
const Hello = () => {
const xxx = useXxx(…)
}

  • 说明:自定义 hooks 就是一个函数,可以完全按照对函数的理解,来理解自定义 hooks
    • 比如:参数和返回值都可以可选的,可以提供也可以不提供,根据实际的需求来实现即可

总结

  1. 自定义 hooks 是函数吗? 是
  2. 自定义 hooks 的名称有什么约束?必须以 use 开头
  3. 自定义 hooks 可以没有参数或返回值吗?可以

    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;

    // …
    };
    分析以上【我的页面】和【个人信息页面】中获取数据的逻辑,有两个不同点:

  4. 分发的 action 函数不同

  5. 获取的状态不同

所以,只需要把这两点作为自定义 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
步骤

  1. 创建 utils/use-initial-state.ts 文件
  2. 创建 useInitialState 函数(自定义 hook)
  3. 导入用到的包
  4. 将相同的逻辑,直接封装到 useInitialState 函数中
  5. 将不同的地方,作为 useInitialState 函数的参数
  6. 将拿到的状态作为 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 来说,只需要为参数指定类型即可

  1. 参数 action:就是一个函数,所以,直接指定为最简单的函数类型即可
  2. 参数 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 = (action: () => void, stateName: StateName) {}

// — 对比以上两种方式的区别 —
// 1 这种方式:在调用该函数时,最终得到的返回值类型:Token | ProfileState | B 也就是将所有可能出现的情况都列举出来了
const useInitialState = (action: () => void, stateName: keyof RootState) {}

// 2 这种方式:在调用该函数时,最终得到的返回值类型:是某一个状态的类型,这个类型由我们传入的 stateName 类决定
// 比如,useInitialState(getUser, ‘profile’) 返回值类型,就是 profile 这个键对应的类型:ProfileState
const useInitialState = (action: () => void, stateName: StateName) {}

// 调用:
useInitialState(getUser, ‘login’)
useInitialState(getUser, ‘profile’)
useInitialState(getUser, ‘a’)

// 原来讲过的泛型基础:
function id(value: Type): Type {
return value
}

id(10)
// 省略类型不写:
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 改造获取个人信息功能
步骤

  1. 导入自定义 hook
  2. 调用自定义 hook,并传入相应的参数,验证是否有明确的类型提示
  3. 通过返回值,拿到对应的状态

核心代码
Profile/Edit/index.tsx 中:
import { useInitialState } from ‘@/utils/use-initial-state’;

const ProfileEdit = () => {
const { userProfile } = useInitialState(getUserProfile, ‘profile’);

// …
};

15-修改昵称-渲染修改昵称组件

目标:能够根据模板渲染修改昵称组件
步骤

  1. 在 Edit 目录中创建 components 文件夹
  2. 将准备好的修改昵称的模板(EditInput)拷贝到 components 目录中
  3. 从 antd-mobile 中导入 Popup 组件
  4. 导入修改昵称组件,在 Popup 组件中渲染

核心代码
Edit/index.tsx 中:
import { Popup } from ‘antd-mobile’;
import EditInput from ‘./components/EditInput’;

const ProfileEdit = () => {
return (
// …



);
};

16-修改昵称-展示修改昵称组件

目标:能够在点击修改昵称时展示弹出层
步骤

  1. 准备控制弹出层展示或隐藏的状态 inputVisible,默认值为 false
  2. 使用状态 inputVisible 控制 Popup 组件的展示和隐藏
  3. 给昵称绑定点击事件
  4. 在点击事件中修改状态 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 子组件来隐藏弹出层,也就是在子组件中修改父组件中的状态
所以,此处通过子到父的组件通讯来解决
步骤

  1. 在 Edit 组件中,创建一个控制弹出层隐藏的函数
  2. 将该函数作为属性传递给子组件 EditInput
  3. 在 EditInput 子组件中,通过 props 接收该属性
  4. 为 EditInput 组件的 props 指定类型
  5. 在点击导航栏返回按钮时,调用该关闭函数即可

核心代码
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-修改昵称-弹出层显示昵称

目标:能够在弹出层文本框中展示昵称
步骤

  1. 为 EditInput 组件添加 value 属性,用于接收昵称
  2. 在 Edit 组件中将昵称传递给 EditInput 组件
  3. 在 EditInput 组件中创建一个状态,默认值为接收到的昵称
  4. 为 Input 组件设置 value 来展示昵称
  5. 为 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 父组件
步骤

  1. 为 EditInput 组件添加 onUpdateName 函数属性
  2. 在 Edit 组件中为 EditInput 组件传递 onUpdateName 属性
  3. 为提交按钮绑定点击事件
  4. 在点击事件中,调用父组件传递过来的 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 (
// …
right={

提交

}
>
编辑昵称

);
};

20-修改昵称-发送请求

目标:能够修改用户昵称
分析说明

  1. 该接口没有返回值,所以,不需要额外定义接口返回数据的类型
  2. 不管修改昵称还是简介,都是通过同一个接口来修改的,因此,可以通过传入不同的参数来复用该 action

步骤

  1. 创建 updateUserProfile action
  2. 为该 action 指定参数,参数类型为要修改的用户信息
  3. 发送请求修改用户数据

核心代码
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 状态

目标:能够更新用户昵称状态
步骤

  1. 在 types/store.d.ts 中添加更新用户个人资料的 action 类型
  2. 在 actions 中分发 action 以更新用户昵称状态
  3. 在 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-修改简介-复用修改昵称弹出层

目标:能够复用修改昵称弹出层
分析说明
修改简介弹出层有两种实现方式:

  1. 创建一个新的弹出层,也就是参考修改昵称的弹出层重写一遍
  2. 复用修改昵称的弹出层

此处,我们选择复用修改昵称的弹出层组件。
要复用同一个组件,就需要区分出点击的是昵称还是简介,此处,可以通过状态来区分,修改如下:
// 原来:
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,
});
步骤

  1. 修改控制昵称弹出层状态的结构
  2. 修改昵称弹出层的展示、隐藏操作
  3. 修改控制弹出层的 visible 属性值
  4. 修改传递给 EditInput 组件的 value 属性
  5. 为 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-修改简介-展示修改简介弹出层

目标:能够展示修改简介弹出层内容
步骤

  1. 给简介绑定点击事件
  2. 在点击事件中,设置为简介的信息
  3. 在 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 ? ‘昵称’ : ‘简介’}


{isName ? (

placeholder=”请输入”
value={inputValue}
onChange={setInputValue}
/>

) : (
className=”textarea”
placeholder=”请输入”
// 展示:右下角的字数统计
showCount
// 指定内容最大长度
maxLength={100}
// 指定 文本域 展示内容的行数(文本域高度)
rows={4}
value={inputValue}
onChange={setInputValue}
/>
)}


);
};

25-修改简介-弹出层中展示简介内容

目标:能够在复用修改昵称弹出层后正确展示简介内容
分析说明
问题:弹出层中昵称和简介之间会相互影响 操作如下:

  1. 先点击昵称,弹出层中展示昵称内容【正常】
  2. 关闭弹出层后,再点击简介,此时,弹出层中展示的仍然是昵称内容【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 值即可。

  1. 通过 useEffect 监听 props.value 变化

EditInput/index.tsx 中:
useEffect(() => {
// value 为 null 或 undefined 时,设置为默认值为空字符串
setInputValue(value ?? ‘’);
}, [value]);

  1. 为 Popup 组件添加 destroyOnClose 属性

Edit/index.tsx 中:
// 表示在关闭弹出层时,销毁组件内容
destroyOnClose
>

  1. 利用特殊的 key 属性
    • React 内部 diff 时,首先判断 key 是否相同,key 不同直接重新渲染该组件

Edit/index.tsx 中:


25-修改简介-复用提交时的回传逻辑

目标:能够在点击提交时复用修改昵称的回传逻辑
分析说明
复用时,只需额外的提供当前的 type 类型即可
type Props = {
onUpdateName: (name: string) => void;
};

type Props = {
onUpdateProfile: (type: ‘name’ | ‘intro’, value: string) => void;
};
步骤

  1. 修改 EditInput 组件回传数据的函数属性类型
  2. 修改提交按钮中,调用回传数据函数的参数
  3. 修改 Edit 组件中传递给 EditInput 的属性
  4. 修改 Edit 组件中更新用户信息的函数参数
  5. 关闭弹出层

核心代码
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-修改性别-渲染修改性别弹出层

目标:能够显示修改性别弹出层
步骤

  1. 将准备好的修改性别的模板(EditList)拷贝到 components 目录中
  2. 导入修改性别组件,在 Popup 组件中渲染

核心代码
Edit/index.tsx 中:
import EditList from ‘./components/EditList’;

const ProfileEdit = () => {
return (
// …



);
};

27-修改性别-修改性别弹出层的展示或隐藏

目标:能够控制修改性别弹出层的展示或隐藏
分析说明
修改性别和修改头像的弹出层内容几乎是一样的,因此,也可以复用同一个弹出层组件
因此,接下来要从复用的角度,设计修改性别弹出层的逻辑(可以参考刚刚实现的修改昵称和简介)
步骤

  1. 准备用于控制修改性别弹出层的状态
  2. 为性别添加点击事件,在点击事件中修改状态进行展示
  3. 创建隐藏弹出层的控制函数,在点击遮罩时关闭弹出层,并传递给 EditList 组件
  4. 为 EditList 组件添加 props 类型,并接收隐藏函数
  5. 为取消按钮添加点击事件来触发隐藏弹出层

核心代码
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 (
// …
onClick={onGenderShow}
>
性别





)
}
EditList/index.tsx 中:
type Props = {
onClose: () => void;
};

const EditList = ({ onClose }: Props) => {
return (


取消

);
};

28-修改性别-更新数据

目标:能够实现修改性别功能
步骤

  1. 为 EditList 组件添加 type 和 onUpdateProfile 属性及其类型
  2. 为男/女列表项绑定点击事件
  3. 在点击事件中拿到当前当前点击项的 value 值
  4. 调用 onUpdateProfile 回传数据给父组件
  5. 在父组件 Edit 中,为 EditList 组件指定 onUpdateProfile,值为 onUpdateProfile 函数
  6. 关闭修改弹出层

核心代码
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 (


onItemClick(‘0’)}>


onItemClick(‘1’)}>



);
};
Edit/index.tsx 中:
const ProfileEdit = () => {
const onUpdateProfile = () => {
// …
onGenderHide();
};

return (
// …
type={listPopup.type}
// 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;
步骤

  1. 为头像列表项绑定点击事件
  2. 在点击事件中,展示对应弹出层
  3. 在 EditList 组件中,创建性别和头像对应的列表数据
  4. 根据当前传入的 type 属性,决定要渲染哪种列表数据
  5. 根据 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) => (
className=”list-item”
key={item.text}
onClick={() => {
if (type === ‘’) return;
onUpdateProfile(type, item.value);
}}
>
{item.text}

))}


取消


);
};

29-修改头像-弹窗选择图片

目标:能够在点击拍照或本地选择时弹窗选择图片
分析说明
修改头像的逻辑与修改性别的其他用户信息的逻辑不同:

  1. 修改性别的其他用户信息:点击男/女或者提交按钮时,直接发送请求,修改后端数据即可
  2. 修改头像:点击拍照或本地选择时,弹窗让用户选择图片(不考虑拍照),等用户选择图片后,才会发送请求,修改后台数据

因此,需要单独处理修改头像的逻辑。
此时,就会有一个新的问题:如何在点击拍照或本地选择时,弹窗让用户选择图片?
我们知道,HTML 中的 input[type=file] 标签,可以实现图片选择。所以,此处需要用到 file 标签。
但是,点击项并不是 file,因此,可以转换下思路:在点击拍照或本地选择时,触发 file 的点击即可。
// 假设 file 就是 input[type=file] 对应的 DOM 对象:
file.click();
步骤

  1. 在 Edit 组件中,创建一个 input[type=file] 标签,并且设置为 hidden(隐藏该标签)
  2. 创建一个 ref 对象,来拿到 file 标签
  3. 在 onUpdateProfile 回调中,判断类型是否为 photo
  4. 如果是,触发 file 的点击
  5. 如果不是,继续执行原来的逻辑即可

核心代码
Edit/index.tsx 中:
const ProfileEdit = () => {
const fileRef = useRef(null)

const onUpdateProfile = async (
type: ‘name’ | ‘intro’ | ‘gender’ | ‘photo’,
value: string
) => {
if (type === ‘photo’) {
fileRef.current?.click()
} else {
// … 原来的逻辑
}
}

return (
// …





)
}

30-修改头像-组装修改头像的数据

目标:能够组装修改头像需要的数据
步骤

  1. 创建函数,监听 input[type=file] 选择文件的变化
  2. 在函数中,创建 FormData 对象
  3. 根据接口,拿到接口需要规定的参数名,并将选择的文件添加到 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-修改头像-更新头像

目标:能够实现更新头像
步骤

  1. 在 Edit 组件中,分发修改头像的 action,传入 FormData 对象,并关闭弹出层
  2. 根据更新用户头像接口,在 types/data.ts 中,添加接口返回数据的类型
  3. 在 actions 中,创建修改头像的 action,接收到传递过来的 FormData 对象
  4. 发送请求,更新用户头像
  5. 分发 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(‘/user/photo’, data);

dispatch({
type: ‘profile/update’,
payload: {
photo: res.data.data.photo,
},
});
};
};
补充-JS 调用摄像头的资料:

  1. js 调用摄像头拍照上传图片
  2. html input[type=file] 的 capture 属性

    32-修改生日-展示日期选择器

    目标:能够在点击生日时展示日期选择器
    步骤

  3. 创建状态 showBirthday 用来控制日期选择器的展示或隐藏

  4. 将 showBirthday 设置为日期选择器的 visible 属性
  5. 给生日绑定点击事件,在点击事件中修改 showBirthday 值为 true 来展示日期选择器
  6. 为日期选择器设置 value,值为用户的生日值
  7. 在日期选择器关闭的回调中,隐藏日期选择器

核心代码
Edit/index.tsx 中:
const ProfileEdit = () => {
const [showBirthday, setShowBirthday] = useState(false)

const onBirthdayShow = () => {
setShowBirthday(true)
}
const onBirthdayHide = () => {
setShowBirthday(false)
}

return (
// …

生日


visible={showBirthday}
value={new Date(birthday)}
onCancel={onBirthdayHide}
title=”选择年月日”
min={new Date(1900, 0, 1, 0, 0, 0)}
max={new Date()}
/>
)
}

33-修改生日-更新生日

目标:能够实现更新生日
步骤

  1. 创建 onUpdateBirthday 函数,并设置为日期选择器的 onConfirm 属性值
  2. 通过该函数的参数拿到日期选择器中选择的日期
  3. 安装 dayjs ,根据接口来格式化选择的日期为 ‘2018-12-20’
  4. 为更新用户信息的函数 onUpdateProfile 的 type 参数添加 ‘birthday’ 类型
  5. 复用 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 基础方法来实现
步骤

  1. 为退出登录按钮绑定点击事件
  2. 在点击事件中,使用 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-退出登录-功能完成

目标:能够实现退出功能
步骤

  1. 为退出按钮,绑定点击事件,在点击事件中分发退出 action
  2. 在 types 中,创建退出登录的 action 类型
  3. 在 actions/login.ts 中,创建退出 action
  4. 在退出 action 中,分发退出 action,并清理 token
  5. 在 reducers 中,处理退出 action 清空 token
  6. 返回登录页面

核心代码
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-封装鉴权路由组件

目标:能够封装鉴权路由组件实现登录访问控制功能
步骤

  1. 在 components 目录中创建 AuthRoute 路由组件
  2. 在 AuthRoute 组件中,实现路由的登录访问控制逻辑
  3. 未登录时,重定向到登录页面,并传递要访问的路由地址
  4. 登录时,直接渲染要访问的路由

核心代码
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 (
{…rest}
render={() => {
const isLogin = isAuth();
if (isLogin) {
// 登录
return children;
// return
}

dispatch(logout());
// 未登录
return (
to={{
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-登录时跳转到相应页面

目标:能够在登录时根据重定向路径跳转到相应页面
步骤

  1. 在 Login 组件中导入 useLocation 来获取路由重定向时传入的 state
  2. 调用 useLocation hook 时,指定 state 的类型
  3. 登录完成跳转页面时,判断 state 是否存在
  4. 如果存在,跳转到 state 指定的页面
  5. 如果不存在,默认跳转到首页

核心代码
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 响应代码】
步骤

  1. 使用 try-catch 处理异常,出现异常时,清除 token,清空 redux token,跳转到登录页面
  2. 判断本地存储中,是否有 refresh_token
  3. 如果没有,直接跳转到登录页面,让用户登录即可
  4. 如果有,就使用 refresh_token 通过 axios 发送请求,换取新的 token
  5. 将新获取到的 token 存储到本地缓存中和 redux 中
  6. 继续发送原来的请求

核心代码
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);
}
}
});