想必经常写 管理后台 调用 HTTP 增删除改查接口的 React 小伙伴们可能对 axios-hooks 已经十分熟悉了,不熟悉的小伙伴们也别着急,其实它就是一个基于 axios 封装的 react hooks 函数调用方法。让我来给你看一段示例(来自 axios-hooks README.md)

// React
import useAxios from 'axios-hooks'

function App() {
  const [{ data, loading, error }, refetch] = useAxios(
    'https://reqres.in/api/users?delay=1'
  )

  if (loading) return <p>Loading...</p>
  if (error) return <p>Error!</p>

  return (
    <div>
      <button onClick={refetch}>refetch</button>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  )
}

通过一个 useAxios 传递接口地址,然后返回接口的 data、loading、error 响应式状态值,由于是响应式的所以能够做到数据变更优雅的刷新页面,再也不用在各种生命周期函数 await 数据然后 setState 了,接口调用超级优雅。

这么优雅的方式怎么能只由 React 独享呢,自从用上 Vue3 的 TSX + 组合式 API 之后腿不酸了腰也不疼了(bushi),那我大 Vue 也必须有,当我有这个想法的时候那肯定有人比我更早想到,果然找了一下发现 VueUse 都已经给我端上来了 @vueuse/integrations/useAxios 这波得表扬一下 @Anthony Fu。我这里也同样贴一下代码(具体代码我就不解释了,毕竟和上面十分有八分神似)

// Vue
import { defineComponent } from 'vue';
import { useAxios } from '@vueuse/integrations/useAxios';

export default defineComponent({
	setup(props, ctx) {
		const { data, isLoading, error, execute } = useAxios('https://reqres.in/api/users?delay=1');

		return () => {
			if (isLoading) return <p>Loading...</p>;
			if (error) return <p>Error!</p>;

			return (
				<div>
					<button onClick={(e) => execute()}>refetch</button>
					<pre>{JSON.stringify(data, null, 2)}</pre>
				</div>
			);
		};
	},
});

从简单的使用角度来说确实已经和 axios-hooks 很接近了,但是(我要说但是了)在一个站点需要对接多个后端的时候,需要初始化多个 axios 实例在不同页面调用不同后端的接口,这个时候传递参数就会比较麻烦,比如需要在每个页面导入以及传递一个 instance 参数

// Vue
import axios from 'axios'
import { useAxios } from '@vueuse/integrations/useAxios';

const instance = axios.create({
	baseURL: 'http://127.0.0.1:8081',
});

const instanceDefault = axios.create({
	baseURL: 'http://127.0.0.1:8082',
});

const a1 = useAxios('/hello1', instance);
const a2 = useAxios('/hello2', instanceDefault);

而在 axios-hooks 中是有一个 makeUseAxios 能够创建多实例的,用多实例的好处就是我们不需要将 instance 在页面之导来导去。

// React
import axios from 'axios'
import { makeUseAxios } from 'axios-hooks'

const useAxiosAPIv1 = makeUseAxios({
  axios: axios.create({ baseURL: 'http://127.0.0.1:8081' })
})

const useAxiosAPIv2 = makeUseAxios({
  axios: axios.create({ baseURL: 'http://127.0.0.1:8082' })
})

const […] = useAxiosAPIv1("/posts")

const […] = useAxiosAPIv2("/users")

虽然说 @vueuse/integrations 没支持这种创建多实例 useAxios 方法,但是我们可以暴改它让他支持上这种方式呀,我先贴一下我的实现然后再详细介绍一下其中的逻辑(当然,我这里有很多实现也是参 axios-hooks 来简单实现的,可能会存在不少的缺陷)

// Vue 
// file: useAxios.ts
import axios, { type AxiosInstance, type AxiosResponse } from 'axios';
import { useAxios as useAxiosHook } from '@vueuse/integrations/useAxios';

type ConfigureUseAxiosOption = {
	instance?: AxiosInstance;
};

function makeUseAxios(option?: ConfigureUseAxiosOption) {
	let defaultInstance = option?.instance ?? axios;

	function useAxios<T = any, R = AxiosResponse<T>, D = any>(...args: any[]) {
		if (args.length > 0) {
			if (args.length == 1) {
				// Example: useAxios("/api") -> useAxios("/api", defaultInstance)
				// function useAxios<T = any, R = AxiosResponse<T>, D = any>(instance?: AxiosInstance): EasyUseAxiosReturn<T, R, D> & Promise<EasyUseAxiosReturn<T, R, D>>;
				// function useAxios<T = any, R = AxiosResponse<T>, D = any>(config?: AxiosRequestConfig<D>): EasyUseAxiosReturn<T, R, D> & Promise<EasyUseAxiosReturn<T, R, D>>;
				if (typeof args[0] !== 'function') {
					args[1] = defaultInstance;
				}
			} else {
				if (typeof args[1] !== 'function') {
					if (typeof args[0] == 'string') {
						if (args.length >= 3) {
							if (typeof args[2] !== 'function') {
								// Example: useAxios("/api", config, options) -> useAxios("/api", config, instance, options)
								// function useAxios<T = any, R = AxiosResponse<T>, D = any, O extends UseAxiosOptionsWithInitialData<T> = UseAxiosOptionsWithInitialData<T>>(url: string, config?: AxiosRequestConfig<D>, options?: O): StrictUseAxiosReturn<T, R, D, O> & Promise<StrictUseAxiosReturn<T, R, D, O>>;
								// function useAxios<T = any, R = AxiosResponse<T>, D = any, O extends UseAxiosOptionsBase<T> = UseAxiosOptionsBase<T>>(url: string, config?: AxiosRequestConfig<D>, options?: O): StrictUseAxiosReturn<T, R, D, O> & Promise<StrictUseAxiosReturn<T, R, D, O>>;
								const options = args[2];
								args[2] = defaultInstance;
								args[3] = options;
							}
							// Example: useAxios("/api", config, instance, options) -> useAxios("/api", config, instance, options)
							// function useAxios<T = any, R = AxiosResponse<T>, D = any, O extends UseAxiosOptionsWithInitialData<T> = UseAxiosOptionsWithInitialData<T>>(url: string, config: AxiosRequestConfig<D>, instance: AxiosInstance, options?: O): StrictUseAxiosReturn<T, R, D, O> & Promise<StrictUseAxiosReturn<T, R, D, O>>;
							// function useAxios<T = any, R = AxiosResponse<T>, D = any, O extends UseAxiosOptionsBase<T> = UseAxiosOptionsBase<T>>(url: string, config: AxiosRequestConfig<D>, instance: AxiosInstance, options?: O): StrictUseAxiosReturn<T, R, D, O> & Promise<StrictUseAxiosReturn<T, R, D, O>>;
						} else if (args.length === 2) {
							// Example: useAxios("/api", config) -> useAxios("/api", config, instance)
							args[2] = defaultInstance;
						}
					}
				}
				// 用户手动指定 instance 无须替换默认实例
				// Example: useAxios("/api", instance);
				// function useAxios<T = any, R = AxiosResponse<T>, D = any, O extends UseAxiosOptionsWithInitialData<T> = UseAxiosOptionsWithInitialData<T>>(url: string, instance?: AxiosInstance, options?: O): StrictUseAxiosReturn<T, R, D, O> & Promise<StrictUseAxiosReturn<T, R, D, O>>;
				// function useAxios<T = any, R = AxiosResponse<T>, D = any, O extends UseAxiosOptionsBase<T> = UseAxiosOptionsBase<T>>(url: string, instance?: AxiosInstance, options?: O): StrictUseAxiosReturn<T, R, D, O> & Promise<StrictUseAxiosReturn<T, R, D, O>>;
				// function useAxios<T = any, R = AxiosResponse<T>, D = any>(config?: AxiosRequestConfig<D>, instance?: AxiosInstance): EasyUseAxiosReturn<T, R, D> & Promise<EasyUseAxiosReturn<T, R, D>>;
			}
		}

		// deprecated: 最初偷懒版本实现
		// if (args.length < 2) args[1] = {}; // set AxiosRequestConfig<D>
		// if (args.length < 3) args[2] = defaultInstance;

		return useAxiosHook<T, R, D>(...args);
	}

	function configure(option?: ConfigureUseAxiosOption) {
		if (option?.instance) defaultInstance = option.instance;
	}

	return Object.assign(useAxios as typeof useAxiosHook, {
		configure,
	});
}

const useAxios = makeUseAxios();
const configure = useAxios.configure;

export default useAxios;
export { useAxios, makeUseAxios, configure };

我们这里也实现了一个 makeUseAxios 函数接受了一个可选的配置项里面可传递一个默认 axios 实例,在 makeUseAxios 函数中我们定义了两个函数 useAxios 和 configure,其中 useAxios 是参考 @vueuse/integrations/useAxios 定义的重写包括泛型和入参以及返回数据的类型,另外就是 configure 函数,其实现就是利用闭包特性修改 defaultInstance 变量,最后通过 Object.assign 将 useAxios 函数以及 configure 函数返回了。

其实在看 axios-hooks 源代码的时候让我感到惊喜的是利用 Object.assign 实现的在函数上挂 configure 函数的操作,如果你对 Object.assign 不是很了解的话,我下面这段代码可能能够帮助你更简单的理解

const callUseX = function () {
    console.log('call', callUseX);
};
const configX = function () {
    console.log('call configX', configX);
};
callUseX['configX'] = configX;

callUseX();
callUseX.configX();

是的,在上面的代码中 callUseX 既可以当作函数直接调用,又可以当作“对象”调用其包含的函数。这个时候你再去理解 JavaScript 中,每个函数实际上都是一个 Function 对象 就很具体了。

再说回在 Vue3 中使用 makeUseAxios 创建多 useAxios 实例的调用方式,我这里简单写了一个测试用例

// Vue
import { useAxios as useAxiosHook, type UseAxiosOptionsBase } from '@vueuse/integrations/useAxios';
import axios, { type AxiosRequestConfig } from 'axios';
import { makeUseAxios } from './useAxios';

const instance = axios.create({
	baseURL: 'http://127.0.0.1:8081',
	withCredentials: false,
});

const instanceDefault = axios.create({
	baseURL: 'http://127.0.0.1:8082',
	withCredentials: false,
});

// use instanceDefault
const useAxios = makeUseAxios({
	instance: instanceDefault,
});

const option: UseAxiosOptionsBase = {
	immediate: true,
};
const config: AxiosRequestConfig = {};

const a1 = useAxios('/hello1', config); // url -> http://127.0.0.1:8082/hello1
const a2 = useAxios('/hello2', instance); // url -> http://127.0.0.1:8081/hello2
const a3 = useAxios('/hello3', config, instance); // url -> http://127.0.0.1:8081/hello3
const a4 = useAxios('/hello4', config, instance, option); // url -> http://127.0.0.1:8081/hello4
const a5 = useAxios('/hello5', config, option); // url -> http://127.0.0.1:8082/hello5
const a6 = useAxiosHook('/hello6', config, option); // url -> /hello6
const a7 = useAxios('/hello7', config, axios, option); // url -> /hello7
const a8 = useAxios('/hello8', instance); // url -> http://127.0.0.1:8081/hello8

好了,恭喜你又学到一个小技巧,记得点赞、转发、评论三连,我们下次见。