diff --git a/packages/alova/package.json b/packages/alova/package.json new file mode 100644 index 00000000..ac20adc0 --- /dev/null +++ b/packages/alova/package.json @@ -0,0 +1,16 @@ +{ + "name": "@sa/alova", + "version": "0.1.0", + "exports": { + ".": "./src/index.ts" + }, + "typesVersions": { + "*": { + "*": ["./src/*"] + } + }, + "dependencies": { + "@sa/utils": "workspace:*", + "alova": "^3.0.16" + } +} diff --git a/packages/alova/src/constant.ts b/packages/alova/src/constant.ts new file mode 100644 index 00000000..d6c5a338 --- /dev/null +++ b/packages/alova/src/constant.ts @@ -0,0 +1,5 @@ +/** request id key */ +export const REQUEST_ID_KEY = 'X-Request-Id'; + +/** 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..5ec59335 --- /dev/null +++ b/packages/alova/src/index.ts @@ -0,0 +1,83 @@ +import { createAlova } from 'alova'; +import VueHook from 'alova/vue'; +import adapterFetch from 'alova/fetch'; +import { nanoid } from '@sa/utils'; +import { createServerTokenAuthentication } from 'alova/client'; +import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant'; +import { isJSON } from './shared'; + +export interface CustomAlovaConfig { + baseURL: string; + headers?: Record; + timeout?: number; + expiredTokenCodes: string[]; +} + +export interface RequestOptions { + refreshTokenHandler: () => Promise; + onBackendFail: (response: Response) => Promise; +} + +export const createAlovaRequest = (customConfig: CustomAlovaConfig, options: RequestOptions) => { + const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication({ + refreshTokenOnSuccess: { + isExpired: async response => { + const contentType = response.headers.get('Content-Type'); + if (isJSON(contentType ?? '')) { + const resp = response.clone(); + const data = await resp.json(); + const responseCode = String(data.code); + if (customConfig.expiredTokenCodes.includes(responseCode)) { + return true; + } + } + return false; + }, + handler: async () => { + await options.refreshTokenHandler(); + } + }, + // 附加token + assignToken: method => { + const token = localStorage.getItem('SOY_token'); + method.config.headers.Authorization = token ? `Bearer ${JSON.parse(token)}` : null; + } + }); + + const instance = createAlova({ + baseURL: customConfig.baseURL, + timeout: customConfig.timeout ?? 10 * 1000, + requestAdapter: adapterFetch(), + statesHook: VueHook, + beforeRequest: onAuthRequired(({ config }) => { + // 添加配置headers + config.headers = { + ...config.headers, + ...customConfig.headers + }; + + // set request id + const requestId = nanoid(); + config.headers[REQUEST_ID_KEY] = requestId; + }), + responded: onResponseRefreshToken({ + onSuccess: async resp => { + if (resp.ok || resp.status === 304) { + if (isJSON(resp.headers.get('Content-Type') ?? '')) { + const fail = await options.onBackendFail(resp); + if (fail) { + return fail; + } + return await resp.json(); + } + return resp; + } + return Promise.reject(resp); + } + }) + }); + + return instance; +}; + +export { BACKEND_ERROR_CODE, REQUEST_ID_KEY }; diff --git a/packages/alova/src/shared.ts b/packages/alova/src/shared.ts new file mode 100644 index 00000000..70e628ef --- /dev/null +++ b/packages/alova/src/shared.ts @@ -0,0 +1,3 @@ +export function isJSON(contentType: string) { + return contentType.includes('application/json'); +} 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"] +}