@moluoxixi/ajax-package 0.0.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @moluoxixi/ajax-package
2
+
3
+ 基于 `axios` 的可复用 HTTP 客户端包。
4
+
5
+ ## 使用
6
+
7
+ ```ts
8
+ import { getHttpService } from '@moluoxixi/ajax-package'
9
+
10
+ const session = {
11
+ token: 'token-from-app',
12
+ }
13
+
14
+ const http = getHttpService({
15
+ baseURL: 'https://api.example.com',
16
+ token: {
17
+ getToken: () => session.token,
18
+ headerName: 'Authorization',
19
+ formatToken: token => `Bearer ${token}`,
20
+ },
21
+ })
22
+
23
+ const users = await http.get('/users')
24
+ ```
25
+
26
+ ## 响应契约
27
+
28
+ 默认返回 `response.data`,不会假设业务响应结构。需要解析业务包裹层时显式声明契约:
29
+
30
+ ```ts
31
+ const http = getHttpService({
32
+ responseContract: {
33
+ codePath: 'code',
34
+ messagePath: 'message',
35
+ dataPath: 'data.items',
36
+ isSuccess: code => code === 0,
37
+ },
38
+ })
39
+ ```
40
+
41
+ ## 业务剥离范围
42
+
43
+ - 不导出 Vue 插件,不写入 `window.$http`。
44
+ - 不依赖 Element Plus、弹窗组件或通知组件。
45
+ - 不内置登录页跳转、localStorage token 读取、系统异常弹窗和特定业务错误码。
46
+ - HTTP 错误、axios 错误和响应契约错误都会继续抛出,调用方自行决定 UI 副作用。
@@ -0,0 +1,106 @@
1
+ import { AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
2
+
3
+ interface TokenConfig {
4
+ /**
5
+ * 每次请求前按需读取 token;未返回 token 时不会写入任何认证头。
6
+ */
7
+ getToken: () => Promise<string | null | undefined> | string | null | undefined;
8
+ /**
9
+ * 认证头名称默认使用通用的 Authorization,不保留旧包的业务 Token 头约定。
10
+ */
11
+ headerName?: string;
12
+ /**
13
+ * 调用方负责决定 Bearer、Token 或其他格式,工具包不猜测业务协议。
14
+ */
15
+ formatToken?: (token: string) => string;
16
+ }
17
+ interface BaseHttpClientConfig extends AxiosRequestConfig {
18
+ token?: TokenConfig;
19
+ onRequest?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
20
+ onResponse?: (response: AxiosResponse) => AxiosResponse | Promise<AxiosResponse>;
21
+ onResponseError?: (error: AxiosError) => void | Promise<void>;
22
+ onTimeout?: (error: AxiosError) => void | Promise<void>;
23
+ onUnauthorized?: (error: AxiosError) => void | Promise<void>;
24
+ }
25
+ interface HttpResponseContract {
26
+ /**
27
+ * 业务状态码路径;未配置时不做业务成功/失败判断。
28
+ */
29
+ codePath?: string;
30
+ /**
31
+ * 错误消息路径;只有业务失败时才会读取。
32
+ */
33
+ messagePath?: string;
34
+ /**
35
+ * 数据路径;配置后路径缺失会直接抛错,避免返回伪成功的 undefined。
36
+ */
37
+ dataPath?: string;
38
+ /**
39
+ * 判断业务响应是否成功;不提供时只做数据路径提取,不判断 code。
40
+ */
41
+ isSuccess?: (code: unknown, body: unknown, response: AxiosResponse) => boolean;
42
+ }
43
+ interface BaseApiConfig extends BaseHttpClientConfig {
44
+ responseContract?: HttpResponseContract;
45
+ }
46
+ type HttpRequestConfig = AxiosRequestConfig;
47
+ type HttpClientInstance = AxiosInstance;
48
+
49
+ type AwaitedPromiseTuple<Requests extends readonly Promise<unknown>[]> = {
50
+ readonly [Index in keyof Requests]: Awaited<Requests[Index]>;
51
+ };
52
+ declare class BaseHttpClient {
53
+ readonly instance: HttpClientInstance;
54
+ private readonly token;
55
+ private readonly onRequest;
56
+ private readonly onResponse;
57
+ private readonly onResponseError;
58
+ private readonly onTimeout;
59
+ private readonly onUnauthorized;
60
+ constructor(config?: BaseHttpClientConfig);
61
+ protected processRequestConfig(config: InternalAxiosRequestConfig): Promise<InternalAxiosRequestConfig>;
62
+ protected processResponseConfig<R>(response: AxiosResponse<R>): Promise<R>;
63
+ protected processResponseError(error: AxiosError): Promise<void>;
64
+ private setupInterceptors;
65
+ /**
66
+ * 所有快捷方法最终都走 request,便于子类在单点扩展请求语义。
67
+ */
68
+ request<R = unknown>(config: AxiosRequestConfig): Promise<R>;
69
+ get<R = unknown>(url: string, params?: Record<string, unknown>, config?: AxiosRequestConfig): Promise<R>;
70
+ post<R = unknown, D = unknown>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
71
+ put<R = unknown, D = unknown>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
72
+ delete<R = unknown>(url: string, params?: Record<string, unknown>, config?: AxiosRequestConfig): Promise<R>;
73
+ all<const Requests extends readonly Promise<unknown>[]>(requests: Requests): Promise<AwaitedPromiseTuple<Requests>>;
74
+ all<R = unknown>(requests: readonly AxiosRequestConfig[]): Promise<R[]>;
75
+ all<R = unknown>(requests: readonly (AxiosRequestConfig | Promise<R>)[]): Promise<R[]>;
76
+ uploadFile<R = unknown>(url: string, file: Blob, config?: AxiosRequestConfig<FormData>): Promise<R>;
77
+ downloadFile(blob: Blob, filename: string): void;
78
+ }
79
+
80
+ declare class BaseApi extends BaseHttpClient {
81
+ private readonly responseContract;
82
+ constructor(config?: BaseApiConfig);
83
+ protected processResponseConfig<R = unknown>(response: AxiosResponse<R>): Promise<R>;
84
+ }
85
+
86
+ interface HttpResponseErrorOptions {
87
+ body: unknown;
88
+ code: unknown;
89
+ response: AxiosResponse;
90
+ }
91
+ declare class HttpResponseError extends Error {
92
+ readonly body: unknown;
93
+ readonly code: unknown;
94
+ readonly response: AxiosResponse;
95
+ constructor(message: string, options: HttpResponseErrorOptions);
96
+ }
97
+
98
+ declare function createHttpService(config?: BaseApiConfig): BaseApi;
99
+ declare function getHttpService(config?: BaseApiConfig): BaseApi;
100
+
101
+ /**
102
+ * 严格解析点号路径;调用方声明了路径,就必须让响应满足该契约。
103
+ */
104
+ declare function getValueByPath(source: unknown, path: string | undefined): unknown;
105
+
106
+ export { BaseApi, type BaseApiConfig, BaseApi as BaseApiDefault, BaseHttpClient, type BaseHttpClientConfig, BaseHttpClient as BaseHttpClientDefault, type HttpClientInstance, type HttpRequestConfig, type HttpResponseContract, HttpResponseError, type TokenConfig, createHttpService, getHttpService as default, getHttpService, getValueByPath };
package/dist/index.js ADDED
@@ -0,0 +1,201 @@
1
+ // src/BaseHttpClient.ts
2
+ import axios, { AxiosHeaders } from "axios";
3
+ function isTimeoutError(error) {
4
+ return error.code === "ECONNABORTED" || error.code === "ETIMEDOUT";
5
+ }
6
+ function setRequestHeader(config, name, value) {
7
+ const headers = AxiosHeaders.from(config.headers);
8
+ headers.set(name, value);
9
+ config.headers = headers;
10
+ }
11
+ var BaseHttpClient = class {
12
+ instance;
13
+ token;
14
+ onRequest;
15
+ onResponse;
16
+ onResponseError;
17
+ onTimeout;
18
+ onUnauthorized;
19
+ constructor(config = {}) {
20
+ const {
21
+ token,
22
+ onRequest,
23
+ onResponse,
24
+ onResponseError,
25
+ onTimeout,
26
+ onUnauthorized,
27
+ ...axiosConfig
28
+ } = config;
29
+ this.token = token;
30
+ this.onRequest = onRequest;
31
+ this.onResponse = onResponse;
32
+ this.onResponseError = onResponseError;
33
+ this.onTimeout = onTimeout;
34
+ this.onUnauthorized = onUnauthorized;
35
+ this.instance = axios.create(axiosConfig);
36
+ this.setupInterceptors();
37
+ }
38
+ async processRequestConfig(config) {
39
+ if (!this.token) {
40
+ return config;
41
+ }
42
+ const token = await this.token.getToken();
43
+ if (token == null || token === "") {
44
+ return config;
45
+ }
46
+ const headerName = this.token.headerName ?? "Authorization";
47
+ const headerValue = this.token.formatToken?.(token) ?? token;
48
+ setRequestHeader(config, headerName, headerValue);
49
+ return config;
50
+ }
51
+ async processResponseConfig(response) {
52
+ const processedResponse = this.onResponse ? await this.onResponse(response) : response;
53
+ return processedResponse.data;
54
+ }
55
+ async processResponseError(error) {
56
+ if (error.response?.status === 401) {
57
+ await this.onUnauthorized?.(error);
58
+ }
59
+ if (isTimeoutError(error)) {
60
+ await this.onTimeout?.(error);
61
+ }
62
+ await this.onResponseError?.(error);
63
+ }
64
+ setupInterceptors() {
65
+ this.instance.interceptors.request.use(
66
+ async (config) => {
67
+ const processedConfig = await this.processRequestConfig(config);
68
+ return this.onRequest ? await this.onRequest(processedConfig) : processedConfig;
69
+ },
70
+ (error) => Promise.reject(error)
71
+ );
72
+ this.instance.interceptors.response.use(
73
+ (response) => response,
74
+ async (error) => {
75
+ await this.processResponseError(error);
76
+ return Promise.reject(error);
77
+ }
78
+ );
79
+ }
80
+ /**
81
+ * 所有快捷方法最终都走 request,便于子类在单点扩展请求语义。
82
+ */
83
+ async request(config) {
84
+ const response = await this.instance.request(config);
85
+ return await this.processResponseConfig(response);
86
+ }
87
+ async get(url, params, config) {
88
+ return await this.request({ ...config, method: "get", params, url });
89
+ }
90
+ async post(url, data, config) {
91
+ return await this.request({ ...config, data, method: "post", url });
92
+ }
93
+ async put(url, data, config) {
94
+ return await this.request({ ...config, data, method: "put", url });
95
+ }
96
+ async delete(url, params, config) {
97
+ return await this.request({ ...config, method: "delete", params, url });
98
+ }
99
+ async all(requests) {
100
+ const tasks = requests.map((request) => {
101
+ return request instanceof Promise ? request : this.request(request);
102
+ });
103
+ return await Promise.all(tasks);
104
+ }
105
+ async uploadFile(url, file, config) {
106
+ const formData = new FormData();
107
+ formData.append("file", file);
108
+ return await this.post(url, formData, config);
109
+ }
110
+ downloadFile(blob, filename) {
111
+ if (typeof window === "undefined" || typeof document === "undefined") {
112
+ throw new TypeError("[ajax-package] downloadFile requires a browser runtime");
113
+ }
114
+ const url = window.URL.createObjectURL(blob);
115
+ const link = document.createElement("a");
116
+ link.href = url;
117
+ link.download = filename;
118
+ document.body.appendChild(link);
119
+ try {
120
+ link.click();
121
+ } finally {
122
+ link.remove();
123
+ window.URL.revokeObjectURL(url);
124
+ }
125
+ }
126
+ };
127
+ var BaseHttpClient_default = BaseHttpClient;
128
+
129
+ // src/errors.ts
130
+ var HttpResponseError = class extends Error {
131
+ body;
132
+ code;
133
+ response;
134
+ constructor(message, options) {
135
+ super(message);
136
+ this.name = "HttpResponseError";
137
+ this.body = options.body;
138
+ this.code = options.code;
139
+ this.response = options.response;
140
+ }
141
+ };
142
+
143
+ // src/path.ts
144
+ import { readValueByPath } from "@moluoxixi/core";
145
+ function getValueByPath(source, path) {
146
+ return readValueByPath(source, path, {
147
+ createMissingPathError: (missingPath) => new Error(`[ajax-package] response path not found: ${missingPath}`),
148
+ strict: true
149
+ });
150
+ }
151
+
152
+ // src/BaseApi.ts
153
+ function resolveResponseMessage(contract, body, code) {
154
+ if (contract.messagePath) {
155
+ const message = getValueByPath(body, contract.messagePath);
156
+ return typeof message === "string" ? message : String(message);
157
+ }
158
+ return `HTTP response contract rejected code: ${String(code)}`;
159
+ }
160
+ var BaseApi = class extends BaseHttpClient_default {
161
+ responseContract;
162
+ constructor(config = {}) {
163
+ const { responseContract, ...baseConfig } = config;
164
+ super(baseConfig);
165
+ this.responseContract = responseContract;
166
+ }
167
+ async processResponseConfig(response) {
168
+ const body = await super.processResponseConfig(response);
169
+ if (!this.responseContract) {
170
+ return body;
171
+ }
172
+ const code = this.responseContract.codePath ? getValueByPath(body, this.responseContract.codePath) : void 0;
173
+ if (this.responseContract.isSuccess && !this.responseContract.isSuccess(code, body, response)) {
174
+ throw new HttpResponseError(
175
+ resolveResponseMessage(this.responseContract, body, code),
176
+ { body, code, response }
177
+ );
178
+ }
179
+ return getValueByPath(body, this.responseContract.dataPath);
180
+ }
181
+ };
182
+ var BaseApi_default = BaseApi;
183
+
184
+ // src/factory.ts
185
+ function createHttpService(config = {}) {
186
+ return new BaseApi_default(config);
187
+ }
188
+ function getHttpService(config = {}) {
189
+ return createHttpService(config);
190
+ }
191
+ export {
192
+ BaseApi,
193
+ BaseApi_default as BaseApiDefault,
194
+ BaseHttpClient,
195
+ BaseHttpClient_default as BaseHttpClientDefault,
196
+ HttpResponseError,
197
+ createHttpService,
198
+ getHttpService as default,
199
+ getHttpService,
200
+ getValueByPath
201
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@moluoxixi/ajax-package",
3
+ "type": "module",
4
+ "version": "0.0.58",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/moluoxixi/utils.git",
8
+ "directory": "packages/AjaxPackage"
9
+ },
10
+ "sideEffects": false,
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "main": "./dist/index.js",
18
+ "module": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "files": [
21
+ "README.md",
22
+ "dist/**/*.d.ts",
23
+ "dist/**/*.js"
24
+ ],
25
+ "dependencies": {
26
+ "axios": "^1.16.1",
27
+ "@moluoxixi/core": "0.0.1"
28
+ },
29
+ "devDependencies": {
30
+ "@vitest/coverage-v8": "^3.0.0",
31
+ "tsup": "^8.0.0",
32
+ "typescript": "^5.7.0",
33
+ "vitest": "^3.0.0"
34
+ },
35
+ "scripts": {
36
+ "build": "tsup src/index.ts --format esm --dts --clean",
37
+ "test": "vitest run",
38
+ "test:coverage": "vitest run --coverage",
39
+ "typecheck": "tsc -p tsconfig.json --noEmit"
40
+ }
41
+ }