My Little World

从 Hooks 的角度去组织异步请求

要定义一个自己的 API Client

封装整个应用中异步请求的一些通过设置,以及统一处理,方便在 Hooks 中使用。

通常来说,会包括以下几个方面:

  1. 一些通用的 Header。比如 Authorization Token。
  2. 服务器地址的配置。前端在开发和运行时可能会连接不同的服务器,比如本地服务器或者测试服务器,此时这个 API Client 内部可以根据当前环境判断该连接哪个 URL。
  3. 请求未认证的处理。比如如果 Token 过期了,需要有一个统一的地方进行处理,这时就会弹出对话框提示用户重新登录。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

import axios from "axios";

// 定义相关的 endpoint
const endPoints = {
test: "https://60b2643d62ab150017ae21de.mockapi.io/",
prod: "https://prod.myapi.io/",
staging: "https://staging.myapi.io/"
};

// 创建 axios 的实例
const instance = axios.create({
// 实际项目中根据当前环境设置 baseURL
baseURL: endPoints.test,
timeout: 30000,
// 为所有请求设置通用的 header
headers: { Authorization: "Bear mytoken" }
});

// 听过 axios 定义拦截器预处理所有请求
instance.interceptors.response.use(
(res) => {
// 可以假如请求成功的逻辑,比如 log
return res;
},
(err) => {
if (err.response.status === 403) {
// 统一处理未授权请求,跳转到登录界面
document.location = '/login';
}
return Promise.reject(err);
}
);

export default instance;

封装远程资源

将通过get方法获取的数据其实就是远程数据源,UI依赖远程数据源渲染

比起在组件内部直接发请求,只是把代码换了个地方,也就是写到了 Hook 里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

// useArticle
import { useState, useEffect } from "react";
import apiClient from "./apiClient";

// 将获取文章的 API 封装成一个远程资源 Hook
const useArticle = (id) => {
// 设置三个状态分别存储 data, error, loading
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
// 重新获取数据时重置三个状态
setLoading(true);
setData(null);
setError(null);
apiClient
.get(`/posts/${id}`)
.then((res) => {
// 请求成功时设置返回数据到状态
setLoading(false);
setData(res.data);
})
.catch((err) => {
// 请求失败时设置错误状态
setLoading(false);
setError(err);
});
}, [id]); // 当 id 变化时重新获取数据

// 将三个状态作为 Hook 的返回值
return {
loading,
error,
data
};
};

// 使用


import useArticle from "./useArticle";

const ArticleView = ({ id }) => {
// 将 article 看成一个远程资源,有 data, loading, error 三个状态
const { data, loading, error } = useArticle(id);
if (error) return "Failed.";
if (!data || loading) return "Loading...";
return (
<div className="exp-09-article-view">
<h1>
{id}. {data.title}
</h1>
<p>{data.content}</p>
</div>
);
};

有了这样一个 Hook,React 的函数组件几乎不需要有任何业务的逻辑,而只是把数据映射到 JSX 并显示出来就可以了,在使用的时候非常方便。

在项目中,可以把每一个 Get 请求都做成这样一个 Hook。
数据请求和处理逻辑都放到 Hooks 中,从而实现 Model 和 View 的隔离,
不仅代码更加模块化,而且更易于测试和维护。

这样做是为了保证每个 Hook 自身足够简单。

一般来说,为了让服务器的返回数据满足 UI 上的展现要求,通常需要进一步处理。
而这个对于每个请求的处理逻辑可能都不一样,通过一定的代码重复,能够避免产生太复杂的逻辑。

同时呢,某个远程资源有可能是由多个请求组成的,那么 Hooks 中的逻辑就会不一样,因为要同时发出去多个请求,组成 UI 展现所需要的数据。
所以,将每个 Get 请求都封装成一个 Hook ,也是为了让逻辑更清楚。

这个模式仅适用于 Get 请求的逻辑,对于其它类型,可以使用 useAsync 这样一个自定义的 Hook,
同样也是用 Hook 的思想,把请求的不同状态封装成了一个数据源供组件使用。

处理并发或串行请求

从状态变化的角度去组织异步调用。

函数组件的每一次 render,其实都提供了我们根据状态变化执行不同操作的机会,
就是利用这个机制,通过不同的状态组合,来实现异步请求的逻辑

利用状态的组合变化来实现并发和串行请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// useArticle 同上
// useUser
import { useState, useEffect } from "react";
import apiClient from "./apiClient";

export default (id) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
// 当 id 不存在,直接返回,不发送请求
if (!id) return;
setLoading(true);
setData(null);
setError(null);
apiClient
.get(`/users/${id}`)
.then((res) => {
setLoading(false);
setData(res.data);
})
.catch((err) => {
setLoading(false);
setError(err);
});
}, [id]);
return {
loading,
error,
data
};
};

// 应用

import { useState } from "react";
import CommentList from "./CommentList";
import useArticle from "./useArticle";
import useUser from "./useUser";
import useComments from "./useComments";

const ArticleView = ({ id }) => {
const { data: article, loading, error } = useArticle(id);
const { data: comments } = useComments(id);
const { data: user } = useUser(article?.userId);
if (error) return "Failed.";
if (!article || loading) return "Loading...";
return (
<div className="exp-09-article-view">
<h1>
{id}. {article.title}
</h1>
{user && (
<div className="user-info">
<img src={user.avatar} height="40px" alt="user" />
<div>{user.name}</div>
<div>{article.createdAt}</div>
</div>
)}
<p>{article.content}</p>
<CommentList data={comments || []} />
</div>
);
};

export default () => {
const [id, setId] = useState(1);
return (
<div className="exp-09-article-view-wrapper">
<ul>
<li onClick={() => setId(1)}>Article 1</li>
<li onClick={() => setId(2)}>Article 2</li>
<li onClick={() => setId(3)}>Article 3</li>
<li onClick={() => setId(4)}>Article 4</li>
<li onClick={() => setId(5)}>Article 5</li>
</ul>
<ArticleView id={id} />
</div>
);
};

useArticle 和 useComments属于并发请求;useArticle 和 useUser属于串行请求,

之所以useUser能在article数据变化时重新执行,是因为在useUser的hook里面用useEffect做了依赖

或者换个角度想,把三个钩子的逻辑全部都写在应用代码里,不再封装起来,明显可以看到,
因为useEffect的原因,可以使获取user信息的请求,在拿到article数据后再执行。