diff --git a/packages/alova/package.json b/packages/alova/package.json new file mode 100644 index 00000000..760ca648 --- /dev/null +++ b/packages/alova/package.json @@ -0,0 +1,17 @@ +{ + "name": "@sa/alova", + "version": "0.1.0", + "exports": { + ".": "./src/index.ts", + "./client": "./src/client.ts" + }, + "typesVersions": { + "*": { + "*": ["./src/*"] + } + }, + "dependencies": { + "@sa/utils": "workspace:*", + "alova": "3.0.19" + } +} diff --git a/packages/alova/src/client.ts b/packages/alova/src/client.ts new file mode 100644 index 00000000..0d76ebb9 --- /dev/null +++ b/packages/alova/src/client.ts @@ -0,0 +1 @@ +export * from 'alova/client'; diff --git a/packages/alova/src/constant.ts b/packages/alova/src/constant.ts new file mode 100644 index 00000000..be5c43ca --- /dev/null +++ b/packages/alova/src/constant.ts @@ -0,0 +1,2 @@ +/** the backend error code key */ +export const BACKEND_ERROR_CODE = 'BACKEND_ERROR'; diff --git a/packages/alova/src/index.ts b/packages/alova/src/index.ts new file mode 100644 index 00000000..42642535 --- /dev/null +++ b/packages/alova/src/index.ts @@ -0,0 +1,77 @@ +import { createAlova } from 'alova'; +import type { AlovaDefaultCacheAdapter, AlovaGenerics, AlovaGlobalCacheAdapter, AlovaRequestAdapter } from 'alova'; +import VueHook from 'alova/vue'; +import type { VueHookType } from 'alova/vue'; +import adapterFetch from 'alova/fetch'; +import { createServerTokenAuthentication } from 'alova/client'; +import type { FetchRequestInit } from 'alova/fetch'; +import { BACKEND_ERROR_CODE } from './constant'; +import type { CustomAlovaConfig, RequestOptions } from './type'; + +export const createAlovaRequest = < + RequestConfig = FetchRequestInit, + ResponseType = Response, + ResponseHeader = Headers, + L1Cache extends AlovaGlobalCacheAdapter = AlovaDefaultCacheAdapter, + L2Cache extends AlovaGlobalCacheAdapter = AlovaDefaultCacheAdapter +>( + customConfig: CustomAlovaConfig< + AlovaGenerics + >, + options: RequestOptions> +) => { + const { tokenRefresher } = options; + const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication< + VueHookType, + AlovaRequestAdapter + >({ + refreshTokenOnSuccess: { + isExpired: (response, method) => tokenRefresher?.isExpired(response, method) || false, + handler: async (response, method) => tokenRefresher?.handler(response, method) + }, + refreshTokenOnError: { + isExpired: (response, method) => tokenRefresher?.isExpired(response, method) || false, + handler: async (response, method) => tokenRefresher?.handler(response, method) + } + }); + + const instance = createAlova({ + ...customConfig, + timeout: customConfig.timeout ?? 10 * 1000, + requestAdapter: (customConfig.requestAdapter as any) ?? adapterFetch(), + statesHook: VueHook, + beforeRequest: onAuthRequired(options.onRequest as any), + responded: onResponseRefreshToken({ + onSuccess: async (response, method) => { + // check if http status is success + let error: any = null; + let transformedData: any = null; + try { + if (await options.isBackendSuccess(response)) { + transformedData = await options.transformBackendResponse(response); + } else { + error = new Error('the backend request error'); + error.code = BACKEND_ERROR_CODE; + } + } catch (err) { + error = err; + } + + if (error) { + await options.onError?.(error, response, method); + throw error; + } + + return transformedData; + }, + onComplete: options.onComplete, + onError: (error, method) => options.onError?.(error, null, method) + }) + }); + + return instance; +}; + +export { BACKEND_ERROR_CODE }; +export type * from './type'; +export type * from 'alova'; diff --git a/packages/alova/src/type.ts b/packages/alova/src/type.ts new file mode 100644 index 00000000..27b84b25 --- /dev/null +++ b/packages/alova/src/type.ts @@ -0,0 +1,52 @@ +import type { AlovaGenerics, AlovaOptions, AlovaRequestAdapter, Method, ResponseCompleteHandler } from 'alova'; + +export type CustomAlovaConfig = Omit< + AlovaOptions, + 'statesHook' | 'beforeRequest' | 'responded' | 'requestAdapter' +> & { + /** request adapter. all request of alova will be sent by it. */ + requestAdapter?: AlovaRequestAdapter; +}; + +export interface RequestOptions { + /** + * The hook before request + * + * For example: You can add header token in this hook + * + * @param method alova Method Instance + */ + onRequest?: AlovaOptions['beforeRequest']; + /** + * The hook to check backend response is success or not + * + * @param response alova response + */ + isBackendSuccess: (response: AG['Response']) => Promise; + + /** The config to refresh token */ + tokenRefresher?: { + /** detect the token is expired */ + isExpired(response: AG['Response'], Method: Method): Promise | boolean; + /** refresh token handler */ + handler(response: AG['Response'], Method: Method): Promise; + }; + + /** The hook after backend request complete */ + onComplete?: ResponseCompleteHandler; + + /** + * The hook to handle error + * + * For example: You can show error message in this hook + * + * @param error + */ + onError?: (error: any, response: AG['Response'] | null, methodInstance: Method) => any | Promise; + /** + * transform backend response when the responseType is json + * + * @param response alova response + */ + transformBackendResponse: (response: AG['Response']) => any; +} diff --git a/packages/alova/tsconfig.json b/packages/alova/tsconfig.json new file mode 100644 index 00000000..5823ed54 --- /dev/null +++ b/packages/alova/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "jsx": "preserve", + "lib": ["DOM", "ESNext"], + "baseUrl": ".", + "module": "ESNext", + "moduleResolution": "node", + "resolveJsonModule": true, + "types": ["node"], + "strict": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33a9936c..2dede59c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -160,6 +160,15 @@ importers: specifier: 2.1.6 version: 2.1.6(typescript@5.6.2) + packages/alova: + dependencies: + '@sa/utils': + specifier: workspace:* + version: link:../utils + alova: + specifier: 3.0.19 + version: 3.0.19 + packages/axios: dependencies: '@sa/utils': @@ -275,6 +284,9 @@ importers: packages: + '@alova/shared@1.0.5': + resolution: {integrity: sha512-/a2Qm+xebQJ1OlIgpslK+UL1J7yhkt1/Mqdq58a22+fSVdANukmUcF4j4w1DF3lxZ04SrqP+2oJprJ8UOvM+9Q==} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -1331,6 +1343,10 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + alova@3.0.19: + resolution: {integrity: sha512-G8YEuGn06vwg/B8mvyfRfMtxq8S8t88TwdAPlncyvUKOG4Hz1rKc4aH++QF9H+9coCVTKQzDbE4pWrgl3I0kBw==} + engines: {node: '>= 18.0.0'} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -3371,6 +3387,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + rate-limiter-flexible@5.0.3: + resolution: {integrity: sha512-lWx2y8NBVlTOLPyqs+6y7dxfEpT6YFqKy3MzWbCy95sTTOhOuxufP2QvRyOHpfXpB9OUJPbVLybw3z3AVAS5fA==} + rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} @@ -4193,6 +4212,8 @@ packages: snapshots: + '@alova/shared@1.0.5': {} + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.5 @@ -5298,6 +5319,11 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + alova@3.0.19: + dependencies: + '@alova/shared': 1.0.5 + rate-limiter-flexible: 5.0.3 + ansi-colors@4.1.3: {} ansi-escapes@7.0.0: @@ -7563,6 +7589,8 @@ snapshots: queue-microtask@1.2.3: {} + rate-limiter-flexible@5.0.3: {} + rc9@2.1.2: dependencies: defu: 6.1.4