feat(packages): add @sa/alova

This commit is contained in:
allenli178 2024-09-22 16:51:11 +08:00
parent 0ac95bdcf5
commit 494db95614
5 changed files with 127 additions and 0 deletions

View File

@ -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"
}
}

View File

@ -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';

View File

@ -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<string, string>;
timeout?: number;
expiredTokenCodes: string[];
}
export interface RequestOptions {
refreshTokenHandler: () => Promise<boolean>;
onBackendFail: (response: Response) => Promise<Response | null>;
}
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 };

View File

@ -0,0 +1,3 @@
export function isJSON(contentType: string) {
return contentType.includes('application/json');
}

View File

@ -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"]
}