useFetch
效果
使用
1. 基本使用
ts
import { useFetch } from '@vueuse/core'
const { isFetching, error, data } = useFetch(url)
异步使用
ts
const { isFetching, error, data } = await useFetch(url)
手动触发
ts
const { execute } = useFetch(url, { immediate: false })
execute()
请求拦截器
ts
const { data } = useFetch(url, {
async beforeFetch({ url, options, cancel }) {
const myToken = await getMyToken()
if (!myToken)
cancel()
options.headers = {
...options.headers,
Authorization: `Bearer ${myToken}`,
}
return {
options,
}
},
})
响应拦截器
ts
const { data } = useFetch(url, {
afterFetch(ctx) {
if (ctx.data.title === 'HxH')
ctx.data.title = 'Hunter x Hunter' // Modifies the response data
return ctx
},
})
事件
ts
const { data: a, onFetchResponse } = useFetch(url)
onFetchResponse((response) => {
response.json().then(res => {
console.log(res)
})
})
思路
合并 defaultConfig 与用户配置和其他细项不再多说,重点是 同步获取数据,请求拦截,响应拦截,事件触发
这四个方面
使用 await 同步请求数据
🚀
await
是一个关键字, 它表示等待一个表达式, 返回一个 Promise
对象
🔥🔥🔥其实可以使用普通函数模拟
ts
function a(x): PromiseLike<any> {
return {
then: function (onfulfilled): Promise<any> {
return Promise.resolve(onfulfilled(x));
}
}
}
async function b() {
return await a(2)
}
let r = b();
r.then(res => {
console.log(res)
})
能够使用 await
的条件是需要函数 返回一个以 then
为key
值,value
为返回一个 Promise 的函数的对象
由于请求是异步方法,需要等到 请求完成之后 才可以触发 Promise
的 resolve / reject 方法,需要 waitUntilFinished
来在合适的时机触发
ts
// 配合使用
// return {
// then(onFulfilled: any, onRejected: any) {
// return waitUntilFinished().then(onFulfilled, onRejected);
// },
// }
function until(r: any, v: boolean) {
return new Promise<void>((resolve) => {
watch(
r,
() => {
if (v == r.value) {
resolve();
}
},
{
immediate: true,
}
);
});
}
function waitUntilFinished() {
return new Promise<UseFetchReturn<T>>((resolve, reject) => {
until(isFinished, true)
.then(() => resolve(shell))
.catch((error: Error) => reject(error));
});
}
waitUntilFinished
名如其人 等待直到完成
核心是 until
函数, 使用 vue
提供的watch
,只有当 unitl
的两个参数相等时,resolve
才会触发,resolve
触发之后,waitUntilFinished
才会返回 shell
请求拦截/响应拦截
在请求拦截是发生在请求响应之前,可以合并参数,在响应拦截中可以对数据进行处理
ts
if (config.beforeFetch) {
Object.assign(context, await config.beforeFetch(context));
}
if (config.afterFetch) {
// 把 json 之后的数据和 没有 json 后的数据 统一传入
// 返回 数据解构 data
({ data: responseData } = await config.afterFetch({
data: responseData,
response: fetchResponse,
}));
}
事件触发
使用了发布订阅创建一个工厂函数 createEventHook
ts
function createEventHook<T = any>(): EventHook<T> {
const fns: Set<(param: T) => void> = new Set();
const off = (fn: (param: T) => void) => {
fns.delete(fn);
};
const on = (fn: (param: T) => void) => {
fns.add(fn);
// 切面
const offFn = () => off(fn);
return {
off: offFn,
};
};
const trigger = (param: T) => {
return Promise.all(Array.from(fns).map((fn) => fn(param)));
};
return {
on,
off,
trigger,
};
}
所以使用 createEventHook
创建事件
ts
const responseEvent = createEventHook<Response>();
// 返回给外界
const shell = {
onFetchResponse: responseEvent.on,
}
// 当数据请求完成之后
responseEvent.trigger(responseData);
源码
ts
import {
computed,
ComputedRef,
isRef,
ref,
Ref,
shallowRef,
toRaw,
watch,
} from "vue";
import { EventHookOn, createEventHook } from "utils/createEventHook";
import {
BeforeFetchContext,
DataType,
fetchConfig,
HttpMethod,
UseFetchOptions,
} from "./types";
function containsProp(obj: object, ...props: string[]) {
return props.some((k) => k in obj);
}
function isFetchOptions(obj: object): obj is UseFetchOptions {
return (
obj &&
containsProp(
obj,
"immediate",
"refetch",
"initialData",
"timeout",
"beforeFetch",
"afterFetch",
"onFetchError",
"fetch"
)
);
}
function headersToObject(headers: HeadersInit | undefined) {
if (typeof Headers !== "undefined" && headers instanceof Headers)
return Object.fromEntries([...headers.entries()]);
return headers;
}
const resolveUnref = <T>(val: Ref<T> | T): T => {
return unref(val);
};
export type MaybeComputedRef<T> = ComputedRef<T> | T | Ref<T>;
interface UseFetchReturn<T> {
/**
* Indicates if the fetch request has finished
*/
isFinished: Ref<boolean>;
/**
* The statusCode of the HTTP fetch response
*/
statusCode: Ref<number | null>;
/**
* The raw response of the fetch response
*/
response: Ref<Response | null>;
/**
* Any fetch errors that may have occurred
*/
error: Ref<any>;
/**
* The fetch response body on success, may either be JSON or text
*/
data: Ref<T | null>;
/**
* Indicates if the request is currently being fetched.
*/
isFetching: Ref<boolean>;
/**
* Abort the fetch request
*/
abort: () => void;
execute: (throwOnFailed?: boolean) => Promise<any>;
onFetchResponse: EventHookOn<Response>;
onFetchError: EventHookOn;
onFetchFinally: EventHookOn;
get(): UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>>;
post(
payload?: MaybeComputedRef<unknown>,
type?: string
): UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>>;
// type
json<JSON = any>(): UseFetchReturn<JSON> & PromiseLike<UseFetchReturn<JSON>>;
}
const payloadMapping: Record<string, string> = {
json: "application/json",
text: "text/plain",
};
export function useFetch<T>(
url: string,
args?: Partial<UseFetchOptions>
): UseFetchReturn<T> & PromiseLike<UseFetchReturn<T>> {
let config: UseFetchOptions & fetchConfig = {
data: {},
method: "GET",
type: "text",
payload: undefined as unknown,
immediate: true,
refetch: false,
};
// 与用户 传入的类型进行合并
if (args && isFetchOptions(args)) {
config = { ...config, ...args };
}
/**
* @description 是否在 loading 状态
* @param {boolean} isLoading
*/
const loading = (isLoading: boolean) => {
isFetching.value = isLoading;
isFinished.value = !isLoading;
};
let controller: AbortController | undefined;
/**
* @description abort 取消请求,在 fetch 的 signal属性 注入controller.signal
*/
const abort = () => {
controller?.abort();
controller = new AbortController();
controller.signal.onabort = () => (aborted.value = true);
config = {
...config,
signal: controller.signal,
};
};
/** @type {EventHook<Response>} responseEvent,事件触发 */
const responseEvent = createEventHook<Response>();
const errorEvent = createEventHook<any>();
const finallyEvent = createEventHook<any>();
const aborted = ref(false);
const statusCode = ref<number | null>(null);
const response = shallowRef<Response | null>(null);
const error = shallowRef<any>(null);
const data = shallowRef(config.data || {});
const isFinished = ref(false);
const isFetching = ref(false);
let execute = async (throwOnFailed = false) => {
loading(true);
error.value = null;
statusCode.value = null;
aborted.value = false;
data.value = null;
if (config.payload) {
if (config.payloadType) {
(config.headers as any)["Content-Type"] =
payloadMapping[config.payloadType] ?? config.payloadType;
}
const payload = resolveUnref(config.payload);
// 如果 payloadType 时 json 就先 JSON.stringify
config.body =
config.payloadType === "json"
? JSON.stringify(payload)
: (payload as BodyInit);
}
let isCanceled = false;
const context: BeforeFetchContext = {
url: resolveUnref(url),
options: config,
cancel: () => {
isCanceled = true;
},
};
// 把用户的与当前的 context 合并
if (config.beforeFetch) {
Object.assign(context, await config.beforeFetch(context));
}
// 如果调用了 cancel 函数,就停止
if (isCanceled || !fetch) {
loading(false);
return Promise.resolve(null);
}
/** @type {json / text} 格式化后的真实数据 */
let responseData: any = null;
return new Promise<Response | null>((resolve, reject) => {
fetch(context.url, {
...context.options,
headers: {
...headersToObject(config.headers),
...headersToObject(context.options?.headers),
},
})
.then(async (fetchResponse) => {
// 保留初始数据
response.value = fetchResponse;
statusCode.value = fetchResponse.status;
responseData = await fetchResponse[config.type]();
if (!fetchResponse.ok) {
data.value = config.data || null;
throw new Error(fetchResponse.statusText);
}
if (config.afterFetch) {
// 把 json 之后的数据和 没有 json 后的数据 统一传入
// 返回 数据解构 data
({ data: responseData } = await config.afterFetch({
data: responseData,
response: fetchResponse,
}));
}
data.value = responseData;
responseEvent.trigger(responseData);
return resolve(fetchResponse);
})
.catch(async (fetchError) => {
let errorData = fetchError.message || fetchError.name;
if (config.onFetchError)
({ error: errorData } = await config.onFetchError({
data: responseData,
error: fetchError,
response: response.value,
}));
error.value = errorData;
errorEvent.trigger(fetchError);
if (throwOnFailed) return reject(fetchError);
return resolve(null);
})
.finally(() => {
loading(false);
finallyEvent.trigger(null);
});
});
};
const refetch = ref(config.refetch);
watch([refetch, ref(url)], ([refetch]) => refetch && execute(), {
deep: true,
});
if (config.immediate) {
setTimeout(execute, 0);
}
const shell: UseFetchReturn<T> = {
isFinished,
statusCode,
response,
error,
data,
isFetching,
abort,
execute,
// 会返回一个 off 事件
onFetchResponse: responseEvent.on,
onFetchError: errorEvent.on,
onFetchFinally: finallyEvent.on,
// method
get: setMethod("GET"),
post: setMethod("POST"),
// type
json: setType("json"),
};
function setMethod(method: HttpMethod) {
return (payload?: unknown, payloadType?: string) => {
if (!isFetching.value) {
config.method = method;
config.payload = payload;
config.payloadType = payloadType;
// watch for payload changes
watch(
() => config.payload,
() => execute()
);
const rawPayload = resolveUnref(config.payload);
// Set the payload to json type only if it's not provided and a literal object is provided and the object is not `formData`
// The only case we can deduce the content type and `fetch` can't
if (
!payloadType &&
rawPayload &&
Object.getPrototypeOf(rawPayload) === Object.prototype &&
!(rawPayload instanceof FormData)
)
config.payloadType = "json";
}
return {
...shell,
then(onFulfilled: any) {
return new Promise(onFulfilled);
},
} as any;
};
}
function until(r: any, v: any) {
return new Promise<void>((resolve) => {
watch(
r,
() => {
if (v == r.value) {
resolve();
}
},
{
immediate: true,
}
);
});
}
function waitUntilFinished() {
return new Promise<UseFetchReturn<T>>((resolve, reject) => {
until(isFinished, true)
.then(() => resolve(shell))
.catch((error: Error) => reject(error));
});
}
function setType(type: DataType) {
return () => {
if (!isFetching.value) {
config.type = type;
watch(
() => config.type,
() => execute()
);
return {
...shell,
then(onFulfilled: any, onRejected: any) {
return waitUntilFinished().then(onFulfilled, onRejected);
},
} as any;
}
return {
...shell,
then(onFulfilled: any, onRejected: any) {
return waitUntilFinished().then(onFulfilled, onRejected);
},
} as any;
};
}
return {
...shell,
then(onFulfilled, onRejected) {
return waitUntilFinished().then(onFulfilled, onRejected);
},
};
}