├── docs ├── guide.md └── index.md ├── src ├── hooks │ ├── UseRequest │ │ ├── .DS_Store │ │ ├── Plugins │ │ │ ├── loadingDeleyPlugin.js │ │ │ ├── refreshOnWindowFocusPlugin.js │ │ │ ├── autoRunPlugin.js │ │ │ ├── throttlePlugin.js │ │ │ ├── debouncePlugin.js │ │ │ ├── pollingPlugin.js │ │ │ ├── retryPlugin.js │ │ │ └── catchPlugin.js │ │ ├── index.js │ │ ├── UseRequest.js │ │ └── Fetch.js │ ├── UseUpdate │ │ └── index.js │ ├── utils │ │ ├── limit.js │ │ ├── cacheSubscribe.js │ │ ├── cachePromise.js │ │ ├── cache.js │ │ └── subscribeFocus.js │ ├── UseMemoizedFn │ │ └── index.js │ ├── UseUpdateEffect │ │ └── index.js │ ├── UseCreation │ │ └── index.js │ └── index.md ├── index.js ├── Request │ ├── env.js │ ├── constant.js │ ├── retry.js │ ├── checkStatus.js │ ├── index.js │ ├── canceler.js │ ├── transform.js │ ├── request.js │ └── index.md ├── locales │ ├── tw_TW.json │ ├── zh_CN.json │ ├── ko_KR.json │ ├── ar_AR.json │ ├── th_TH.json │ ├── tr_TR.json │ ├── vi_VN.json │ ├── ru_RU.json │ ├── pl_PL.json │ ├── sv_SE.json │ ├── en_US.json │ ├── id_ID.json │ ├── pt_PT.json │ ├── nl_NL.json │ ├── es_ES.json │ ├── fr_FR.json │ └── de_DE.json ├── utils.js ├── evt.js ├── i18n.js └── components │ └── message │ └── index.jsx ├── tsconfig.json ├── package.json └── README.md /docs/guide.md: -------------------------------------------------------------------------------- 1 | This is a guide example. 2 | -------------------------------------------------------------------------------- /src/hooks/UseRequest/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nemo-crypto/bit-request/HEAD/src/hooks/UseRequest/.DS_Store -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as useRequest } from './hooks/UseRequest'; 2 | 3 | export { default as request } from './Request'; 4 | 5 | export { createRequest } from './Request'; 6 | -------------------------------------------------------------------------------- /src/Request/env.js: -------------------------------------------------------------------------------- 1 | export const device = 2 | typeof window === 'undefined' 3 | ? 'service' 4 | : window.currencyexchange || window.currency_exchange 5 | ? 'app' 6 | : 'browser'; 7 | -------------------------------------------------------------------------------- /src/hooks/UseUpdate/index.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | 3 | function useUpdate() { 4 | const [_, setState] = useState({}); 5 | return useCallback(() => setState({}), []); 6 | } 7 | 8 | export default useUpdate; 9 | -------------------------------------------------------------------------------- /src/Request/constant.js: -------------------------------------------------------------------------------- 1 | export const token = 'token_url'; 2 | 3 | export const CONTENTTYPE = { 4 | JSON: 'application/json;charset=utf-8', 5 | FORM_URLENCODE: 'application/x-www-form-urlencoded;charset=UTF-8', 6 | FORM_DATA: 'multipart/form-data;charset=UTF-8', 7 | }; 8 | -------------------------------------------------------------------------------- /src/hooks/utils/limit.js: -------------------------------------------------------------------------------- 1 | export default function limit(fn, timespan) { 2 | let pending = false; 3 | return (...args) => { 4 | if (pending) return; 5 | pending = true; 6 | fn(...args); 7 | setTimeout(() => { 8 | pending = false; 9 | }, timespan); 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/locales/tw_TW.json: -------------------------------------------------------------------------------- 1 | { 2 | "err.400": "錯誤的請求:伺服器無法理解請求。", 3 | "err.401": "未經授權:需要身份驗證,但身份驗證失敗或未提供。", 4 | "err.403": "禁止訪問:伺服器理解請求,但拒絕授權。", 5 | "err.404": "找不到:找不到請求的資源。", 6 | "err.500": "內部伺服器錯誤:伺服器遇到意外情況,阻止它完成請求。", 7 | "err.501": "未實現:伺服器不支援所請求的功能。", 8 | "err.503": "服務不可用:伺服器目前無法處理請求,可能是因為伺服器過載或正在維護。" 9 | } 10 | -------------------------------------------------------------------------------- /src/locales/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "err.400": "错误请求:服务器无法理解该请求。", 3 | "err.401": "未经授权:需要身份验证,但身份验证失败或未提供。", 4 | "err.403": "禁止访问:服务器理解请求,但拒绝授权。", 5 | "err.404": "未找到:无法找到请求的资源。", 6 | "err.500": "内部服务器错误:服务器遇到了意外情况,阻止它完成请求。", 7 | "err.501": "未实现:服务器不支持所请求的功能。", 8 | "err.503": "服务不可用:服务器当前无法处理请求,可能是由于服务器过载或正在维护。" 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "declaration": false, 5 | "skipLibCheck": true, 6 | "esModuleInterop": true, 7 | "jsx": "react", 8 | "baseUrl": "./", 9 | "paths": { 10 | "@@/*": [".dumi/tmp/*"], 11 | "@bit/request": ["src"], 12 | "@bit/request/*": ["src/*", "*"] 13 | } 14 | }, 15 | "include": [".dumirc.ts", "src/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /src/locales/ko_KR.json: -------------------------------------------------------------------------------- 1 | { 2 | "err.400": "잘못된 요청: 서버가 요청을 이해하지 못했습니다.", 3 | "err.401": "인증되지 않음: 인증이 필요하며 실패했거나 제공되지 않았습니다.", 4 | "err.403": "금지됨: 서버가 요청을 이해했지만 권한을 거부합니다.", 5 | "err.404": "찾을 수 없음: 요청한 리소스를 찾을 수 없습니다.", 6 | "err.500": "내부 서버 오류: 서버가 요청을 처리하는 데 방해가 되는 예상치 못한 상태를 만났습니다.", 7 | "err.501": "구현되지 않음: 서버는 요청된 기능을 지원하지 않습니다.", 8 | "err.503": "서비스 이용 불가: 서버는 현재 일시적으로 과부하되었거나 서버 유지 보수로 인해 요청을 처리할 수 없습니다." 9 | } 10 | -------------------------------------------------------------------------------- /src/hooks/UseMemoizedFn/index.js: -------------------------------------------------------------------------------- 1 | import { useMemo, useRef } from 'react'; 2 | 3 | function useMemoizedFn(fn) { 4 | const fnRef = useRef(fn); 5 | 6 | fnRef.current = useMemo(() => fn, [fn]); 7 | 8 | const memoizedFn = useRef(); 9 | 10 | if (!memoizedFn.current) { 11 | memoizedFn.current = function (...args) { 12 | return fnRef.current.apply(this, args); 13 | }; 14 | } 15 | return memoizedFn.current; 16 | } 17 | 18 | export default useMemoizedFn; 19 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hero: 3 | title: library 4 | description: A react request library 5 | actions: 6 | - text: Hello 7 | link: / 8 | - text: World 9 | link: / 10 | features: 11 | - title: Hello 12 | emoji: 💎 13 | description: Put hello description here 14 | - title: World 15 | emoji: 🌈 16 | description: Put world description here 17 | - title: '!' 18 | emoji: 🚀 19 | description: Put ! description here 20 | --- 21 | 22 | @bit/request 23 | -------------------------------------------------------------------------------- /src/hooks/UseUpdateEffect/index.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | function useUpdateEffect(effect, deps) { 4 | const isMounted = useRef(false); 5 | 6 | useEffect(() => { 7 | return () => { 8 | isMounted.current = false; 9 | }; 10 | }, []); 11 | 12 | useEffect(() => { 13 | if (!isMounted.current) { 14 | isMounted.current = true; 15 | } else { 16 | return effect(); 17 | } 18 | }, deps); 19 | } 20 | 21 | export default useUpdateEffect; 22 | -------------------------------------------------------------------------------- /src/hooks/utils/cacheSubscribe.js: -------------------------------------------------------------------------------- 1 | const listeners = {}; 2 | 3 | const trigger = (key, data) => { 4 | if (listeners[key]) { 5 | listeners[key].forEach((item) => item(data)); 6 | } 7 | }; 8 | 9 | const subscribe = (key, listener) => { 10 | if (!listeners[key]) { 11 | listeners[key] = []; 12 | } 13 | listeners[key].push(listener); 14 | 15 | return function unsubscribe() { 16 | const index = listeners[key].indexOf(listener); 17 | listeners[key].splice(index, 1); 18 | }; 19 | }; 20 | 21 | export { subscribe, trigger }; 22 | -------------------------------------------------------------------------------- /src/locales/ar_AR.json: -------------------------------------------------------------------------------- 1 | { 2 | "err.400": "طلب غير صحيح: الخادم لا يستطيع فهم الطلب.", 3 | "err.401": "غير مصرح: الهوية مطلوبة وفشلت أو لم يتم توفيرها.", 4 | "err.403": "ممنوع: الخادم يفهم الطلب ولكنه يرفض التفويض.", 5 | "err.404": "غير موجود: لا يمكن العثور على المورد المطلوب.", 6 | "err.500": "خطأ داخلي في الخادم: واجه الخادم حالة غير متوقعة منعته من تنفيذ الطلب.", 7 | "err.501": "غير مُنفَّذ: الخادم لا يدعم الميزة المطلوبة.", 8 | "err.503": "الخدمة غير متوفرة: الخادم غير قادر حاليًا على التعامل مع الطلب بسبب الحمل الزائد المؤقت أو صيانة الخادم." 9 | } 10 | -------------------------------------------------------------------------------- /src/locales/th_TH.json: -------------------------------------------------------------------------------- 1 | { 2 | "err.400": "คำขอไม่ถูกต้อง: เซิร์ฟเวอร์ไม่สามารถเข้าใจคำขอได้", 3 | "err.401": "ไม่ได้รับอนุญาต: ต้องการการพิสูจน์ตัวตนและล้มเหลวหรือไม่ได้รับ", 4 | "err.403": "ห้าม: เซิร์ฟเวอร์เข้าใจคำขอ แต่ปฏิเสธให้สิทธิ์", 5 | "err.404": "ไม่พบ: ไม่สามารถค้นหาทรัพยากรที่ร้องขอ", 6 | "err.500": "ข้อผิดพลาดภายในเซิร์ฟเวอร์: เซิร์ฟเวอร์พบสถานการณ์ที่ไม่คาดคิดทำให้ไม่สามารถทำตามคำขอได้", 7 | "err.501": "ยังไม่ได้นำมาใช้: เซิร์ฟเวอร์ไม่รองรับความสามารถที่ขอ", 8 | "err.503": "บริการไม่พร้อมให้บริการ: เซิร์ฟเวอร์ไม่สามารถดำเนินการตามคำขอในขณะนี้เนื่องจากการโอนซองหรือการบำรุงรักษาชั่วคราวของเซิร์ฟเวอร์" 9 | } 10 | -------------------------------------------------------------------------------- /src/locales/tr_TR.json: -------------------------------------------------------------------------------- 1 | { 2 | "err.400": "Geçersiz İstek: Sunucu isteği anlayamadı.", 3 | "err.401": "Yetkisiz: Kimlik doğrulama gerekiyor ve başarısız oldu veya sağlanmadı.", 4 | "err.403": "Yasak: Sunucu isteği anladı, ancak yetkilendirmeyi reddediyor.", 5 | "err.404": "Bulunamadı: İstenen kaynak bulunamadı.", 6 | "err.500": "İç Sunucu Hatası: Sunucu, isteği yerine getirmesini engelleyen beklenmeyen bir durumla karşılaştı.", 7 | "err.501": "Uygulanmadı: Sunucu, istenen özelliği desteklemiyor.", 8 | "err.503": "Hizmet Kullanılamıyor: Sunucu şu anda geçici olarak aşırı yükleme veya bakım nedeniyle isteği işleyemiyor." 9 | } 10 | -------------------------------------------------------------------------------- /src/locales/vi_VN.json: -------------------------------------------------------------------------------- 1 | { 2 | "err.400": "Yêu cầu Không Hợp Lệ: Máy chủ không thể hiểu yêu cầu.", 3 | "err.401": "Không được Ủy Quyền: Yêu cầu xác thực và đã thất bại hoặc không được cung cấp.", 4 | "err.403": "Bị Từ Chối: Máy chủ hiểu yêu cầu, nhưng từ chối ủy quyền.", 5 | "err.404": "Không Tìm Thấy: Không thể tìm thấy nguồn tài nguyên yêu cầu.", 6 | "err.500": "Lỗi Nội Bộ của Máy Chủ: Máy chủ gặp phải một điều kiện không mong muốn đã ngăn chặn nó từ việc thực hiện yêu cầu.", 7 | "err.501": "Chưa Thực Hiện: Máy chủ không hỗ trợ tính năng được yêu cầu.", 8 | "err.503": "Dịch Vụ Không Khả Dụng: Máy chủ hiện không thể xử lý yêu cầu do quá tải tạm thời hoặc bảo trì máy chủ." 9 | } 10 | -------------------------------------------------------------------------------- /src/locales/ru_RU.json: -------------------------------------------------------------------------------- 1 | { 2 | "err.400": "Неверный запрос: Сервер не смог понять запрос.", 3 | "err.401": "Не авторизован: Требуется аутентификация, и она не удалась или не предоставлена.", 4 | "err.403": "Запрещено: Сервер понял запрос, но отказывается предоставить авторизацию.", 5 | "err.404": "Не найдено: Запрошенный ресурс не найден.", 6 | "err.500": "Внутренняя ошибка сервера: Сервер столкнулся с непредвиденным состоянием, которое помешало ему выполнить запрос.", 7 | "err.501": "Не реализовано: Сервер не поддерживает запрошенную функцию.", 8 | "err.503": "Сервис недоступен: Сервер в данный момент не может обработать запрос из-за временной перегрузки или обслуживания сервера." 9 | } 10 | -------------------------------------------------------------------------------- /src/locales/pl_PL.json: -------------------------------------------------------------------------------- 1 | { 2 | "err.400": "Zły żądanie: Serwer nie mógł zrozumieć żądania.", 3 | "err.401": "Nieautoryzowany: Wymagane jest uwierzytelnienie i nie powiodło się lub nie zostało podane.", 4 | "err.403": "Zakazane: Serwer zrozumiał żądanie, ale odmawia autoryzacji.", 5 | "err.404": "Nie znaleziono: Nie można znaleźć żądanego zasobu.", 6 | "err.500": "Wewnętrzny błąd serwera: Serwer napotkał nieoczekiwaną sytuację, która uniemożliwiła mu spełnienie żądania.", 7 | "err.501": "Nie zaimplementowano: Serwer nie obsługuje żądanej funkcji.", 8 | "err.503": "Usługa niedostępna: Serwer obecnie nie może obsługiwać żądania z powodu tymczasowego przeciążenia lub konserwacji serwera." 9 | } 10 | -------------------------------------------------------------------------------- /src/hooks/utils/cachePromise.js: -------------------------------------------------------------------------------- 1 | const cachePromise = new Map(); 2 | 3 | const getCachePromise = (cacheKey) => { 4 | return cachePromise.get(cacheKey); 5 | }; 6 | 7 | const setCachePromise = (cacheKey, promise) => { 8 | // Should cache the same promise, cannot be promise.finally 9 | // Because the promise.finally will change the reference of the promise 10 | cachePromise.set(cacheKey, promise); 11 | 12 | // no use promise.finally for compatibility 13 | promise 14 | .then((res) => { 15 | cachePromise.delete(cacheKey); 16 | return res; 17 | }) 18 | .catch(() => { 19 | cachePromise.delete(cacheKey); 20 | }); 21 | }; 22 | 23 | export { getCachePromise, setCachePromise }; 24 | -------------------------------------------------------------------------------- /src/locales/sv_SE.json: -------------------------------------------------------------------------------- 1 | { 2 | "err.400": "Felaktig förfrågan: Servern kunde inte förstå förfrågan.", 3 | "err.401": "Obehörig: Autentisering krävs och har misslyckats eller har inte tillhandahållits.", 4 | "err.403": "Förbjuden: Servern förstod förfrågan men vägrar att auktorisera den.", 5 | "err.404": "Ej hittad: Begärd resurs kunde inte hittas.", 6 | "err.500": "Intern Serverfel: Servern stötte på ett oväntat tillstånd som förhindrade den från att uppfylla begäran.", 7 | "err.501": "Ej Implementerat: Servern stöder inte den begärda funktionen.", 8 | "err.503": "Tjänst Ej Tillgänglig: Servern kan för närvarande inte hantera begäran på grund av temporär överbelastning eller underhåll av servern." 9 | } 10 | -------------------------------------------------------------------------------- /src/hooks/UseCreation/index.js: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | function depsAreSame(oldDeps, deps) { 4 | if (oldDeps === deps) return true; 5 | for (let i = 0; i < oldDeps.length; i++) { 6 | if (!Object.is(oldDeps[i], deps[i])) return false; 7 | } 8 | return true; 9 | } 10 | 11 | function useCreation(factory, deps) { 12 | const { current } = useRef({ 13 | deps, 14 | obj: undefined, 15 | initialized: false, 16 | }); 17 | 18 | if (current.initialized === false || !depsAreSame(current.deps, deps)) { 19 | current.deps = deps; 20 | current.obj = factory(); 21 | current.initialized = true; 22 | } 23 | return current.obj; 24 | } 25 | 26 | export default useCreation; 27 | -------------------------------------------------------------------------------- /src/locales/en_US.json: -------------------------------------------------------------------------------- 1 | { 2 | "err.400": "Bad Request: The server could not understand the request.", 3 | "err.401": "Unauthorized: Authentication is required and has failed or has not been provided.", 4 | "err.403": "Forbidden: You do not have permission to perform this operation", 5 | "err.404": "Not Found: The requested resource could not be found.", 6 | "err.500": "Internal Server Error: The server encountered an unexpected condition that prevented it from fulfilling the request.", 7 | "err.501": "Not Implemented: The server does not support the requested feature.", 8 | "err.503": "Service Unavailable: The server is currently unable to handle the request due to temporary overloading or maintenance of the server." 9 | } 10 | -------------------------------------------------------------------------------- /src/locales/id_ID.json: -------------------------------------------------------------------------------- 1 | { 2 | "err.400": "Permintaan Tidak Valid: Server tidak dapat memahami permintaan.", 3 | "err.401": "Tidak Diizinkan: Autentikasi diperlukan dan gagal atau tidak diberikan.", 4 | "err.403": "Dilarang: Server memahami permintaan, tetapi menolak memberikan izin.", 5 | "err.404": "Tidak Ditemukan: Sumber daya yang diminta tidak dapat ditemukan.", 6 | "err.500": "Kesalahan Internal Server: Server mengalami kondisi yang tidak terduga yang mencegahnya memenuhi permintaan.", 7 | "err.501": "Belum Diimplementasikan: Server tidak mendukung fitur yang diminta.", 8 | "err.503": "Layanan Tidak Tersedia: Server saat ini tidak dapat menangani permintaan karena kelebihan beban sementara atau pemeliharaan server." 9 | } 10 | -------------------------------------------------------------------------------- /src/locales/pt_PT.json: -------------------------------------------------------------------------------- 1 | { 2 | "err.400": "Pedido Inválido: O servidor não conseguiu entender o pedido.", 3 | "err.401": "Não Autorizado: A autenticação é necessária e falhou ou não foi fornecida.", 4 | "err.403": "Proibido: O servidor entendeu o pedido, mas recusa autorização.", 5 | "err.404": "Não Encontrado: Não foi possível encontrar o recurso solicitado.", 6 | "err.500": "Erro Interno do Servidor: O servidor encontrou uma condição inesperada que o impediu de atender ao pedido.", 7 | "err.501": "Não Implementado: O servidor não suporta a funcionalidade solicitada.", 8 | "err.503": "Serviço Indisponível: O servidor está atualmente incapaz de lidar com o pedido devido a sobrecarga temporária ou manutenção do servidor." 9 | } 10 | -------------------------------------------------------------------------------- /src/locales/nl_NL.json: -------------------------------------------------------------------------------- 1 | { 2 | "err.400": "Bad Request: De server kon het verzoek niet begrijpen.", 3 | "err.401": "Unauthorized: Authenticatie is vereist en is mislukt of niet verstrekt.", 4 | "err.403": "Forbidden: De server begreep het verzoek maar weigert autorisatie.", 5 | "err.404": "Not Found: De gevraagde bron kon niet worden gevonden.", 6 | "err.500": "Interne Serverfout: De server heeft een onverwachte toestand ervaren die hem heeft verhinderd om aan het verzoek te voldoen.", 7 | "err.501": "Niet geïmplementeerd: De server ondersteunt de gevraagde functie niet.", 8 | "err.503": "Service Niet Beschikbaar: De server kan het verzoek momenteel niet verwerken vanwege tijdelijke overbelasting of onderhoud van de server." 9 | } 10 | -------------------------------------------------------------------------------- /src/locales/es_ES.json: -------------------------------------------------------------------------------- 1 | { 2 | "err.400": "Solicitud incorrecta: El servidor no pudo entender la solicitud.", 3 | "err.401": "No autorizado: Se requiere autenticación y ha fallado o no se ha proporcionado.", 4 | "err.403": "Prohibido: El servidor entendió la solicitud, pero se niega a autorizarla.", 5 | "err.404": "No encontrado: No se pudo encontrar el recurso solicitado.", 6 | "err.500": "Error Interno del Servidor: El servidor encontró una condición inesperada que le impidió cumplir con la solicitud.", 7 | "err.501": "No Implementado: El servidor no admite la característica solicitada.", 8 | "err.503": "Servicio No Disponible: El servidor actualmente no puede manejar la solicitud debido a una sobrecarga temporal o mantenimiento del servidor." 9 | } 10 | -------------------------------------------------------------------------------- /src/locales/fr_FR.json: -------------------------------------------------------------------------------- 1 | { 2 | "err.400": "Requête incorrecte : Le serveur n'a pas pu comprendre la requête.", 3 | "err.401": "Non autorisé : L'authentification est requise et a échoué ou n'a pas été fournie.", 4 | "err.403": "Interdit : Le serveur a compris la requête, mais refuse de l'autoriser.", 5 | "err.404": "Non trouvé : La ressource demandée est introuvable.", 6 | "err.500": "Erreur Interne du Serveur : Le serveur a rencontré une condition inattendue qui l'a empêché de répondre à la requête.", 7 | "err.501": "Non Mis en Œuvre : Le serveur ne prend pas en charge la fonction demandée.", 8 | "err.503": "Service Indisponible : Le serveur est actuellement incapable de traiter la requête en raison d'une surcharge temporaire ou de la maintenance du serveur." 9 | } 10 | -------------------------------------------------------------------------------- /src/locales/de_DE.json: -------------------------------------------------------------------------------- 1 | { 2 | "err.400": "Bad Request: Der Server konnte die Anfrage nicht verstehen.", 3 | "err.401": "Unauthorized: Authentifizierung ist erforderlich und ist fehlgeschlagen oder wurde nicht bereitgestellt.", 4 | "err.403": "Forbidden: Der Server hat die Anfrage verstanden, verweigert jedoch die Autorisierung.", 5 | "err.404": "Not Found: Die angeforderte Ressource konnte nicht gefunden werden.", 6 | "err.500": "Interner Serverfehler: Der Server hat einen unerwarteten Zustand erlebt, der ihn daran hinderte, die Anfrage zu erfüllen.", 7 | "err.501": "Nicht implementiert: Der Server unterstützt das angeforderte Feature nicht.", 8 | "err.503": "Dienst nicht verfügbar: Der Server kann die Anfrage derzeit aufgrund vorübergehender Überlastung oder Wartung nicht verarbeiten." 9 | } 10 | -------------------------------------------------------------------------------- /src/hooks/utils/cache.js: -------------------------------------------------------------------------------- 1 | const cache = new Map(); 2 | 3 | const setCache = (key, cacheTime, cachedData) => { 4 | const currentCache = cache.get(key); 5 | if (currentCache?.timer) { 6 | clearTimeout(currentCache.timer); 7 | } 8 | 9 | let timer = undefined; 10 | 11 | if (cacheTime > -1) { 12 | // if cache out, clear it 13 | timer = setTimeout(() => { 14 | cache.delete(key); 15 | }, cacheTime); 16 | } 17 | 18 | cache.set(key, { 19 | ...cachedData, 20 | timer, 21 | }); 22 | }; 23 | 24 | const getCache = (key) => { 25 | return cache.get(key); 26 | }; 27 | 28 | const clearCache = (key) => { 29 | if (key) { 30 | const cacheKeys = Array.isArray(key) ? key : [key]; 31 | cacheKeys.forEach((cacheKey) => cache.delete(cacheKey)); 32 | } else { 33 | cache.clear(); 34 | } 35 | }; 36 | 37 | export { clearCache, getCache, setCache }; 38 | -------------------------------------------------------------------------------- /src/hooks/UseRequest/Plugins/loadingDeleyPlugin.js: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | function loadingDeleyPlugin(fetchInstance, options) { 4 | const { loadingDeley, ready } = options; 5 | // eslint-disable-next-line react-hooks/rules-of-hooks 6 | const timerRef = useRef(); 7 | 8 | if (!loadingDeley) { 9 | return {}; 10 | } 11 | 12 | const cancelTimeout = () => { 13 | if (timerRef.current) { 14 | clearTimeout(timerRef.current); 15 | } 16 | }; 17 | 18 | return { 19 | onBefore: () => { 20 | cancelTimeout(); 21 | if (ready !== false) { 22 | timerRef.current = setTimeout(() => { 23 | fetchInstance.setState({ 24 | loading: true, 25 | }); 26 | }, loadingDeley); 27 | } 28 | 29 | return { 30 | loading: false, 31 | }; 32 | }, 33 | onFinally: () => { 34 | cancelTimeout(); 35 | }, 36 | onCancel: () => { 37 | cancelTimeout(); 38 | }, 39 | }; 40 | } 41 | 42 | export default loadingDeleyPlugin; 43 | -------------------------------------------------------------------------------- /src/hooks/utils/subscribeFocus.js: -------------------------------------------------------------------------------- 1 | const listeners = []; 2 | 3 | const isBrowser = !!( 4 | typeof window !== 'undefined' && 5 | window.document && 6 | window.document.createElement 7 | ); 8 | 9 | const isDocumentVisible = () => { 10 | if (isBrowser) { 11 | return document.visibilityState !== 'hidden'; 12 | } 13 | return true; 14 | }; 15 | 16 | const isOnline = () => { 17 | if (isBrowser && typeof navigator.onLine !== undefined) { 18 | return navigator.onLine; 19 | } 20 | return true; 21 | }; 22 | 23 | function subscribe(listener) { 24 | listeners.push(listener); 25 | return function unsubscribe() { 26 | const index = listeners.indexOf(listener); 27 | if (index > -1) { 28 | listeners.splice(index, 1); 29 | } 30 | }; 31 | } 32 | 33 | if (isBrowser) { 34 | const revalidate = () => { 35 | if (!isDocumentVisible() || !isOnline()) return; 36 | for (let i = 0; i < listeners.length; i++) { 37 | const listener = listeners[i]; 38 | listener(); 39 | } 40 | }; 41 | window.addEventListener('visibilitychange', revalidate, false); 42 | window.addEventListener('focus', revalidate, false); 43 | } 44 | 45 | export default subscribe; 46 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import { isPlainObject } from 'lodash'; 2 | 3 | export const isFunction = (fn) => { 4 | return Object.prototype.toString.call(fn) === '[object Function]'; 5 | }; 6 | 7 | export const deepMerge = (...configs) => { 8 | const newConfig = {}; 9 | 10 | configs.forEach((config) => { 11 | Object.keys(config).forEach((key) => { 12 | if (isPlainObject(config[key])) { 13 | if (newConfig[key]) { 14 | newConfig[key] = deepMerge(newConfig[key], config[key]); 15 | } else { 16 | newConfig[key] = deepMerge(config[key]); 17 | } 18 | } else { 19 | newConfig[key] = config[key]; 20 | } 21 | }); 22 | }); 23 | 24 | return newConfig; 25 | }; 26 | 27 | /** 28 | * 获取URL中指定参数的值 29 | * @param {String} name 名称 30 | * @param {String} url 地址 31 | * @return {String} 值 32 | */ 33 | export const getParameterByName = (name, url) => { 34 | name = name.replace(/[[]/, '\\[').replace(/[\]]/, '\\]'); 35 | let regex = new RegExp('[\\?&]' + name + '=([^&#]*)'), 36 | results = regex.exec(url || window.location.search); 37 | return results === null 38 | ? '' 39 | : decodeURIComponent(results[1].replace(/\+/g, ' ')); 40 | }; 41 | -------------------------------------------------------------------------------- /src/evt.js: -------------------------------------------------------------------------------- 1 | class EventEmitter { 2 | constructor() { 3 | this._events = {}; 4 | } 5 | 6 | on(eventName, callback) { 7 | if (this._events[eventName]) { 8 | if (this.eventName !== 'newListener') { 9 | this.emit('newListener', eventName); 10 | } 11 | } 12 | const callbacks = this._events[eventName] || []; 13 | callbacks.push(callback); 14 | this._events[eventName] = callbacks; 15 | } 16 | 17 | emit(eventName, ...args) { 18 | const callbacks = this._events[eventName] || []; 19 | callbacks.forEach((cb) => cb(...args)); 20 | } 21 | 22 | once(eventName, callback) { 23 | const one = (...args) => { 24 | callback(...args); 25 | this.off(eventName, one); 26 | }; 27 | one.initialCallback = callback; 28 | this.on(eventName, one); 29 | } 30 | 31 | off(eventName, callback) { 32 | const callbacks = this._events[eventName] || []; 33 | const newCallbacks = callbacks.filter( 34 | (fn) => 35 | fn !== callback && 36 | fn.initialCallback !== callback /* 用于once的取消订阅 */, 37 | ); 38 | this._events[eventName] = newCallbacks; 39 | } 40 | } 41 | 42 | export default new EventEmitter(); 43 | 44 | export { EventEmitter }; 45 | -------------------------------------------------------------------------------- /src/hooks/UseRequest/Plugins/refreshOnWindowFocusPlugin.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import limit from '../../utils/limit'; 3 | import subscribeFocus from '../../utils/subscribeFocus'; 4 | 5 | const refreshOnWindowFocusPlugin = ( 6 | fetchInstance, 7 | { refreshOnWindowFocus, focusTimespan = 1000 * 10 }, 8 | ) => { 9 | // eslint-disable-next-line react-hooks/rules-of-hooks 10 | const unsubscribeRef = useRef(); 11 | 12 | const stopSubscribe = () => { 13 | unsubscribeRef.current?.(); 14 | }; 15 | 16 | // eslint-disable-next-line react-hooks/rules-of-hooks 17 | useEffect(() => { 18 | if (refreshOnWindowFocus) { 19 | const limitRefresh = limit( 20 | fetchInstance.refresh.bind(fetchInstance), 21 | focusTimespan, 22 | ); 23 | unsubscribeRef.current = subscribeFocus(() => { 24 | limitRefresh(); 25 | }); 26 | } 27 | return () => { 28 | stopSubscribe(); 29 | }; 30 | }, [refreshOnWindowFocus, focusTimespan]); 31 | 32 | // eslint-disable-next-line react-hooks/rules-of-hooks 33 | useEffect(() => { 34 | return () => { 35 | stopSubscribe(); 36 | }; 37 | }, []); 38 | 39 | return {}; 40 | }; 41 | 42 | export default refreshOnWindowFocusPlugin; 43 | -------------------------------------------------------------------------------- /src/Request/retry.js: -------------------------------------------------------------------------------- 1 | import { isNumber } from 'lodash'; 2 | 3 | export class AxiosRetry { 4 | maximum_backoff = 64 * 1000; 5 | retryTimes = 3; 6 | /** 7 | * 重试 8 | */ 9 | retry(axiosInstance, error) { 10 | const { config } = error; 11 | const retryRequest = config?.requestOptions?.retryRequest || 0; 12 | const count = isNumber(retryRequest) 13 | ? retryRequest 14 | : retryRequest?.count || this.retryTimes; 15 | config.__retryCount = config.__retryCount || 0; 16 | if (config.__retryCount >= count) { 17 | return; 18 | } 19 | //请求返回后config的header不正确造成重试请求失败,删除返回headers采用默认headers 20 | delete config.headers; 21 | const waitTime = this.createTimeout(config.__retryCount); 22 | return this.delay(waitTime).then(() => { 23 | config.__retryCount += 1; 24 | axiosInstance(config); 25 | }); 26 | } 27 | 28 | createTimeout(times) { 29 | const random_number_milliseconds = Math.floor(Math.random() * 1000); 30 | return Math.min( 31 | Math.pow(2, times) * 1000 + random_number_milliseconds, 32 | this.maximum_backoff, 33 | ); 34 | } 35 | 36 | /** 37 | * 延迟 38 | */ 39 | delay(waitTime) { 40 | return new Promise((resolve) => setTimeout(resolve, waitTime)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/hooks/UseRequest/Plugins/autoRunPlugin.js: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import useUpdateEffect from '../../UseUpdateEffect'; 3 | 4 | function autoRunPlugin( 5 | fetchInstance, 6 | { manual, ready = true, defaultParams = [], refreshDeps = [] }, 7 | ) { 8 | // eslint-disable-next-line react-hooks/rules-of-hooks 9 | const hasAutoRun = useRef(false); 10 | hasAutoRun.current = false; 11 | // eslint-disable-next-line react-hooks/rules-of-hooks 12 | useUpdateEffect(() => { 13 | if (!manual && ready) { 14 | hasAutoRun.current = true; 15 | fetchInstance.run(...defaultParams); 16 | } 17 | }, [ready]); 18 | // eslint-disable-next-line react-hooks/rules-of-hooks 19 | useUpdateEffect(() => { 20 | if (hasAutoRun.current) { 21 | return; 22 | } 23 | if (!manual) { 24 | hasAutoRun.current = true; 25 | fetchInstance.refresh(); 26 | } 27 | }, [...refreshDeps]); 28 | 29 | return { 30 | onBefore: () => { 31 | if (!ready) { 32 | return { 33 | stopNow: true, 34 | }; 35 | } 36 | }, 37 | }; 38 | } 39 | 40 | autoRunPlugin.onInit = ({ ready = true, manual }) => { 41 | return { 42 | loading: !manual && ready, 43 | }; 44 | }; 45 | 46 | export default autoRunPlugin; 47 | -------------------------------------------------------------------------------- /src/hooks/UseRequest/Plugins/throttlePlugin.js: -------------------------------------------------------------------------------- 1 | import { throttle } from 'lodash'; 2 | import { useEffect, useRef } from 'react'; 3 | 4 | function throttlePlugin(fetchInstance, options) { 5 | const { throttleWait } = options; 6 | // eslint-disable-next-line react-hooks/rules-of-hooks 7 | const throttleRef = useRef(); 8 | // eslint-disable-next-line react-hooks/rules-of-hooks 9 | useEffect(() => { 10 | if (throttleWait) { 11 | const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance); 12 | 13 | throttleRef.current = throttle((callback) => { 14 | callback(); 15 | }, throttleWait); 16 | 17 | fetchInstance.runAsync = (...args) => { 18 | return new Promise((resolve, reject) => { 19 | throttleRef.current?.(() => { 20 | _originRunAsync(...args) 21 | .then(resolve) 22 | .catch(reject); 23 | }); 24 | }); 25 | }; 26 | 27 | return () => { 28 | throttleRef.current?.cancel(); 29 | fetchInstance.runAsync = _originRunAsync; 30 | }; 31 | } 32 | }, [throttleWait]); 33 | 34 | if (!throttleWait) { 35 | return {}; 36 | } 37 | 38 | return { 39 | onCancel: () => { 40 | throttleRef.current?.cancel(); 41 | }, 42 | }; 43 | } 44 | 45 | export default throttlePlugin; 46 | -------------------------------------------------------------------------------- /src/hooks/UseRequest/Plugins/debouncePlugin.js: -------------------------------------------------------------------------------- 1 | import { debounce } from 'lodash'; 2 | import { useEffect, useRef } from 'react'; 3 | 4 | function debouncePlugin(fetchInstance, options) { 5 | const { debounceWait } = options; 6 | // eslint-disable-next-line react-hooks/rules-of-hooks 7 | const debouncedRef = useRef(); 8 | // eslint-disable-next-line react-hooks/rules-of-hooks 9 | useEffect(() => { 10 | if (debounceWait) { 11 | const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance); 12 | 13 | debouncedRef.current = debounce((callback) => { 14 | callback(); 15 | }, debounceWait); 16 | 17 | fetchInstance.runAsync = (...args) => { 18 | return new Promise((resolve, reject) => { 19 | debouncedRef.current?.(() => { 20 | _originRunAsync(...args) 21 | .then(resolve) 22 | .catch(reject); 23 | }); 24 | }); 25 | }; 26 | 27 | return () => { 28 | debouncedRef.current?.cancel(); 29 | fetchInstance.runAsync = _originRunAsync; 30 | }; 31 | } 32 | }, [debounceWait]); 33 | 34 | if (!debounceWait) { 35 | return {}; 36 | } 37 | 38 | return { 39 | onCancel: () => { 40 | debouncedRef.current?.cancel(); 41 | }, 42 | }; 43 | } 44 | 45 | export default debouncePlugin; 46 | -------------------------------------------------------------------------------- /src/hooks/UseRequest/index.js: -------------------------------------------------------------------------------- 1 | import { isArray, isFunction } from 'lodash'; 2 | import { default as request } from '../../Request'; 3 | import autoRunPlugin from './Plugins/autoRunPlugin'; 4 | import cachePlugin from './Plugins/catchPlugin'; 5 | import debouncePlugin from './Plugins/debouncePlugin'; 6 | import loadingDeleyPlugin from './Plugins/loadingDeleyPlugin'; 7 | import pollingInterval from './Plugins/pollingPlugin'; 8 | import refreshOnWindowFocusPlugin from './Plugins/refreshOnWindowFocusPlugin'; 9 | import throttlePlugin from './Plugins/throttlePlugin'; 10 | import { default as requestHook } from './UseRequest'; 11 | 12 | const fetch = (service) => () => { 13 | const [url, config, options] = service; 14 | return request.request({ url, method: 'GET', ...config }, options); 15 | }; 16 | 17 | function useRequest(service, options, plugins) { 18 | let servicefn = () => new Promise(() => {}); 19 | if (isFunction(service)) { 20 | servicefn = service; 21 | } else if (isArray(service)) { 22 | servicefn = fetch(service); 23 | } 24 | return requestHook(servicefn, options || {}, [ 25 | ...(plugins || []), 26 | autoRunPlugin, 27 | cachePlugin, 28 | loadingDeleyPlugin, 29 | pollingInterval, 30 | throttlePlugin, 31 | debouncePlugin, 32 | refreshOnWindowFocusPlugin, 33 | ]); 34 | } 35 | 36 | export default useRequest; 37 | -------------------------------------------------------------------------------- /src/hooks/UseRequest/Plugins/pollingPlugin.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | function pollingPlugin(fetchInstance, options) { 4 | const { pollingInterval } = options; 5 | // eslint-disable-next-line react-hooks/rules-of-hooks 6 | const timerRef = useRef(); 7 | // eslint-disable-next-line react-hooks/rules-of-hooks 8 | const countRef = useRef(0); 9 | 10 | const stopPolling = () => { 11 | if (timerRef.current) { 12 | clearTimeout(timerRef.current); 13 | } 14 | }; 15 | // eslint-disable-next-line react-hooks/rules-of-hooks 16 | useEffect(() => { 17 | if (!pollingInterval) { 18 | stopPolling(); 19 | } 20 | }, [pollingInterval]); 21 | 22 | if (!pollingInterval) { 23 | return {}; 24 | } 25 | 26 | return { 27 | onBefore: () => { 28 | stopPolling(); 29 | }, 30 | onError: () => { 31 | countRef.current += 1; 32 | }, 33 | onSuccess: () => { 34 | countRef.current = 0; 35 | }, 36 | onFinally: () => { 37 | if (countRef.current === 0) { 38 | // 轮询失败关闭轮询 39 | timerRef.current = setTimeout(() => { 40 | fetchInstance.refresh(); 41 | }, pollingInterval); 42 | } else { 43 | countRef.current = 0; 44 | } 45 | }, 46 | onCancel: () => { 47 | stopPolling(); 48 | }, 49 | }; 50 | } 51 | 52 | export default pollingPlugin; 53 | -------------------------------------------------------------------------------- /src/Request/checkStatus.js: -------------------------------------------------------------------------------- 1 | import message from '../components/message'; 2 | import intl from '../i18n'; 3 | 4 | export function checkStatus(status, msg, config, error) { 5 | const { errorMessageMode } = config?.requestOptions || {}; 6 | 7 | let errorMessage = ''; 8 | 9 | if (error?.message?.includes('timeout')) { 10 | errorMessage = 'Network timeout please refresh and try again!'; 11 | } else { 12 | switch (status) { 13 | case 400: 14 | errorMessage = intl('err.400'); 15 | break; 16 | case 401: 17 | errorMessage = intl('err.401'); 18 | // 未登录 todo 跳登录 19 | break; 20 | case 403: 21 | errorMessage = intl('err.403'); 22 | break; 23 | case 404: 24 | errorMessage = intl('err.404'); 25 | break; 26 | case 500: 27 | errorMessage = intl('err.500'); 28 | break; 29 | case 501: 30 | errorMessage = intl('err.501'); 31 | break; 32 | case 503: 33 | errorMessage = intl('err.503'); 34 | break; 35 | } 36 | } 37 | 38 | errorMessage = msg || errorMessage; 39 | 40 | if (errorMessage) { 41 | if (errorMessageMode === 'toast') { 42 | message(errorMessage); 43 | } else if (errorMessageMode === 'log') { 44 | // todo 上报 45 | console.log({ 46 | status, 47 | msg: errorMessage, 48 | }); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/i18n.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie'; 2 | import { get, memoize, template } from 'lodash'; 3 | import { getParameterByName } from './utils'; 4 | 5 | // const locales = { 6 | // 'en_US': './locales/en_US.json', // 英文 7 | // 'tw_TW': './locales/tw_TW.json', // 中文 - 繁体 8 | // 'es_ES': './locales/es_ES.json', // 西班牙 9 | // 'id_ID': './locales/id_ID.json', // 印尼 10 | // 'ar_AR': './locales/ar_AR.json', // 阿拉伯语 11 | // 'pl_PL': './locales/pl_PL.json', // 波兰语 12 | // 'tr_TR': './locales/tr_TR.json', // 土耳其语 13 | // 'fr_FR': './locales/fr_FR.json', // 法语 14 | // 'de_DE': './locales/de_DE.json', // 德语 15 | // 'th_TH': './locales/th_TH.json', // 泰语 16 | // 'nl_NL': './locales/nl_NL.json', // 荷兰语 17 | // 'pt_PT': './locales/pt_PT.json', // 葡萄牙语 18 | // 'ru_RU': './locales/ru_RU.json', // 俄语 19 | // 'vi_VN': './locales/vi_VN.json', // 越南语 20 | // 'sv_SE': './locales/sv_SE.json', // 瑞典语 21 | // 'ko_KR': './locales/ko_KR.json', // 韩语 22 | // }; 23 | 24 | const currentLang = 25 | (typeof window !== 'undefined' && 26 | getParameterByName('hl', window?.location?.search || '')) || 27 | Cookies.get('clientCommonlanguage') || 28 | 'en_US'; 29 | const currentTemp = require(`./locales/${currentLang}.json`); 30 | 31 | function intl(key, values) { 32 | const compiled = template(get(currentTemp, key), { 33 | interpolate: /{([\s\S]+?)}/g, // {myVar} 34 | }); 35 | 36 | return compiled(values); 37 | } 38 | 39 | export default memoize(intl); 40 | -------------------------------------------------------------------------------- /src/hooks/UseRequest/Plugins/retryPlugin.js: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | const retryPlugin = (fetchInstance, { retryInterval, retryCount }) => { 4 | // eslint-disable-next-line react-hooks/rules-of-hooks 5 | const timerRef = useRef(); 6 | // eslint-disable-next-line react-hooks/rules-of-hooks 7 | const countRef = useRef(0); 8 | // eslint-disable-next-line react-hooks/rules-of-hooks 9 | const triggerByRetry = useRef(false); 10 | 11 | if (!retryCount) { 12 | return {}; 13 | } 14 | 15 | return { 16 | onBefore: () => { 17 | if (!triggerByRetry.current) { 18 | countRef.current = 0; 19 | } 20 | triggerByRetry.current = false; 21 | 22 | if (timerRef.current) { 23 | clearTimeout(timerRef.current); 24 | } 25 | }, 26 | onSuccess: () => { 27 | countRef.current = 0; 28 | }, 29 | onError: () => { 30 | countRef.current += 1; 31 | if (retryCount === -1 || countRef.current <= retryCount) { 32 | // Exponential backoff 33 | const timeout = 34 | retryInterval ?? Math.min(1000 * 2 ** countRef.current, 30000); 35 | timerRef.current = setTimeout(() => { 36 | triggerByRetry.current = true; 37 | fetchInstance.refresh(); 38 | }, timeout); 39 | } else { 40 | countRef.current = 0; 41 | } 42 | }, 43 | onCancel: () => { 44 | countRef.current = 0; 45 | if (timerRef.current) { 46 | clearTimeout(timerRef.current); 47 | } 48 | }, 49 | }; 50 | }; 51 | 52 | export default retryPlugin; 53 | -------------------------------------------------------------------------------- /src/Request/index.js: -------------------------------------------------------------------------------- 1 | import { deepMerge } from '../utils'; 2 | import { CONTENTTYPE } from './constant'; 3 | import { device } from './env'; 4 | import { Request } from './request'; 5 | import { transform } from './transform'; 6 | 7 | // requestOptions 8 | 9 | function createRequest(opt = {}) { 10 | const requestInstance = new Request( 11 | deepMerge( 12 | { 13 | baseURL: '/', 14 | timeout: device === 'service' ? 5000 : 6000, 15 | headers: { 'Content-Type': CONTENTTYPE.FORM_URLENCODE }, 16 | // 默认拦截器 17 | transform, 18 | 19 | // 配置项,下面的选项都可以在独立的接口请求中覆盖 20 | requestOptions: { 21 | // 是否携带公共参数 Boolean|Object 22 | publicParams: true, 23 | 24 | // 接口拼接地址 25 | urlPrefix: '', 26 | 27 | // 是否返回原生响应头 比如:需要获取响应头时使用该属性 28 | isReturnNativeResponse: false, 29 | 30 | // 消息提示类型 'none' 'toast' 'log' 31 | errorMessageMode: 'toast', 32 | 33 | // 忽略重复请求 34 | ignoreCancelToken: false, 35 | 36 | // 重试次数 Number|Boolean|Object {count: 3} 37 | retryRequest: 3, 38 | 39 | // 格式化请求响应数据 40 | transformData: null, 41 | }, 42 | withCredentials: false, 43 | }, 44 | opt || {}, 45 | ), 46 | ); 47 | 48 | return { 49 | request: requestInstance.request.bind(requestInstance), 50 | get: requestInstance.get.bind(requestInstance), 51 | post: requestInstance.post.bind(requestInstance), 52 | upload: requestInstance.upload.bind(requestInstance), 53 | put: requestInstance.put.bind(requestInstance), 54 | delete: requestInstance.delete.bind(requestInstance), 55 | setRequestHooks: requestInstance.setRequestHooks.bind(requestInstance), 56 | }; 57 | } 58 | 59 | const request = createRequest(); 60 | 61 | export default request; 62 | 63 | export { createRequest }; 64 | -------------------------------------------------------------------------------- /src/hooks/UseRequest/UseRequest.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import useCreation from '../UseCreation'; 3 | import useMemoizedFn from '../UseMemoizedFn'; 4 | import useUpdate from '../UseUpdate'; 5 | import Fetch from './Fetch'; 6 | 7 | function useRequest(service, options, plugins) { 8 | const { manual = false, ...rest } = options || {}; 9 | 10 | const serviceRef = useRef(service); 11 | 12 | const update = useUpdate(); 13 | 14 | const fetchOptions = { manual, ...rest }; 15 | 16 | const fetchInstance = useCreation(() => { 17 | /** 18 | * {loading, data, params, error} 19 | */ 20 | const initState = plugins 21 | .map((p) => p?.onInit?.(fetchOptions)) 22 | .filter(Boolean); 23 | 24 | return new Fetch( 25 | serviceRef, 26 | fetchOptions, 27 | update, 28 | Object.assign({}, ...initState), 29 | ); 30 | }, []); 31 | 32 | fetchInstance.options = fetchOptions; 33 | 34 | fetchInstance.plugins = plugins.map((p) => p(fetchInstance, fetchOptions)); 35 | 36 | useEffect(() => { 37 | if (!manual) { 38 | const params = fetchInstance.state.params || options.defaultParams || []; 39 | fetchInstance.run(...params); 40 | } 41 | return () => { 42 | fetchInstance.cancel(); 43 | }; 44 | }, []); 45 | 46 | return { 47 | loading: fetchInstance.state.loading, 48 | data: fetchInstance.state.data, 49 | error: fetchInstance.state.error, 50 | params: fetchInstance.state.params || [], 51 | cancel: useMemoizedFn(fetchInstance.cancel.bind(fetchInstance)), 52 | refresh: useMemoizedFn(fetchInstance.refresh.bind(fetchInstance)), 53 | refreshAsync: useMemoizedFn(fetchInstance.refreshAsync.bind(fetchInstance)), 54 | run: useMemoizedFn(fetchInstance.run.bind(fetchInstance)), 55 | runAsync: useMemoizedFn(fetchInstance.runAsync.bind(fetchInstance)), 56 | mutate: useMemoizedFn(fetchInstance.mutate.bind(fetchInstance)), 57 | }; 58 | } 59 | 60 | export default useRequest; 61 | -------------------------------------------------------------------------------- /src/components/message/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | class Message extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { visible: true }; 7 | } 8 | 9 | componentDidMount() { 10 | const { duration = 3000, onClose } = this.props; 11 | this.timer = setTimeout(() => { 12 | this.setState({ visible: false }, onClose); 13 | }, duration); 14 | } 15 | 16 | componentWillUnmount() { 17 | clearTimeout(this.timer); 18 | } 19 | handleClose = () => { 20 | const { onClose } = this.props; 21 | this.setState({ visible: false }, onClose); 22 | }; 23 | render() { 24 | const classNames = { 25 | position: 'fixed', 26 | top: ' 50%', 27 | left: '50%', 28 | transform: 'translate(-50%, -50%)', 29 | zIndex: 999, 30 | minWidth: ' 260px', 31 | maxWidth: '500px', 32 | wordWrap: 'break-word', 33 | textAlign: 'center', 34 | padding: ' 14px 20px', 35 | fontSize: '14px', 36 | color: '#fff', 37 | backgroundColor: 'rgba(67, 86, 104, 0.8)', 38 | borderRadius: ' 4px', 39 | }; 40 | const { message } = this.props; 41 | const { visible } = this.state; 42 | return ( 43 |
44 | {visible && ( 45 |
46 | {message} 47 |
48 | )} 49 |
50 | ); 51 | } 52 | } 53 | 54 | const message = (message, duration = 3000, onClose) => { 55 | const div = document.createElement('div'); 56 | document.body.appendChild(div); 57 | const handleClose = () => { 58 | // eslint-disable-next-line react/no-deprecated 59 | ReactDOM.unmountComponentAtNode(div); 60 | document.body.removeChild(div); 61 | }; 62 | // eslint-disable-next-line react/no-deprecated 63 | ReactDOM.render( 64 | , 69 | div, 70 | ); 71 | }; 72 | 73 | export default message; 74 | -------------------------------------------------------------------------------- /src/Request/canceler.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import qs from 'qs'; 3 | import { isFunction } from '../utils'; 4 | 5 | // 声明一个 Map 用于存储每个请求的标识 和 取消函数 6 | let pendingMap = new Map(); 7 | 8 | const isAbortControllerSupport = () => { 9 | let abortControllerSupported = false; 10 | 11 | try { 12 | new AbortController(); 13 | abortControllerSupported = true; 14 | } catch (e) {} 15 | 16 | return abortControllerSupported; 17 | }; 18 | 19 | export const getPendingUrl = (config) => 20 | [ 21 | config.method, 22 | config.url, 23 | qs.stringify(config.data), 24 | qs.stringify(config.params), 25 | ].join('&'); 26 | 27 | export class AxiosCanceler { 28 | /** 29 | * 添加请求 30 | * @param {Object} config 31 | */ 32 | addPending(config) { 33 | this.removePending(config); 34 | const url = getPendingUrl(config); 35 | if (isAbortControllerSupport()) { 36 | const contraller = new AbortController(); 37 | config.signal = config.signal || contraller.signal; 38 | if (!pendingMap.has(url)) { 39 | pendingMap.set(url, contraller); 40 | } 41 | } else { 42 | config.cancelToken = 43 | config.cancelToken || 44 | new axios.CancelToken((cancel) => { 45 | if (!pendingMap.has(url)) { 46 | pendingMap.set(url, cancel); 47 | } 48 | }); 49 | } 50 | } 51 | 52 | /** 53 | * @description: 清空所有pending 54 | */ 55 | removeAllPending() { 56 | pendingMap.forEach((cancel) => { 57 | if (cancel) { 58 | isFunction(cancel?.abort) ? cancel.abort() : cancel(); 59 | } 60 | }); 61 | pendingMap.clear(); 62 | } 63 | 64 | /** 65 | * 移除请求 66 | * @param {Object} config 67 | */ 68 | removePending(config) { 69 | const url = getPendingUrl(config); 70 | 71 | if (pendingMap.has(url)) { 72 | // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除 73 | const cancel = pendingMap.get(url); 74 | if (cancel) { 75 | isFunction(cancel?.abort) ? cancel.abort(url) : cancel(url); 76 | } 77 | pendingMap.delete(url); 78 | } 79 | } 80 | 81 | /** 82 | * @description: 重置 83 | */ 84 | reset() { 85 | pendingMap = new Map(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bit/request", 3 | "version": "0.5.0", 4 | "description": "A react request library", 5 | "license": "MIT", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "build": "father build", 13 | "build:watch": "father dev", 14 | "dev": "dumi dev", 15 | "docs:build": "dumi build", 16 | "doctor": "father doctor", 17 | "lint": "npm run lint:es && npm run lint:css", 18 | "lint:css": "stylelint \"{src,test}/**/*.{css,less}\"", 19 | "lint:es": "eslint \"{src,test}/**/*.{js,jsx,ts,tsx}\"", 20 | "prepare": "husky install && dumi setup", 21 | "prepublishOnly": "father doctor && npm run build", 22 | "start": "npm run dev" 23 | }, 24 | "commitlint": { 25 | "extends": [ 26 | "@commitlint/config-conventional" 27 | ] 28 | }, 29 | "lint-staged": { 30 | "*.{md,json}": [ 31 | "prettier --write --no-error-on-unmatched-pattern" 32 | ], 33 | "*.{css,less}": [ 34 | "stylelint --fix", 35 | "prettier --write" 36 | ], 37 | "*.{js,jsx}": [ 38 | "eslint --fix", 39 | "prettier --write" 40 | ], 41 | "*.{ts,tsx}": [ 42 | "eslint --fix", 43 | "prettier --parser=typescript --write" 44 | ] 45 | }, 46 | "dependencies": { 47 | "axios": "^1.6.2", 48 | "js-cookie": "^3.0.5", 49 | "lodash": "^4.17.21", 50 | "qs": "^6.11.2", 51 | "query-string": "^8.1.0" 52 | }, 53 | "devDependencies": { 54 | "@commitlint/cli": "^17.1.2", 55 | "@commitlint/config-conventional": "^17.1.0", 56 | "@types/react": "^18.0.0", 57 | "@types/react-dom": "^18.0.0", 58 | "@umijs/lint": "^4.0.0", 59 | "dumi": "^2.2.13", 60 | "eslint": "^8.23.0", 61 | "father": "^4.1.0", 62 | "husky": "^8.0.1", 63 | "lint-staged": "^13.0.3", 64 | "prettier": "^2.7.1", 65 | "prettier-plugin-organize-imports": "^3.0.0", 66 | "prettier-plugin-packagejson": "^2.2.18", 67 | "react": "^18.0.0", 68 | "react-dom": "^18.0.0", 69 | "stylelint": "^14.9.1" 70 | }, 71 | "peerDependencies": { 72 | "react": ">=16.9.0", 73 | "react-dom": ">=16.9.0" 74 | }, 75 | "publishConfig": { 76 | "access": "public" 77 | }, 78 | "authors": [ 79 | "https://github.com/nemo-crypto" 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /src/hooks/UseRequest/Fetch.js: -------------------------------------------------------------------------------- 1 | class Fetch { 2 | plugins = []; 3 | count = 0; 4 | state = { 5 | loading: false, 6 | params: undefined, 7 | data: undefined, 8 | error: undefined, 9 | }; 10 | 11 | constructor(serviceRef, options, update, initState) { 12 | this.state = { 13 | ...this.state, 14 | loading: !options.manual, 15 | ...initState, 16 | }; 17 | this.serviceRef = serviceRef; 18 | this.update = update; 19 | } 20 | 21 | setState(state) { 22 | this.state = { 23 | ...this.state, 24 | ...state, 25 | }; 26 | this.update(); 27 | } 28 | 29 | runPluginHandler(event, ...rest) { 30 | const res = this.plugins.map((p) => p[event]?.(...rest)).filter(Boolean); 31 | return Object.assign({}, ...res); 32 | } 33 | 34 | async runAsync(...params) { 35 | this.count += 1; 36 | const currentCount = this.count; 37 | 38 | const { 39 | stopNow = false, 40 | returnNow = false, 41 | ...state 42 | } = this.runPluginHandler('onBefore', params); 43 | 44 | // stop request 45 | if (stopNow) { 46 | return new Promise(() => {}); 47 | } 48 | 49 | this.setState({ 50 | loading: true, 51 | params, 52 | ...state, 53 | }); 54 | 55 | // return now 56 | if (returnNow) { 57 | return Promise.resolve(state.data); 58 | } 59 | 60 | this.options.onBefore?.(params); 61 | 62 | try { 63 | // replace service 64 | let { servicePromise } = this.runPluginHandler( 65 | 'onRequest', 66 | this.serviceRef.current, 67 | params, 68 | ); 69 | 70 | if (!servicePromise) { 71 | servicePromise = this.serviceRef.current(...params); 72 | } 73 | 74 | const res = await servicePromise; 75 | 76 | if (currentCount !== this.count) { 77 | // prevent run.then when request is canceled 78 | return new Promise(() => {}); 79 | } 80 | 81 | // const formattedResult = this.options.formatResultRef.current ? this.options.formatResultRef.current(res) : res; 82 | 83 | this.setState({ 84 | data: res, 85 | error: undefined, 86 | loading: false, 87 | }); 88 | 89 | this.options.onSuccess?.(res, params); 90 | this.runPluginHandler('onSuccess', res, params); 91 | 92 | this.options.onFinally?.(params, res, undefined); 93 | 94 | if (currentCount === this.count) { 95 | this.runPluginHandler('onFinally', params, res, undefined); 96 | } 97 | 98 | return res; 99 | } catch (error) { 100 | if (currentCount !== this.count) { 101 | // prevent run.then when request is canceled 102 | return new Promise(() => {}); 103 | } 104 | 105 | this.setState({ 106 | error, 107 | loading: false, 108 | }); 109 | 110 | this.options.onError?.(error, params); 111 | this.runPluginHandler('onError', error, params); 112 | 113 | this.options.onFinally?.(params, undefined, error); 114 | 115 | if (currentCount === this.count) { 116 | this.runPluginHandler('onFinally', params, undefined, error); 117 | } 118 | 119 | throw error; 120 | } 121 | } 122 | 123 | run(...params) { 124 | this.runAsync(...params).catch((error) => { 125 | if (!this.options?.onError) { 126 | console.error(error); 127 | } 128 | }); 129 | } 130 | 131 | cancel() { 132 | this.count += 1; 133 | this.setState({ 134 | loading: false, 135 | }); 136 | 137 | this.runPluginHandler('onCancel'); 138 | } 139 | 140 | refresh() { 141 | this.run(...(this.state.params || [])); 142 | } 143 | 144 | refreshAsync() { 145 | return this.runAsync(...(this.state.params || [])); 146 | } 147 | 148 | mutate(data) { 149 | const targetData = isFunction(data) ? data(this.state.data) : data; 150 | this.runPluginHandler('onMutate', targetData); 151 | this.setState({ 152 | data: targetData, 153 | }); 154 | } 155 | } 156 | 157 | export default Fetch; 158 | -------------------------------------------------------------------------------- /src/hooks/UseRequest/Plugins/catchPlugin.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import useCreation from '../../UseCreation'; 3 | import { getCache, setCache } from '../../utils/cache'; 4 | import { getCachePromise, setCachePromise } from '../../utils/cachePromise'; 5 | import { subscribe, trigger } from '../../utils/cacheSubscribe'; 6 | 7 | const cachePlugin = ( 8 | fetchInstance, 9 | { 10 | cacheKey, 11 | cacheTime = 5 * 60 * 1000, 12 | staleTime = 0, 13 | setCache: customSetCache, 14 | getCache: customGetCache, 15 | }, 16 | ) => { 17 | // eslint-disable-next-line react-hooks/rules-of-hooks 18 | const unSubscribeRef = useRef(); 19 | // eslint-disable-next-line react-hooks/rules-of-hooks 20 | const currentPromiseRef = useRef(); 21 | 22 | const _setCache = (key, cachedData) => { 23 | if (customSetCache) { 24 | customSetCache(cachedData); 25 | } else { 26 | setCache(key, cacheTime, cachedData); 27 | } 28 | trigger(key, cachedData.data); 29 | }; 30 | 31 | const _getCache = (key, params) => { 32 | if (customGetCache) { 33 | return customGetCache(params); 34 | } 35 | return getCache(key); 36 | }; 37 | // eslint-disable-next-line react-hooks/rules-of-hooks 38 | useCreation(() => { 39 | if (!cacheKey) { 40 | return; 41 | } 42 | 43 | // get data from cache when init 44 | const cacheData = _getCache(cacheKey); 45 | if (cacheData && Object.hasOwnProperty.call(cacheData, 'data')) { 46 | fetchInstance.state.data = cacheData.data; 47 | fetchInstance.state.params = cacheData.params; 48 | if ( 49 | staleTime === -1 || 50 | new Date().getTime() - cacheData.time <= staleTime 51 | ) { 52 | fetchInstance.state.loading = false; 53 | } 54 | } 55 | 56 | // subscribe same cachekey update, trigger update 57 | unSubscribeRef.current = subscribe(cacheKey, (data) => { 58 | fetchInstance.setState({ data }); 59 | }); 60 | }, []); 61 | // eslint-disable-next-line react-hooks/rules-of-hooks 62 | useEffect(() => { 63 | return () => { 64 | unSubscribeRef.current?.(); 65 | }; 66 | }, []); 67 | 68 | if (!cacheKey) { 69 | return {}; 70 | } 71 | 72 | return { 73 | onBefore: (params) => { 74 | const cacheData = _getCache(cacheKey, params); 75 | 76 | if (!cacheData || !Object.hasOwnProperty.call(cacheData, 'data')) { 77 | return {}; 78 | } 79 | 80 | // If the data is fresh, stop request 81 | if ( 82 | staleTime === -1 || 83 | new Date().getTime() - cacheData.time <= staleTime 84 | ) { 85 | return { 86 | loading: false, 87 | data: cacheData?.data, 88 | error: undefined, 89 | returnNow: true, 90 | }; 91 | } else { 92 | // If the data is stale, return data, and request continue 93 | return { 94 | data: cacheData?.data, 95 | error: undefined, 96 | }; 97 | } 98 | }, 99 | onRequest: (service, args) => { 100 | let servicePromise = getCachePromise(cacheKey); 101 | 102 | // If has servicePromise, and is not trigger by self, then use it 103 | if (servicePromise && servicePromise !== currentPromiseRef.current) { 104 | return { servicePromise }; 105 | } 106 | 107 | servicePromise = service(...args); 108 | currentPromiseRef.current = servicePromise; 109 | setCachePromise(cacheKey, servicePromise); 110 | return { servicePromise }; 111 | }, 112 | onSuccess: (data, params) => { 113 | if (cacheKey) { 114 | // cancel subscribe, avoid trgger self 115 | unSubscribeRef.current?.(); 116 | _setCache(cacheKey, { 117 | data, 118 | params, 119 | time: new Date().getTime(), 120 | }); 121 | // resubscribe 122 | unSubscribeRef.current = subscribe(cacheKey, (d) => { 123 | fetchInstance.setState({ data: d }); 124 | }); 125 | } 126 | }, 127 | onMutate: (data) => { 128 | if (cacheKey) { 129 | // cancel subscribe, avoid trigger self 130 | unSubscribeRef.current?.(); 131 | _setCache(cacheKey, { 132 | data, 133 | params: fetchInstance.state.params, 134 | time: new Date().getTime(), 135 | }); 136 | // resubscribe 137 | unSubscribeRef.current = subscribe(cacheKey, (d) => { 138 | fetchInstance.setState({ data: d }); 139 | }); 140 | } 141 | }, 142 | }; 143 | }; 144 | 145 | export default cachePlugin; 146 | -------------------------------------------------------------------------------- /src/Request/transform.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import Cookies from 'js-cookie'; 3 | import { isBoolean, isNumber, isObject } from 'lodash'; 4 | import queryString from 'query-string'; 5 | import { isFunction } from '../utils'; 6 | import { checkStatus } from './checkStatus'; 7 | import { CONTENTTYPE, token as token_url } from './constant'; 8 | import { device } from './env'; 9 | import { AxiosRetry } from './retry'; 10 | 11 | /** 12 | * @description: 数据处理,方便区分多种处理方式 13 | */ 14 | export const transform = { 15 | /** 16 | * @description: 处理请求数据。直接返回 17 | */ 18 | afterRequestHook: (res, options) => { 19 | const { isReturnNativeResponse } = options; 20 | 21 | // 是否返回原生响应头 比如:需要获取响应头时使用该属性 22 | if (isReturnNativeResponse) { 23 | return res; 24 | } 25 | 26 | let { data, config } = res || {}; 27 | 28 | if (!data) { 29 | throw new Error('request error!'); 30 | } 31 | 32 | const { transformData } = options; 33 | 34 | if (transformData && isFunction(transformData)) { 35 | try { 36 | data = transformData(data); 37 | } catch (err) { 38 | throw new Error(err || new Error('request error!')); 39 | } 40 | } 41 | // 除json等文件请求以外的响应数据如果没有code,提示响应数据格式不符合标准 42 | if (!('code' in data) && !/^[\s\S]*\.[\s\S]*$/.test(config.url)) { 43 | console.warn( 44 | '请求返回的数据格式不符合标准,请添加transformData修改或让服务端调整响应数据格式。否则下次升级版本将直接报错', 45 | ); 46 | } 47 | 48 | return data || null; 49 | }, 50 | 51 | /** 52 | * @description: 请求之前处理config 53 | */ 54 | beforeRequestHook: (config, options) => { 55 | const { data, method, headers = {} } = config || {}; 56 | const { urlPrefix } = options; 57 | 58 | const contentType = headers['Content-Type'] || headers['content-type']; 59 | 60 | if ( 61 | method?.toUpperCase() !== 'GET' && 62 | contentType === CONTENTTYPE.FORM_URLENCODE && 63 | data 64 | ) { 65 | config.data = queryString.stringify(data); 66 | } 67 | 68 | if (urlPrefix) { 69 | config.url = `${urlPrefix}${config.url}`; 70 | } 71 | 72 | return config; 73 | }, 74 | 75 | /** 76 | * @description: 请求拦截器处理 77 | */ 78 | requestInterceptors: (config, options) => { 79 | /** 80 | * 添加公共参数 publicParams Boolean | Object 81 | **/ 82 | const withPublicParams = 83 | config.requestOptions?.publicParams !== 'undefined' 84 | ? config.requestOptions?.publicParams 85 | : options.requestOptions?.publicParams; 86 | 87 | let publicParams = {}; 88 | 89 | if (isObject(withPublicParams)) { 90 | publicParams = withPublicParams; 91 | } else if (typeof withPublicParams === 'boolean' && withPublicParams) { 92 | let token = ''; 93 | if (device !== 'service') { 94 | // 非app用户登录校验 95 | if (device !== 'app') { 96 | token = Cookies.get(token_url) || ''; 97 | } 98 | 99 | if (token) { 100 | publicParams = { 101 | token, // 用户token 102 | appName: navigator.appName, // 浏览器名称 103 | appCodeName: navigator.appCodeName, // 浏览器代码名称 104 | appVersion: navigator.appVersion, // 浏览器版本号 105 | userAgent: navigator.userAgent, // 浏览器版本信息 106 | cookieEnabled: navigator.cookieEnabled, // 浏览器是否启用cookie 107 | platform: navigator.platform, // 客户端操作系统 108 | userLanguage: navigator.language, // 浏览器语言 109 | vendor: navigator.vendor, // 浏览器厂商 110 | onLine: navigator.onLine, // 浏览器是否需要连接网络 111 | product: navigator.product, // 浏览器产品名称 112 | productSub: navigator.productSub, // 浏览器产品其他信息 113 | mimeTypesLen: navigator.mimeTypes?.length, // 浏览器的MIME类型数量 114 | pluginsLen: navigator.plugins?.length, // 浏览器的插件数量 115 | javaEnbled: navigator.javaEnabled(), // 浏览器是否启用JAVA 116 | windowScreenWidth: window.screen.width, // 屏幕分辨率 - 宽 117 | windowScreenHeight: window.screen.height, // 屏幕分辨率 - 高 118 | windowColorDepth: window.screen.colorDepth, // 屏幕色彩位数 119 | }; 120 | } 121 | } 122 | } 123 | 124 | // 发送公共参数 125 | config.params = config.params 126 | ? Object.assign({}, publicParams, config.params) 127 | : publicParams; 128 | 129 | return config; 130 | }, 131 | 132 | /** 133 | * @description: 请求错误拦截器处理 134 | */ 135 | 136 | requestInterceptorsCatch: (error) => { 137 | return Promise.reject(error); 138 | }, 139 | 140 | /** 141 | * @description: 响应拦截器处理 142 | */ 143 | responseInterceptors: (res) => { 144 | // 请求失败 2xx 145 | if (res.status !== 200) { 146 | return Promise.reject(res); 147 | } 148 | return res; 149 | }, 150 | 151 | /** 152 | * @description: 响应错误处理 153 | */ 154 | responseInterceptorsCatch: (axiosInstance, error) => { 155 | const { response, config = {} } = error || {}; 156 | 157 | // 请求是被取消的 158 | if (axios.isCancel(error)) { 159 | return Promise.reject(error); 160 | } 161 | 162 | if (device !== 'service') { 163 | // 统一状态码处理 164 | if (!config?.__retryCount) { 165 | checkStatus( 166 | response?.status, 167 | response?.data?.message || '', 168 | config || {}, 169 | error, 170 | ); 171 | } 172 | 173 | // 自动重试机制 只针对client端的GET请求 174 | const retryRequest = new AxiosRetry(); 175 | const retryRequestTimes = config?.requestOptions?.retryRequest; // Number || Boolean || Object 176 | const needRetry = isBoolean(retryRequestTimes) 177 | ? retryRequestTimes 178 | : isNumber(retryRequestTimes) 179 | ? retryRequestTimes > 0 180 | : true; 181 | config?.method?.toUpperCase() === 'GET' && 182 | needRetry && 183 | retryRequest.retry(axiosInstance, error); 184 | } 185 | 186 | // 忽略重试error 187 | if (config?.__retryCount) { 188 | return; 189 | } 190 | return Promise.reject(error); 191 | }, 192 | }; 193 | -------------------------------------------------------------------------------- /src/Request/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { cloneDeep } from 'lodash'; 3 | import { isFunction } from '../utils'; 4 | import { AxiosCanceler } from './canceler'; 5 | import { CONTENTTYPE } from './constant'; 6 | import { device } from './env'; 7 | 8 | /** 9 | * @description: axios模块 10 | */ 11 | export class Request { 12 | constructor(options) { 13 | this.options = options; 14 | this.axiosInstance = axios.create(options); 15 | this.setupInterceptors(); 16 | } 17 | 18 | getAxios() { 19 | return this.axiosInstance; 20 | } 21 | 22 | /** 23 | * @description: 重新配置axios 24 | */ 25 | configAxios(config) { 26 | if (!this.axiosInstance) { 27 | return; 28 | } 29 | this.createAxios(config); 30 | } 31 | 32 | /** 33 | * @description: 添加自定义拦截器 34 | */ 35 | 36 | setRequestHooks({ beforeRequestHook, afterRequestHook }) { 37 | if (this.axiosInstance) { 38 | this.axiosInstance.interceptors.request.use( 39 | (config) => { 40 | if (beforeRequestHook && isFunction(beforeRequestHook)) { 41 | config = beforeRequestHook(config, this.options); 42 | } 43 | 44 | return config; 45 | }, 46 | (error) => { 47 | return Promise.reject(error); 48 | }, 49 | ); 50 | this.axiosInstance.interceptors.response.use( 51 | (res) => { 52 | if (afterRequestHook && isFunction(afterRequestHook)) { 53 | res = afterRequestHook(res); 54 | } 55 | 56 | return res; 57 | }, 58 | (error) => { 59 | return Promise.reject(error); 60 | }, 61 | ); 62 | } 63 | } 64 | 65 | /** 66 | * @description: 设置通用header 67 | */ 68 | setHeader(headers) { 69 | if (!this.axiosInstance) { 70 | return; 71 | } 72 | Object.assign(this.axiosInstance.defaults.headers, headers); 73 | } 74 | 75 | /** 76 | * @description: 创建axios实例 77 | */ 78 | createAxios(config) { 79 | this.axiosInstance = axios.create(config); 80 | } 81 | 82 | /** 83 | * @description: 拦截器配置 84 | */ 85 | setupInterceptors() { 86 | const { transform } = this.options; 87 | if (!transform) { 88 | return; 89 | } 90 | const { 91 | requestInterceptors, 92 | requestInterceptorsCatch, 93 | responseInterceptors, 94 | responseInterceptorsCatch, 95 | } = transform; 96 | 97 | const axiosCanceler = new AxiosCanceler(); 98 | 99 | // 请求拦截器配置处理 100 | this.axiosInstance.interceptors.request.use( 101 | (config) => { 102 | // 是否忽略重复请求 优先取单个请求里的配置 103 | const ignoreCancel = 104 | config.requestOptions?.ignoreCancelToken !== 'undefined' 105 | ? config.requestOptions?.ignoreCancelToken 106 | : this.options.requestOptions?.ignoreCancelToken; 107 | !ignoreCancel && axiosCanceler.addPending(config); 108 | 109 | if (requestInterceptors && isFunction(requestInterceptors)) { 110 | config = requestInterceptors(config, this.options); 111 | } 112 | 113 | return config; 114 | }, 115 | (error) => { 116 | if (requestInterceptorsCatch && isFunction(requestInterceptorsCatch)) { 117 | return requestInterceptorsCatch(error); 118 | } 119 | }, 120 | ); 121 | 122 | // 响应结果拦截器处理 123 | this.axiosInstance.interceptors.response.use( 124 | (res) => { 125 | res && axiosCanceler.removePending(res.config); 126 | 127 | if (responseInterceptors && isFunction(responseInterceptors)) { 128 | res = responseInterceptors(res); 129 | } 130 | 131 | return res; 132 | }, 133 | (error) => { 134 | if ( 135 | responseInterceptorsCatch && 136 | isFunction(responseInterceptorsCatch) 137 | ) { 138 | return responseInterceptorsCatch(this.axiosInstance, error); 139 | } 140 | }, 141 | ); 142 | } 143 | 144 | /** 145 | * @description: 请求方法 146 | * options: requestOptions 147 | */ 148 | request(config, options) { 149 | let conf = cloneDeep(config); 150 | 151 | const { requestOptions, transform } = this.options; 152 | 153 | // 单个请求和初始化requestOptions合并 154 | const mergedOpt = Object.assign({}, requestOptions, options); 155 | const { beforeRequestHook, afterRequestHook } = transform || {}; 156 | if (beforeRequestHook && isFunction(beforeRequestHook)) { 157 | conf = beforeRequestHook(conf, mergedOpt); 158 | } 159 | 160 | //这里重新 赋值成最新的配置 161 | conf.requestOptions = mergedOpt; 162 | 163 | return new Promise((resolve, reject) => { 164 | this.axiosInstance 165 | .request(conf) 166 | .then((res) => { 167 | let ret = res; 168 | if (afterRequestHook && isFunction(afterRequestHook)) { 169 | try { 170 | ret = afterRequestHook(res, mergedOpt); 171 | } catch (err) { 172 | return reject(err || new Error('request error!')); 173 | } 174 | } 175 | 176 | resolve(ret); 177 | }) 178 | .catch((e) => { 179 | // 取消请求 180 | if (axios.isCancel(e)) { 181 | return console.warn(e); 182 | } 183 | reject(e); 184 | }); 185 | }); 186 | } 187 | 188 | get(url, params, config, options) { 189 | return this.request({ url, params, ...config, method: 'GET' }, options); 190 | } 191 | 192 | post(url, data, config, options) { 193 | return this.request( 194 | { 195 | url, 196 | data, 197 | ...config, 198 | headers: { 199 | ...{ 'Content-Type': CONTENTTYPE.JSON }, 200 | ...(config?.headers || {}), 201 | }, 202 | method: 'POST', 203 | }, 204 | options, 205 | ); 206 | } 207 | 208 | put(url, config, options) { 209 | return this.request({ url, ...config, method: 'PUT' }, options); 210 | } 211 | 212 | delete(url, config, options) { 213 | return this.request({ url, ...config, method: 'DELETE' }, options); 214 | } 215 | 216 | upload(url, params, config) { 217 | if (device === 'service' || !FormData) { 218 | return Promise.reject('仅支持客户端上传'); 219 | } 220 | 221 | const formData = new FormData(); 222 | const fileName = params.name || 'file'; 223 | if (params.fileName) { 224 | formData.append(fileName, params.file, params.fileName); 225 | } else { 226 | formData.append(fileName, params.file); 227 | } 228 | 229 | if (params.data) { 230 | Object.keys(params.data).forEach((key) => { 231 | const value = params.data[key]; 232 | if (Array.isArray(value)) { 233 | value.forEach((item) => { 234 | formData.append(`${key}[]`, item); 235 | }); 236 | return; 237 | } 238 | 239 | formData.append(key, params.data[key]); 240 | }); 241 | } 242 | 243 | return this.request( 244 | { 245 | url, 246 | ...config, 247 | method: 'POST', 248 | data: formData, 249 | headers: { 250 | 'Content-type': CONTENTTYPE.FORM_DATA, 251 | }, 252 | }, 253 | { 254 | ignoreCancelToken: true, 255 | }, 256 | ); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/Request/index.md: -------------------------------------------------------------------------------- 1 | # request 2 | 3 | ```jsx 4 | import { request } from '@bit/request'; 5 | import { useEffect, useState } from 'react'; 6 | 7 | export default () => { 8 | const [json, setJson] = useState(null); 9 | const [data, setData] = useState(null); 10 | const [data1, setPostData] = useState(null); 11 | const [data2, setPostFormData] = useState(null); 12 | 13 | const fetchJson = () => { 14 | request 15 | .get('/json/languse/bitruedemand/assetexport/en_US.json') 16 | .then((res) => { 17 | setJson(JSON.stringify(res)); 18 | }); 19 | }; 20 | 21 | const fetchData = () => { 22 | request 23 | .get('/api/exchange-web/web/activity/getUserLoginStatus', { test: true }) 24 | .then((res) => { 25 | setData(JSON.stringify(res)); 26 | }); 27 | }; 28 | 29 | const postData = () => { 30 | request 31 | .post('/feapi/fe-co-api/common/public_info', { 32 | securityInfo: '', 33 | type: '1,2,3', 34 | uaTime: new Date(), 35 | }) 36 | .then((res) => { 37 | setPostData(JSON.stringify(res)); 38 | }); 39 | }; 40 | 41 | const postFormData = () => { 42 | request 43 | .post( 44 | '/feapi/fe-co-api/common/public_info', 45 | { 46 | securityInfo: '', 47 | type: '1,2,3', 48 | uaTime: new Date(), 49 | }, 50 | { 51 | headers: { 52 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 53 | }, 54 | }, 55 | ) 56 | .then((res) => { 57 | setPostFormData(JSON.stringify(res)); 58 | }); 59 | }; 60 | 61 | return ( 62 |
63 |
64 | 65 | json: 66 |
67 | {json} 68 |
69 |
70 |
71 | 72 | data: 73 |
74 | {data} 75 |
76 |
77 |
78 | 79 | data: 80 |
81 | {data1} 82 |
83 |
84 |
85 | 86 | data: 87 |
88 | {data2} 89 |
90 |
91 | ); 92 | }; 93 | ``` 94 | 95 | ## request 请求方法 96 | 97 | `get(url, params, config, options)` 98 | 99 | `post(url, data, config, options)` 100 | 101 | `put(url, config, options)` 102 | 103 | `delete(url, config, options)` 104 | 105 | `upload(url, params, config)` 上传 params 中必须包含 file 属性 106 | 107 | ## options 108 | 109 | | 参数 | 说明 | 类型 | 默认值 | 110 | | ---------------------- | --------------------- | -------------------- | ----------------- | 111 | | publicParams | 请求携带的公共参数 | Boolean/Object | 见下 publicparams | 112 | | urlPrefix | 请求前缀 | String | '' | 113 | | isReturnNativeResponse | 是否返回原生 response | Boolean | false | 114 | | errorMessageMode | 错误消息提示类型 | 'toast'/'log'/'none' | 'none' | 115 | | ignoreCancelToken | 允许重复请求 | Boolean | false | 116 | | retryRequest | 失败是否重试 | Boolean/Number | true | 117 | 118 | ## 公共业务参数 publicparams 119 | 120 | 浏览器环境登录用户默认携带设备参数 121 | 122 | | 参数 | 含义 | 默认值 | 123 | | ------------------ | ---------------------- | -------------------------- | 124 | | token | token | token_url | 125 | | appName | 浏览器名称 | navigator.appName | 126 | | appCodeName | 浏览器代码名称 | navigator.appCodeName | 127 | | appVersion | 浏览器版本号 | navigator.appVersion | 128 | | userAgent | 浏览器版本信息 | navigator.userAgent | 129 | | cookieEnabled | 浏览器是否启用 cookie | navigator.cookieEnabled | 130 | | platform | 客户端操作系统 | navigator.platform | 131 | | userLanguage | 浏览器语言 | navigator.language | 132 | | vendor | 浏览器厂商 | navigator.vendor | 133 | | onLine | 浏览器是否连接网络 | navigator.onLine | 134 | | product | 浏览器产品名称 | navigator.product | 135 | | productSub | 浏览器产品其他信息 | navigator.productSub | 136 | | mimeTypesLen | 浏览器的 MIME 类型数量 | navigator.mimeTypes.length | 137 | | pluginsLen | 浏览器的插件数量 | navigator.plugins.length | 138 | | javaEnbled | 浏览器是否启用 JAVA | navigator.javaEnabled() | 139 | | windowScreenWidth | 屏幕分辨率 - 宽 | window.screen.width | 140 | | windowScreenHeight | 屏幕分辨率 - 高 | window.screen.width | 141 | | windowColorDepth | 屏幕色彩位数 | window.screen.colorDepth | 142 | 143 | ## 自定义拦截器 144 | 145 | `request.setRequestHooks()` 可以设置自定义 hooks; 146 | 147 | | 自定义 hook | trigger | 参数 | 返回值 | 148 | | ----------------- | --------------- | ----------------------------------- | --------------- | 149 | | beforeRequestHook | request 发起前 | (config, option) => {return config} | 返回最新 config | 150 | | afterRequestHook | response 返回后 | (res, option) => {return res} | 返回最终 res | 151 | 152 | ```jsx 153 | import { request } from '@bit/request'; 154 | import { useEffect, useState } from 'react'; 155 | export default () => { 156 | const [hooks, setHooks] = useState(false); 157 | const [json, setJson] = useState(null); 158 | 159 | const fetchJson = () => { 160 | request 161 | .get('/json/languse/bitruedemand/assetexport/en_US.json') 162 | .then((res) => { 163 | setJson(JSON.stringify(res)); 164 | }); 165 | }; 166 | 167 | const setHook = () => { 168 | setHooks(true); 169 | const beforeRequestHook = (config, options) => { 170 | config.cusHook = true; 171 | return config; 172 | }; 173 | const afterRequestHook = (res) => { 174 | res.cusHook = true; 175 | return res; 176 | }; 177 | 178 | request.setRequestHooks({ 179 | beforeRequestHook, 180 | afterRequestHook, 181 | }); 182 | }; 183 | 184 | return ( 185 |
186 | 187 |
188 | 189 | json: 190 |
191 | {json} 192 |
193 |
194 | ); 195 | }; 196 | ``` 197 | 198 | ## response 数据格式 199 | 200 | 统一数据格式 `{code:0, data, msg}`,对于响应数据格式不符合要求的接口,请联系服务端调整接口数据格式,或者通过 transformData 手动更改响应数据 201 | 202 | ## transformData 203 | 204 | ```jsx 205 | import { request } from '@bit/request'; 206 | import { useEffect, useState } from 'react'; 207 | export default () => { 208 | const [hooks, setHooks] = useState(false); 209 | const [json, setJson] = useState(null); 210 | 211 | const fetchJson = () => { 212 | request 213 | .get('/json/languse/bitruedemand/assetexport/en_US.json', null, null, { 214 | transformData: (res) => { 215 | return { 216 | code: 0, 217 | data: res, 218 | message: 'success', 219 | }; 220 | }, 221 | }) 222 | .then((res) => { 223 | setJson(JSON.stringify(res)); 224 | }) 225 | .catch((err) => {}); 226 | }; 227 | 228 | return ( 229 |
230 |
231 | 232 | json: 233 |
234 | {json} 235 |
236 |
237 | ); 238 | }; 239 | ``` 240 | 241 | ## 上传 242 | 243 | ```jsx 244 | import { request } from '@bit/request'; 245 | import { useRef } from 'react'; 246 | export default () => { 247 | const fileRef = useRef(); 248 | 249 | const handleUpload = (e) => { 250 | e.preventDefault(); 251 | const file = fileRef.current.files[0]; 252 | if (file) { 253 | request 254 | .upload('/api/exchange-web/web/userCenterGoogle/handleFileUpload', { 255 | file: file, 256 | }) 257 | .then((res) => { 258 | console.log(res); 259 | }); 260 | } 261 | }; 262 | 263 | return ( 264 |
265 |
266 | 267 | 268 |
269 |
270 | ); 271 | }; 272 | ``` 273 | 274 | ## 异常流 275 | 276 | ```jsx 277 | import { request } from '@bit/request'; 278 | import { useEffect, useState } from 'react'; 279 | 280 | export default () => { 281 | const [errorMode, setErrorMode] = useState('none'); 282 | const [error, setError] = useState(null); 283 | 284 | const fetchJson = () => { 285 | request 286 | .get('/json/languse/bitruedemand/assetexport/en_US1.json', null, null, { 287 | errorMessageMode: errorMode, 288 | }) 289 | .then((res) => { 290 | // ... 291 | }) 292 | .catch((err) => { 293 | setError(err); 294 | }); 295 | }; 296 | 297 | const fetchData = () => { 298 | request 299 | .get('/api/exchange-web/web/activity/notgetUserLoginStatus', null, null, { 300 | errorMessageMode: errorMode, 301 | }) 302 | .then((res) => { 303 | // ... 304 | }) 305 | .catch((err) => { 306 | setError(err); 307 | }); 308 | }; 309 | 310 | const postData = () => { 311 | request 312 | .post( 313 | '/feapi/fe-co-api/common/public_info1', 314 | { 315 | securityInfo: '', 316 | type: '1,2,3', 317 | uaTime: new Date(), 318 | }, 319 | null, 320 | { errorMessageMode: errorMode }, 321 | ) 322 | .then((res) => { 323 | // ... 324 | }) 325 | .catch((err) => { 326 | setError(err); 327 | }); 328 | }; 329 | 330 | const switchErrorMode = (mode) => () => setErrorMode(mode); 331 | 332 | return ( 333 |
334 |
335 |
errorMode: {errorMode}
336 |
337 |
errorStatus: {JSON.stringify(error)}
338 |
339 | 340 | 341 | 342 |
343 |
344 |
345 | 346 |
347 |
348 |
349 | 350 |
351 |
352 |
353 | 354 |
355 |
356 | ); 357 | }; 358 | ``` 359 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Development 2 | 3 | ```bash 4 | # install dependencies 5 | $ yarn install 6 | 7 | # develop library by docs demo 8 | $ yarn start 9 | 10 | # build library source code 11 | $ yarn run build 12 | 13 | # build library source code in watch mode 14 | $ yarn run build:watch 15 | 16 | # build docs 17 | $ yarn run docs:build 18 | 19 | # check your project for potential problems 20 | $ yarn run doctor 21 | ``` 22 | 23 | # request 24 | 25 | ```jsx 26 | import { request } from '@bit/request'; 27 | import { useEffect, useState } from 'react'; 28 | 29 | export default () => { 30 | const [json, setJson] = useState(null); 31 | const [data, setData] = useState(null); 32 | const [data1, setPostData] = useState(null); 33 | const [data2, setPostFormData] = useState(null); 34 | 35 | const fetchJson = () => { 36 | request 37 | .get('/json/languse/test/assetexport/en_US.json') 38 | .then((res) => { 39 | setJson(JSON.stringify(res)); 40 | }); 41 | }; 42 | 43 | const fetchData = () => { 44 | request 45 | .get('/api/exchange-web/web/activity/getUserLoginStatus', { test: true }) 46 | .then((res) => { 47 | setData(JSON.stringify(res)); 48 | }); 49 | }; 50 | 51 | const postData = () => { 52 | request 53 | .post('/feapi/fe-co-api/common/public_info', { 54 | securityInfo: '', 55 | type: '1,2,3', 56 | uaTime: new Date(), 57 | }) 58 | .then((res) => { 59 | setPostData(JSON.stringify(res)); 60 | }); 61 | }; 62 | 63 | const postFormData = () => { 64 | request 65 | .post( 66 | '/feapi/fe-co-api/common/public_info', 67 | { 68 | securityInfo: '', 69 | type: '1,2,3', 70 | uaTime: new Date(), 71 | }, 72 | { 73 | headers: { 74 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 75 | }, 76 | }, 77 | ) 78 | .then((res) => { 79 | setPostFormData(JSON.stringify(res)); 80 | }); 81 | }; 82 | 83 | return ( 84 |
85 |
86 | 87 | json: 88 |
89 | {json} 90 |
91 |
92 |
93 | 94 | data: 95 |
96 | {data} 97 |
98 |
99 |
100 | 101 | data: 102 |
103 | {data1} 104 |
105 |
106 |
107 | 108 | data: 109 |
110 | {data2} 111 |
112 |
113 | ); 114 | }; 115 | ``` 116 | 117 | ## request 请求方法 118 | 119 | `get(url, params, config, options)` 120 | 121 | `post(url, data, config, options)` 122 | 123 | `put(url, config, options)` 124 | 125 | `delete(url, config, options)` 126 | 127 | `upload(url, params, config)` 上传 params 中必须包含 file 属性 128 | 129 | ## options 130 | 131 | | 参数 | 说明 | 类型 | 默认值 | 132 | | ---------------------- | --------------------- | -------------------- | ----------------- | 133 | | publicParams | 请求携带的公共参数 | Boolean/Object | 见下 publicparams | 134 | | urlPrefix | 请求前缀 | String | '' | 135 | | isReturnNativeResponse | 是否返回原生 response | Boolean | false | 136 | | errorMessageMode | 错误消息提示类型 | 'toast'/'log'/'none' | 'none' | 137 | | ignoreCancelToken | 允许重复请求 | Boolean | false | 138 | | retryRequest | 失败是否重试 | Boolean/Number | true | 139 | 140 | ## 公共业务参数 publicparams 141 | 142 | 浏览器环境登录用户默认携带设备参数 143 | 144 | | 参数 | 含义 | 默认值 | 145 | | ------------------ | ----------------------| -------------------------- | 146 | | token | token | token_url | 147 | | appName | 浏览器名称 | navigator.appName | 148 | | appCodeName | 浏览器代码名称 | navigator.appCodeName | 149 | | appVersion | 浏览器版本号 | navigator.appVersion | 150 | | userAgent | 浏览器版本信息 | navigator.userAgent | 151 | | cookieEnabled | 浏览器是否启用 cookie | navigator.cookieEnabled | 152 | | platform | 客户端操作系统 | navigator.platform | 153 | | userLanguage | 浏览器语言 | navigator.language | 154 | | vendor | 浏览器厂商 | navigator.vendor | 155 | | onLine | 浏览器是否连接网络 | navigator.onLine | 156 | | product | 浏览器产品名称 | navigator.product | 157 | | productSub | 浏览器产品其他信息 | navigator.productSub | 158 | | mimeTypesLen | 浏览器的 MIME 类型数量 | navigator.mimeTypes.length | 159 | | pluginsLen | 浏览器的插件数量 | navigator.plugins.length | 160 | | javaEnbled | 浏览器是否启用 JAVA | navigator.javaEnabled() | 161 | | windowScreenWidth | 屏幕分辨率 - 宽 | window.screen.width | 162 | | windowScreenHeight | 屏幕分辨率 - 高 | window.screen.width | 163 | | windowColorDepth | 屏幕色彩位数 | window.screen.colorDepth | 164 | 165 | ## 自定义拦截器 166 | 167 | `request.setRequestHooks()` 可以设置自定义 hooks; 168 | 169 | | 自定义 hook | trigger | 参数 | 返回值 | 170 | | ----------------- | ---------------| ----------------------------------- | ---------------| 171 | | beforeRequestHook | request 发起前 | (config, option) => {return config} | 返回最新 config | 172 | | afterRequestHook | response 返回后 | (res, option) => {return res} | 返回最终 res | 173 | 174 | ```jsx 175 | import { request } from '@bit/request'; 176 | import { useEffect, useState } from 'react'; 177 | export default () => { 178 | const [hooks, setHooks] = useState(false); 179 | const [json, setJson] = useState(null); 180 | 181 | const fetchJson = () => { 182 | request 183 | .get('/json/languse/tets/assetexport/en_US.json') 184 | .then((res) => { 185 | setJson(JSON.stringify(res)); 186 | }); 187 | }; 188 | 189 | const setHook = () => { 190 | setHooks(true); 191 | const beforeRequestHook = (config, options) => { 192 | config.cusHook = true; 193 | return config; 194 | }; 195 | const afterRequestHook = (res) => { 196 | res.cusHook = true; 197 | return res; 198 | }; 199 | 200 | request.setRequestHooks({ 201 | beforeRequestHook, 202 | afterRequestHook, 203 | }); 204 | }; 205 | 206 | return ( 207 |
208 | 209 |
210 | 211 | json: 212 |
213 | {json} 214 |
215 |
216 | ); 217 | }; 218 | ``` 219 | 220 | ## response 数据格式 221 | 222 | 统一数据格式 `{code:0, data, msg}`,对于响应数据格式不符合要求的接口,请联系服务端调整接口数据格式,或者通过 transformData 手动更改响应数据 223 | 224 | ## transformData 225 | 226 | ```jsx 227 | import { request } from '@bit/request'; 228 | import { useEffect, useState } from 'react'; 229 | export default () => { 230 | const [hooks, setHooks] = useState(false); 231 | const [json, setJson] = useState(null); 232 | 233 | const fetchJson = () => { 234 | request 235 | .get('/json/languse/test/assetexport/en_US.json', null, null, { 236 | transformData: (res) => { 237 | return { 238 | code: 0, 239 | data: res, 240 | message: 'success', 241 | }; 242 | }, 243 | }) 244 | .then((res) => { 245 | setJson(JSON.stringify(res)); 246 | }) 247 | .catch((err) => {}); 248 | }; 249 | 250 | return ( 251 |
252 |
253 | 254 | json: 255 |
256 | {json} 257 |
258 |
259 | ); 260 | }; 261 | ``` 262 | 263 | ## 文件上传 264 | 265 | ```jsx 266 | import { request } from '@bit/request'; 267 | import { useRef } from 'react'; 268 | export default () => { 269 | const fileRef = useRef(); 270 | 271 | const handleUpload = (e) => { 272 | e.preventDefault(); 273 | const file = fileRef.current.files[0]; 274 | if (file) { 275 | request 276 | .upload('/api/exchange-web/web/userCenterGoogle/handleFileUpload', { 277 | file: file, 278 | }) 279 | .then((res) => { 280 | console.log(res); 281 | }); 282 | } 283 | }; 284 | 285 | return ( 286 |
287 |
288 | 289 | 290 |
291 |
292 | ); 293 | }; 294 | ``` 295 | 296 | ## 异常流处理 297 | 298 | ```jsx 299 | import { request } from '@bit/request'; 300 | import { useEffect, useState } from 'react'; 301 | 302 | export default () => { 303 | const [errorMode, setErrorMode] = useState('none'); 304 | const [error, setError] = useState(null); 305 | 306 | const fetchJson = () => { 307 | request 308 | .get('/json/languse/test/assetexport/en_US1.json', null, null, { 309 | errorMessageMode: errorMode, 310 | }) 311 | .then((res) => { 312 | // ... 313 | }) 314 | .catch((err) => { 315 | setError(err); 316 | }); 317 | }; 318 | 319 | const fetchData = () => { 320 | request 321 | .get('/api/exchange-web/web/activity/notgetUserLoginStatus', null, null, { 322 | errorMessageMode: errorMode, 323 | }) 324 | .then((res) => { 325 | // ... 326 | }) 327 | .catch((err) => { 328 | setError(err); 329 | }); 330 | }; 331 | 332 | const postData = () => { 333 | request 334 | .post( 335 | '/feapi/fe-co-api/common/public_info1', 336 | { 337 | securityInfo: '', 338 | type: '1,2,3', 339 | uaTime: new Date(), 340 | }, 341 | null, 342 | { errorMessageMode: errorMode }, 343 | ) 344 | .then((res) => { 345 | // ... 346 | }) 347 | .catch((err) => { 348 | setError(err); 349 | }); 350 | }; 351 | 352 | const switchErrorMode = (mode) => () => setErrorMode(mode); 353 | 354 | return ( 355 |
356 |
357 |
errorMode: {errorMode}
358 |
359 |
errorStatus: {JSON.stringify(error)}
360 |
361 | 362 | 363 | 364 |
365 |
366 |
367 | 368 |
369 |
370 |
371 | 372 |
373 |
374 |
375 | 376 |
377 |
378 | ); 379 | }; 380 | ``` 381 | 382 | -------------------------------------------------------------------------------- /src/hooks/index.md: -------------------------------------------------------------------------------- 1 | # useRequest 2 | 3 | api: `{data, error, loading, params, cancel, refresh, run } = useRequest(service, options)`; 4 | 5 | ## 基础用法 6 | 7 | 使用`useRequest(service, options)`来自动管理服务端状态; 8 | 9 | ```jsx 10 | /** 11 | * defaultShowCode: true 12 | */ 13 | import { request, useRequest } from '@bit/request'; 14 | 15 | const service = (...params) => 16 | request.get('/api/exchange-web/web/activity/getUserLoginStatus', ...params); 17 | 18 | export default () => { 19 | const { data, loading, error } = useRequest(service); 20 | return ( 21 |
22 | userName:{' '} 23 | {loading ? 'loading' : error ? error.message : JSON.stringify(data?.data)} 24 |
25 | ); 26 | }; 27 | ``` 28 | 29 | 使用`useRequest([url, config, requestOptions], options)`; 30 | 31 | ```jsx 32 | /** 33 | * defaultShowCode: true 34 | */ 35 | import { request, useRequest } from '@bit/request'; 36 | 37 | export default () => { 38 | const { data, loading, error } = useRequest([ 39 | '/api/exchange-web/web/coin/symbolForAsset', 40 | { method: 'GET', params: {} }, 41 | ]); 42 | 43 | return ( 44 |
45 | symbols:{' '} 46 | {loading 47 | ? 'loading' 48 | : error 49 | ? error.message 50 | : JSON.stringify(Object.keys(data))} 51 |
52 | ); 53 | }; 54 | ``` 55 | 56 | ## 失败/成功回调 57 | 58 | 通过设置`options.onSuccess`和`options.onError`触发请求回调 59 | 60 | ```jsx 61 | /** 62 | * defaultShowCode: true 63 | */ 64 | import { request, useRequest } from '@bit/request'; 65 | 66 | const service = (...params) => 67 | request.get('/api/exchange-web/web/activity/getUserLoginStatus', ...params); 68 | 69 | export default () => { 70 | const { data, loading, error, run } = useRequest(service, { 71 | onSuccess: (res, params) => { 72 | console.log(res, params); 73 | }, 74 | onError: (error, params) => { 75 | console.log(error, params); 76 | }, 77 | manual: true, 78 | }); 79 | 80 | const fetchUserInfo = () => { 81 | if (loading) return; 82 | run(); 83 | }; 84 | return ( 85 |
86 | 87 |
88 | userName: 89 | {loading 90 | ? 'loading' 91 | : error 92 | ? error.message 93 | : JSON.stringify(data?.data)} 94 |
95 |
96 | ); 97 | }; 98 | ``` 99 | 100 | ## 手动触发 101 | 102 | 通过设置`options.manual = true`手动触发请求 103 | 104 | ```jsx 105 | /** 106 | * defaultShowCode: true 107 | */ 108 | import { request, useRequest } from '@bit/request'; 109 | 110 | const service = (...params) => 111 | request.get('/api/exchange-web/web/activity/getUserLoginStatus', ...params); 112 | 113 | export default () => { 114 | const { data, loading, error, run } = useRequest(service, { manual: true }); 115 | 116 | const fetchUserInfo = () => { 117 | if (loading) return; 118 | run(); 119 | }; 120 | 121 | return ( 122 |
123 | 124 |
125 | userName:{' '} 126 | {loading 127 | ? 'loading' 128 | : error 129 | ? error.message 130 | : JSON.stringify(data?.data)} 131 |
132 |
133 | ); 134 | }; 135 | ``` 136 | 137 | ## 延迟 loading 138 | 139 | 通过设置`options.loadingDeley = 3000`延迟 loading 加载效果, 适合无感知状态切换,减少用户等待的焦灼感 140 | 141 | ```jsx 142 | /** 143 | * defaultShowCode: true 144 | */ 145 | import { useState, useRef, useCallback } from 'react'; 146 | import { request, useRequest } from '@bit/request'; 147 | 148 | const service = (...params) => 149 | request.get('/api/exchange-web/web/activity/getUserLoginStatus', ...params); 150 | 151 | export default () => { 152 | const [loadingDeley, setLoadingDeley] = useState(false); 153 | const loadingDeleyRef = useRef(0); 154 | const { data, loading, error, run } = useRequest(service, { 155 | loadingDeley: loadingDeleyRef.current, 156 | manual: true, 157 | }); 158 | 159 | const setDeley = useCallback(() => { 160 | setLoadingDeley(!loadingDeley); 161 | loadingDeleyRef.current = !loadingDeley ? 2000 : 0; 162 | }, [loadingDeley]); 163 | 164 | const runRequest = () => run(); 165 | 166 | return ( 167 |
168 | 171 |
172 | 173 |
174 | userName:{' '} 175 | {loading 176 | ? 'loading' 177 | : error 178 | ? error.message 179 | : JSON.stringify(data?.data)} 180 |
181 |
182 | ); 183 | }; 184 | ``` 185 | 186 | ## 依赖更新自动加载 187 | 188 | 支持`option.ready`和`option.refreshDeps`两种依赖更新方式,设置了 ready 后,manual 请求需要在 ready 为 true 的状态下,run 才会发起请求。 189 | `option.refreshDeps`会在初始化和依赖项发生变化时触发请求。 190 | 191 | ```jsx 192 | /** 193 | * defaultShowCode: true 194 | */ 195 | import { useState, useRef, useCallback } from 'react'; 196 | import { request, useRequest } from '@bit/request'; 197 | 198 | const service = (...params) => 199 | request.get('/api/exchange-web/web/activity/getUserLoginStatus', ...params); 200 | 201 | export default () => { 202 | const [ready, setReady] = useState(false); 203 | const [count, setCount] = useState(0); 204 | const { data, loading, error } = useRequest(service, { 205 | ready: ready, 206 | }); 207 | 208 | const { 209 | data: data1, 210 | loading: loading1, 211 | error: error1, 212 | } = useRequest(service, { 213 | ready: ready, 214 | refreshDeps: [count], 215 | }); 216 | 217 | const handleReady = () => { 218 | setReady(true); 219 | }; 220 | 221 | const handleCount = useCallback(() => { 222 | setCount(count + 1); 223 | }, [count]); 224 | 225 | return ( 226 |
227 |
228 | 229 |

ready:{`${ready}`}

230 |
231 | userName:{' '} 232 | {loading 233 | ? 'loading' 234 | : error 235 | ? error.message 236 | : JSON.stringify(data?.data)} 237 |
238 |
239 |
240 |
241 | 242 |

count:{count}

243 |
244 | userName:{' '} 245 | {loading1 246 | ? 'loading' 247 | : error1 248 | ? error1.message 249 | : JSON.stringify(data1?.data)} 250 |
251 |
252 |
253 | ); 254 | }; 255 | ``` 256 | 257 | ## 防抖 258 | 259 | 通过设置`option.debounceWait=300`开启请求防抖 260 | 261 | ```jsx 262 | /** 263 | * defaultShowCode: true 264 | */ 265 | import { useState, useRef, useCallback } from 'react'; 266 | import { request, useRequest } from '@bit/request'; 267 | 268 | const service = (...params) => 269 | request.get('/api/exchange-web/web/activity/getUserLoginStatus', ...params); 270 | 271 | export default () => { 272 | const [value, setValue] = useState(undefined); 273 | const [ready, setReady] = useState(false); 274 | const { data, loading, error } = useRequest(service, { 275 | ready, 276 | refreshDeps: [value], 277 | debounceWait: 300, 278 | }); 279 | 280 | const handleInput = (e) => { 281 | const value = e.target.value; 282 | setReady(true); 283 | setValue(value); 284 | }; 285 | 286 | return ( 287 |
288 | 289 |
290 | userName:{' '} 291 | {loading 292 | ? 'loading' 293 | : error 294 | ? error.message 295 | : JSON.stringify(data?.data)} 296 |
297 |
298 | ); 299 | }; 300 | ``` 301 | 302 | ## 节流 303 | 304 | 通过设置`option.throttleWait=300`开启请求节流 305 | 306 | ```jsx 307 | /** 308 | * defaultShowCode: true 309 | */ 310 | import { useState, useRef, useCallback } from 'react'; 311 | import { request, useRequest } from '@bit/request'; 312 | 313 | const service = (...params) => 314 | request.get('/api/exchange-web/web/activity/getUserLoginStatus', ...params); 315 | 316 | export default () => { 317 | const { data, loading, error, run } = useRequest(service, { 318 | manual: true, 319 | throttleWait: 1000 * 2, 320 | }); 321 | 322 | const handleMouse = (e) => { 323 | run(); 324 | }; 325 | 326 | return ( 327 |
328 |
332 |
333 | userName:{' '} 334 | {loading 335 | ? 'loading' 336 | : error 337 | ? error.message 338 | : JSON.stringify(data?.data)} 339 |
340 |
341 | ); 342 | }; 343 | ``` 344 | 345 | ## 缓存 346 | 347 | 通过设置`option.cacheKey`开启请求缓存,通过设置`option.catchTime`设置缓存时间,超出这个时间会清空缓存,通过设置`option.staleTime`设置数据保鲜时间,在这个时间内不会重新发起请求,直接返回缓存数据;超过`staleTime`保鲜时间但是小于`catchTime`的请求会正常发起,但是会优先返回缓存数据,等请求数据返回后自动更新状态;同一个`cacheKey`的内容,在全局是共享的。 348 | 默认值:`option.catchTime = 5 * 60 * 1000`, `option.staleTime = 0`; 349 | 350 | ```jsx 351 | /** 352 | * defaultShowCode: true 353 | */ 354 | import { useState, useRef, useCallback } from 'react'; 355 | import { request, useRequest } from '@bit/request'; 356 | 357 | const service = (...params) => { 358 | return new Promise((resolve) => { 359 | setTimeout(() => { 360 | resolve({ 361 | data: Math.random() * 100, 362 | time: new Date().toString(), 363 | }); 364 | }, 1000); 365 | }); 366 | }; 367 | 368 | export default () => { 369 | const currRef = useRef(new Date().getTime()); 370 | const [duration, setDuration] = useState(0); 371 | const { data, loading, error, run } = useRequest(service, { 372 | cacheKey: 'random', 373 | catchTime: 1000 * 60, 374 | staleTime: 1000 * 30, 375 | manual: true, 376 | }); 377 | 378 | const handleRefresh = () => { 379 | const Now = new Date().getTime(); 380 | setDuration(Now - currRef.current); 381 | run(); 382 | }; 383 | 384 | const handleRun = () => { 385 | const Now = new Date().getTime(); 386 | currRef.current = Now; 387 | run(); 388 | }; 389 | 390 | return ( 391 |
392 | 393 | 394 |
request Interval: {duration}
395 |
396 | data: {loading && !data ? 'loading' : JSON.stringify(data?.data)} 397 |
398 |
399 | time: {loading && !data ? 'loading' : JSON.stringify(data?.time)} 400 |
401 |
402 | ); 403 | }; 404 | ``` 405 | 406 | ## 轮询 407 | 408 | 通过设置`option.pollingInterval=3000`开启请求轮训 409 | 410 | ```jsx 411 | /** 412 | * defaultShowCode: true 413 | */ 414 | import { useState, useRef, useCallback } from 'react'; 415 | import { request, useRequest } from '@bit/request'; 416 | 417 | const service = (...params) => 418 | request.get('/api/exchange-web/web/activity/getUserLoginStatus', ...params); 419 | 420 | export default () => { 421 | const { data, loading, error, run, cancel } = useRequest(service, { 422 | manual: true, 423 | pollingInterval: 1000 * 5, 424 | }); 425 | 426 | const handleStartPolling = () => { 427 | run(); 428 | }; 429 | 430 | const handleStopPolling = () => { 431 | cancel(); 432 | }; 433 | 434 | return ( 435 |
436 | 437 | 438 |
439 | userName:{' '} 440 | {loading 441 | ? 'loading' 442 | : error 443 | ? error.message 444 | : JSON.stringify(data?.data)} 445 |
446 |
447 | ); 448 | }; 449 | ``` 450 | 451 | ## 屏幕重新聚焦自动请求 452 | 453 | 通过设置`option.refreshOnWindowFocus=true`,在浏览器窗口 refocus 和 revisible 时,会重新发起请求。默认间隔是 1000 \* 10ms, 可以通过设置`option.focusTimespan`改变间隔时间 454 | 455 | ```jsx 456 | /** 457 | * defaultShowCode: true 458 | */ 459 | 460 | import { useState, useRef, useCallback } from 'react'; 461 | import { request, useRequest } from '@bit/request'; 462 | 463 | const service = (...params) => 464 | request.get('/api/exchange-web-gateway/hackerCompensate/info', ...params); 465 | 466 | export default () => { 467 | const { data, loading, error } = useRequest(service, { 468 | refreshOnWindowFocus: true, 469 | }); 470 | 471 | return ( 472 |
473 | info:{' '} 474 | {loading ? 'loading' : error ? error.message : JSON.stringify(data?.data)} 475 |
476 | ); 477 | }; 478 | ``` 479 | --------------------------------------------------------------------------------