Geass's Studio.

从0到1实现useAxios

字数统计: 2k阅读时长: 8 min
2020/04/27 Share

本文首发于 知乎专栏:饿了么大前端

在 React 发布 16.8.0 版本后,Hooks 功能正式启用。这一改变让函数式组件获得了质的飞跃,拥有了如同类组件般处理各种副作用的能力。而自定义 Hooks 的能力,进一步让我们通过 Hooks 封装,进行能力的抽象复用。

今天笔者将带领大家从 0 到 1 实现一个常用的数据请求 Hook —— useAxios。

该 Hook 将具有以下能力:

  1. 全局配置
  2. 手动请求控制
  3. 请求状态管理
  4. 请求取消

为什么要用 Hook

为了说明使用 Hook 的优点,我们通过简单的一个 demo 进行对比。

不使用 Hook:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default () => {
const [data, setData] = useState()
const [loading, setLoading] = useState(false)
const [error, setError] = useState()

useEffect(() => {
setLoading(true);
Axios.get('https://xxx/api/something')
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
}, [/*dependencies*/])
if (loading) { return <div>Loading...</div> }
return error
? <div>{JSON.stringify(error)}</div>
: <div>{JSON.stringify(data)}</div>
}

使用 Hook:

1
2
3
4
5
6
7
export default () => {
const [{ response, loading, error }] = useAxios('https://xxx/api/something')
if (loading) { return <div>Loading...</div> }
return error
? <div>{JSON.stringify(error)}</div>
: <div>{JSON.stringify(data)}</div>
}

在不使用 Hook 的情况下,我们需要对每个请求的状态(成功或失败,请求中与请求完成等)进行管理。而在业务中耦合这部分逻辑,页面代码量增加不说,状态的维护也将增加页面的复杂性。反之通过抽离状态,统一由 Hook 管理,将极大程度减免我们的重复操作和管理成本。

接下来就让我们正式进行该 Hook 的设计

简易 Hook

通过上文两个 demo 的对比,我们已经能初步了解需要抽象的逻辑为请求的状态和数据。抛除展示部分,留下可复用的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
export default (config, dependencies) => {
const [data, setData] = useState()
const [loading, setLoading] = useState(false)
const [error, setError] = useState()

useEffect(() => {
setLoading(true);
Axios.request(config)
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
}, [dependencies])
return [{ data, loading, error }]

抽离视图部分,我们得到的 useAxios v0.1 即实现了一个接收 AxiosRequestConfig 和依赖 dependencies 为参数,返回结果数据和状态的 Hook。

乍一看很简单,然而当我们使用的时候就会发现, 很多时候我们仅仅想定义请求而非实际调用,其次,我们希望拥有手动调用请求的能力,例如在点击事件执行时调用某个方法。而目前的简易 Hook 显然不能达到预期。

添加手动控制

为了手动调用请求,要求我们对外暴露一个请求函数 refresh ;而为了控制是否定义即请求,我们为 Hook 添加一个 trigger 控制变量。进一步,因为传入 AxiosRequestConfig 对象包含了所有依赖的内容,我们去除额外的依赖传入,将 config 对象本身置为依赖对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default (config, options) => {
const [output, setOutput] = useState({ error: undefined, data: undefined, loading: false})

const stringifyConfig = JSON.stringify(config)

const refresh = (overwriteConfig) => {
setOutput({ ...output, loading: true })
return Axios.request({...config, overwriteConfig})
.then(data => setOutput({ ...output, data, loading: false }))
.catch(error => setOutput({ ...output, error, loading: false }))
}
useEffect(() => {
if (options.trigger) {
refresh()
}
}, [stringifyConfig])
return [output, refresh]

在这里有些细节点:为了更好的监测 axios 配置的变更,将配置字符串化作为依赖。同时对于对外暴露的 refresh 函数,传入一个可复写原始配置的 overwrite 参数来增强手动控制的能力。而到了这一步,useAxios 的基本功能基本上已经实现,小伙伴们可以开始使用自己的 Hook 进行 axios 的请求服务了。

添加全局配置

但俗话说贪欲是人类前进的原动力。虽然有了基础功能,但人总是想要更好的。现在的 Hook,功能虽然实现了,但每次都要写完整的 url,baseURL 的配置毫无意义,withCredentials 这些通用的配置每次都要书写一遍。在头疼之余,第一反应是我们是不是能有一个全局配置统一设置呢。

说干就干,全局配置的实现,第一想法是设置一个全局的 axiosInstance 进行存储管理,每次使用都调用同一个实例对象。这的确能完成我们的要求,但是否有更好的方法呢?这时候就要请出 React 中的另一个 Hook —— useContext 隆重登场了。根据 useContext 的定义,能将一系列数据透传给其子节点。如此一来,在根节点处用一个配置 Context 设置 axiosInstance 对象,然后就能在其子节点中获取并使用。

在这里,我们使用 AxiosConfig 对象作为配置 Context 的组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export const AxiosContext = React.createContext(null)

const AxiosConfig = (props) => {
const { config } = props
let axiosInstance

if (config) {
axiosInstance = Axios.create(config)
} else {
axiosInstance = Axios.create()
}

return <AxiosContext.Provider value={{ axiosInstance }}>{props.children}</AxiosContext.Provider>
}

export default AxiosConfig

在代码中通过入参 config 创建一个 axiosInstance 对象,然后通过 Context.Provider 透传给其下所有子组件。接下来,在子组件的 useAxios 里获取对应的实例对象进行调用就 ok 了。这里只列出相关片段代码

1
2
3
4
5
6
7
8
9
10
11
12
const useAxios = (/**/) {
const globalConfig = useContext(AxiosContext) || {}
const axiosInstance = globalConfig.axiosInstance || Axios.create()

// ......
const refresh = () => {
//......
axiosInstance.request() // 使用获取的实例请求数据
// ......
}
// ......
}

这里我们要注意的一点是,因为全局配置仅是一个可选项,所以存在不进行全局配置的情况。因此我们需要做一个兜底方案,就是一个默认配置的 axios 实例。

请求取消

典型的场景为使用全局状态管理,左侧列表右侧详情的页面,当点击左侧列表时将通过一系列接口获取数据详情,当我们快速点击列表时候,往往由于网络请求不稳定而容易产生后请求先到的情况而导致后来先到的情况。具体而言就是列表 A,B,先后快速点击A,B。我们期望的情况是A,B的请求依次顺序结束,最终得到 B 的结果。但现实中常出现最后会得到 A 的结果。为了规避此类情况,我们可以在请求 B 的同时,进行请求 A 的取消。

在 axios 中采用 cancelToken 进行请求取消。在此前配置中,虽然可以通过 AxiosRequestConfig 手动配置 cancelToken 。但本着复用原则,我们可通过配置项进行统一管理,因此我们为 options 添加一个新属性 cancelable ,遵循高级配置默认关闭的原则,默认值为 false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const useAxios = (/**/, options) {
const cancelable = options.cancelable || false
const cancelSource = useRef()
// ......
const refresh = () => {
if (cancelSource.current) {
// 取消上一次请求
cancelSource.current.cancel();
}
cancelSource.current = cancelable ? Axios.CancelToken.source() : undefined
//......
axiosInstance.request({
// ......
cancelToken: (cancelSource.current || {}).token
}) // 使用获取的实例请求数据
// ......
}
// ......
}

在这里,我们通过 useRef 进行了 cancelToken 的保存,以便在下一次渲染时获取上一次的值。

紧接着,在每次重新请求的同时,我们检查 cancelToken 的状态,进行请求的取消。之后对新请求根据配置项决定是否需要一个新的 cancelToken。如此一来便完成了取消请求的配置。

总结

至此,我们便从0开始,一步一步完成了一个 axios 的 hook,从最基本的功能到全局配置,取消请求等,此外还可以添加缓存等一系列功能。可以看到,整个 Hook 的搭建并不是一开始就考虑到各种扩展情况而是仅仅实现了最基础的请求功能,但是留出了扩展的方式。也就是所谓的不过度设计但面向开放而设计才是好设计。接下来就让我们基于此搭建具有自己特色的各种 Hooks 吧。

代码全文在 github 上,欢迎小伙伴提意见和 star ~

CATALOG
  1. 1. 为什么要用 Hook
  • 简易 Hook
  • 添加手动控制
  • 添加全局配置
  • 请求取消
  • 总结