import React, { useEffect, useRef, useState } from "react"; import { getItem, removeItem as removeCookieItem } from "@atomintl/aui-hooks"; import { message } from "@atomintl/aui-next"; import { AES } from "./AES"; import { getRequestPrefix } from "./config"; export { APINameEnum } from "./ApiNameEnum"; /** 需要用到的data之外的数据 */ interface IExternalData { /** 服务器时间,取自接口外层数据的ts字段 */ serverTS?: number; } type ResponseData = Record & IExternalData; type RequestData = Record; export interface FetchOptions { request?: RequestInit; formatter?: (res: Response) => Promise; skip?: boolean; ignoreToken?: boolean; } export interface FetchHandler { onError?: (error: Error) => void; onComplete?: (data: any) => void; } /** 返回的请求方法的第三个参数 - 配置项 */ export interface DoFetchOptions { /** 强制更新参数,保持为最新 */ forceRefresh?: boolean; headers?: HeadersInit; } export type UpdateHandler = ( previousResult: T & IExternalData, fetchResult: { fetchMoreResult: (T & IExternalData) | undefined } ) => T & IExternalData; export interface FetchMoreOptions

{ params?: P; update: UpdateHandler<(T & IExternalData) | undefined>; } const TOKEN_STR = "token"; function getAbortController(): AbortController | null { if (typeof window !== "undefined" && window.AbortController) { return new window.AbortController(); } return null; } class ApiError extends Error { public code: number; constructor(message: string, code: number) { super(message); this.code = code; } } /** * @example * getPathParams('https://api.qa.atomintl.com/common/v2/currency/:coin/', {coin: 'ERC20-USDT'}) * ⬇️ * 'https://api.qa.atomintl.com/common/v2/currency/ERC20-USDT/' * * @param pattern * @param params */ function getPathParams(pattern: string, params?: Record): string { if (!params) { return pattern; } let url = pattern; Object.entries(params).forEach(([name, value]) => { const encodedValue = encodeURIComponent(value.toString()); url = url.replace(":" + name, encodedValue); }); return url; } // Fix the request header language key; function getLocale(): string { return ( (typeof window !== "undefined" && (window as any).___gatsbyIntl && (window as any).___gatsbyIntl.language) || "en-US" ); } function isObject(target: any) { return Object.prototype.toString.call(target) === "[object Object]"; } async function defaultFormatter(res: Response): Promise { const result = await res.json(); const resCode = Number(result.code); if (resCode == 1) { // 服务器时间合并到返回的数据中 return isObject(result.data) ? { serverTS: result.ts, ...result.data } : result.data; } else { throw new ApiError(result.message || result.msg, resCode); } } function generateRequestInit( request: RequestInit = {}, signal?: AbortSignal, locale: string = "en-US", token?: string, ignoreToken: boolean = false ): RequestInit { if (!token) { token = getItem(TOKEN_STR); } const headers = new Headers(request.headers || {}); headers.append("Accept-Language", locale); headers.append("source", "web"); if (token && !ignoreToken) { const decryptTokenStr = AES.instance.aesDecrypt(token); headers.append("Authorization", `Bearer ${decryptTokenStr}`); } return Object.assign({ signal }, request, { headers }); } // 生成API URL function generateBaseUrl(url: string, baseUrl: string, pathParams?: Record): string { if (/^http(s)?/.test(url)) { return getPathParams(url, pathParams); } // return `${baseUrl}${getPathParams(url, pathParams)}`; const prefix = getRequestPrefix(url); return `${prefix}${getPathParams(url, pathParams)}`; } function generateUrl(url: string, params: { [key: string]: string } = {}): URL { const Url = new URL(url); for (const key in params) { Url.searchParams.append(key, params[key]); } return Url; } const mobileReg = /Android|webOS|iPhone|iPod|BlackBerry|iPad/i; type useFetchReturn

= { // 是否加载中 loading: boolean; // 返回经过formatter格式化后的数据 data: (T & IExternalData) | undefined; // 是否有错误 error: Error | ApiError | null; // 加载更多数据,用于分页、滚动加载 fetchMore: (options: FetchMoreOptions) => void; // 刷新数据 refresh: ( params?: P, pathParams?: Record, fetchOptions?: DoFetchOptions ) => Promise<(T & IExternalData) | undefined>; }; const API_Prefix = ``; const Main_Domain = ``; const logout = () => {}; export function useFetch

( url: string, params?: P, options?: FetchOptions | null, handler?: FetchHandler ): useFetchReturn { const [loading, setLoading] = useState(false); const [data, setData] = React.useState<(T & IExternalData) | undefined>(undefined); const [error, setError] = React.useState(null); const isMount = useRef(false); // const controller = getAbortController(); const controller = useRef(null); // 初始参数 const _params = useRef({}); // fetchMore 上一次查询参数 const _prevParams = useRef

({} as P); useEffect(() => { if (params) { _params.current = params; } }, []); // fetch data const fetchData = async ( params?: P, pathParams?: Record, fetchOptions = {} as DoFetchOptions ): Promise<(T & IExternalData) | undefined> => { // 如果连续请求,则取消上一次未完成的请求 if (loading) return; if (loading) { controller.current?.abort(); } setLoading(true); const { forceRefresh, headers } = fetchOptions; const requestInit = generateRequestInit( { ...options?.request, headers: { ...options?.request?.headers, ...headers } }, controller.current?.signal, getLocale(), "", options?.ignoreToken ); if (forceRefresh) { _prevParams.current = {}; } // 合并请求参数 params = Object.assign({}, _prevParams.current, params); const res = await fetch( generateUrl(generateBaseUrl(url, API_Prefix, pathParams), Object.assign({}, _params.current, params)).toString(), requestInit ); // 缓存上一次请求参数 _prevParams.current = params; // let data; if (options?.formatter) { return await options.formatter(res); } return await defaultFormatter(res); // return data; }; const errorHandler = (error: ApiError) => { if (isMount) { setError(error); // 如果没有传递错误处理方法,则统一通过message.error显示错误信息 if (typeof handler?.onError === "function") { handler.onError(error); } else { const isMobile = typeof window !== "undefined" && mobileReg.test(window.navigator.userAgent); const errCode = Number(error.code); // 移除token if (errCode === 401 || errCode === 403) { message.warning(error.message); //清除用户信息 logout(); removeCookieItem(TOKEN_STR, "/", Main_Domain); } else if (error.name === "AbortError" || error.message.includes("NetworkError")) { // NetworkError - 使用fetch请求接口,如果火狐浏览器在请求没完成的情况下就跳到另外的页面,会报含有 `NetworkError` 关键字的错误信息 // 如果是取消请求或者资源加载错误(资源加载错误主要针对火狐浏览器),忽略错误信息 } else { message.error(error.message); } } } }; const doFetch = async ( params?: P, pathParams?: Record, fetchOptions = {} as DoFetchOptions ): Promise<(T & IExternalData) | undefined> => { try { // setLoading(true); // const controller = getAbortController(); // 如果连续请求,则取消上一次未完成的请求 if (loading) { controller.current?.abort(); } controller.current = getAbortController(); const data = (await fetchData(params, pathParams, fetchOptions)) as any; if (isMount) { setData(data); handler?.onComplete?.(data); } // 查询完成后,更新刷新查询参数 const { forceRefresh } = fetchOptions; _prevParams.current = forceRefresh ? {} : params; const tokenFromApi = data?.newToken || data?.token; // 若data里含有newToken字段,则重置token if (tokenFromApi) { // const encryptTokenStr = AES.instance.aesEncrypt(tokenFromApi).encryptedData; // const domain = window.location.hostname === "localhost" ? "localhost" : `.${Main_Domain}`; // setItem("token", encryptTokenStr, Infinity, "/", domain); } return data; } catch (error) { // @ts-ignore errorHandler(error); // handler?.onError?.(error); } finally { if (isMount) { setLoading(false); } } }; useEffect(() => { isMount.current = true; if (url && !options?.skip) { doFetch(); } return () => { isMount.current = false; controller.current?.abort(); }; }, [url, options?.skip]); // 加载更多 const fetchMore = async (options: FetchMoreOptions) => { try { // const params = Object.assign({}, _prevParams.current, options.params); const moreData = await fetchData(options.params); if (typeof options.update === "function") { // 通过update 方法处理完合并成新的state,然后设置 setTimeout(() => { if (!!data) { const result = options.update(data, { fetchMoreResult: moreData }); setData(result); } }, 0); } else { throw new Error("options.update is invalid."); } } catch (error) { // @ts-ignore errorHandler(error); } finally { setLoading(false); } }; // 刷新结果 const refresh = async ( params?: P, pathParams?: Record, fetchOptions?: DoFetchOptions ): Promise<(T & IExternalData) | undefined> => { return doFetch(params, pathParams, fetchOptions); }; return { loading, data, error, fetchMore, refresh }; } export function usePost

( url: string, options?: FetchOptions | null, handler?: FetchHandler ): [ ( data: P, pathParams?: Record, fetchOptions?: DoFetchOptions ) => Promise<(T & IExternalData) | undefined>, { loading: boolean; data: (T & IExternalData) | undefined; error: Error | null } ] { const [loading, setLoading] = useState(false); const [data, setData] = React.useState<(T & IExternalData) | undefined>(undefined); const [error, setError] = React.useState(null); const controller = getAbortController(); // useEffect(() => { const post = async ( data?: P, pathParams?: Record, fetchOptions = {} as DoFetchOptions ): Promise<(T & IExternalData) | undefined> => { setLoading(true); const isFormData = data instanceof FormData; const bodyData = isFormData ? data : JSON.stringify(data); const { headers } = fetchOptions; try { const requestInit = generateRequestInit( { body: bodyData as string | FormData, // must match 'Content-Type' header cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached // credentials: 'same-origin', // include, same-origin, *omit method: "POST", // *GET, POST, PUT, DELETE, etc. ...options?.request, headers: isFormData ? {} : { "Content-Type": "application/json", ...options?.request?.headers, ...headers } }, controller?.signal, getLocale(), "", options?.ignoreToken ); const res = await fetch(generateBaseUrl(url, API_Prefix, pathParams), requestInit); let resData; if (options?.formatter) { resData = await options.formatter(res); } else { resData = await defaultFormatter(res); } // 若data里含有newToken/token字段,则重置token const tokenFromApi = resData?.newToken || resData?.token; // 若data里含有newToken字段,则重置token if (tokenFromApi) { // const encryptTokenStr = AES.instance.aesEncrypt(tokenFromApi).encryptedData; // const domain = window.location.hostname === "localhost" ? "localhost" : `.${Main_Domain}`; // setItem("token", encryptTokenStr, Infinity, "/", domain); } setData(resData); handler?.onComplete?.({ result: resData, data }); return resData; } catch (error) { setError(error); if (typeof handler?.onError === "function") { // handler.onError(error); } else { // const isMobile = typeof window !== "undefined" && mobileReg.test(window.navigator.userAgent); message.error(error.message); } } finally { setLoading(false); } }; // 组件卸载时取消post useEffect(() => { return () => { controller?.abort(); }; }, []); return [post, { loading, error, data }]; } // 手动load数据方法 export function useLazyFetch

( url: string, params?: P, options: FetchOptions = {}, handler?: FetchHandler ): [ ( params?: P, pathParams?: Record, fetchOptions?: DoFetchOptions ) => Promise<(T & IExternalData) | undefined>, useFetchReturn ] { const { loading, data, error, fetchMore, refresh } = useFetch( url, params, { ...options, skip: true }, handler ); return [refresh, { loading, data, error, fetchMore, refresh }]; }