@yl546/core-api 1.0.0

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.
@@ -0,0 +1,66 @@
1
+ import { AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
2
+ import { Api } from '../Api';
3
+ import { DEFAULT_RESPONSE_FIELDS } from '../constants';
4
+ import { AuthAdapter } from './auth';
5
+ import { LoginResponse } from './login';
6
+ export * from './auth';
7
+ export * from './login';
8
+ export declare const VERSION = "1.0.0";
9
+ export type ApiResponseFields = Partial<typeof DEFAULT_RESPONSE_FIELDS>;
10
+ export type ResolvedApiResponseFields = typeof DEFAULT_RESPONSE_FIELDS;
11
+ export type ApiRequestInterceptor = (config: InternalAxiosRequestConfig, api: Api) => InternalAxiosRequestConfig;
12
+ export interface ApiEvent {
13
+ /** 登录成功 */
14
+ 'login-success': (response: LoginResponse) => void;
15
+ /** 认证失败 */
16
+ 'auth-failed': () => void;
17
+ /** 登出成功 */
18
+ 'logout-success': () => void;
19
+ }
20
+ export interface ApiEndpointConstructor {
21
+ new (api: Api): any;
22
+ }
23
+ export type ApiOptions = {
24
+ readonly baseUrl?: string;
25
+ /** 鉴权适配器,由各应用自行配置 */
26
+ readonly auth?: AuthAdapter;
27
+ readonly version?: string;
28
+ readonly endpoints?: Array<ApiEndpointConstructor>;
29
+ /** 响应字段取值 key 配置 */
30
+ readonly responseFields?: ApiResponseFields;
31
+ /** 请求拦截,用于修改入参 */
32
+ readonly onRequest?: ApiRequestInterceptor;
33
+ };
34
+ export type ApiRequestConfig = AxiosRequestConfig & {
35
+ url: string;
36
+ query?: {
37
+ [key: string]: any;
38
+ };
39
+ };
40
+ export type RawApiResponse<D> = {
41
+ code: number;
42
+ msg: string;
43
+ data: D;
44
+ time: number;
45
+ };
46
+ export type Pagination = {
47
+ /** 总条数 */
48
+ total: number;
49
+ /** 当前页码 */
50
+ pageNum: number;
51
+ /** 每页条数 */
52
+ pageSize: number;
53
+ };
54
+ /**
55
+ * 分页查询参数
56
+ */
57
+ export type PaginationQueryParams = {
58
+ /** 当前页码 */
59
+ pageNum: number;
60
+ /** 每页条数 */
61
+ pageSize: number;
62
+ };
63
+ export type PaginationData<T> = {
64
+ items: T[];
65
+ pagination: Pagination;
66
+ };
@@ -0,0 +1,23 @@
1
+ export interface LoginParams {
2
+ /** 用户名 */
3
+ username: string;
4
+ /** 密码 */
5
+ password: string;
6
+ }
7
+ /** 登录响应 */
8
+ export interface LoginResponse {
9
+ /** 组织ID */
10
+ organId: number;
11
+ /** 员工ID */
12
+ workerId: number;
13
+ /** 令牌 */
14
+ token: string;
15
+ /** 昵称 */
16
+ nickname: string;
17
+ /** 头像 */
18
+ image: string;
19
+ /** 经度 */
20
+ x: string;
21
+ /** 纬度 */
22
+ y: string;
23
+ }
@@ -0,0 +1,3 @@
1
+ import type { ResolvedApiResponseFields } from '../types';
2
+ /** 将接口 data 格式化为调用方声明的业务类型 T(实体 / 列表 / PaginationData 等) */
3
+ export declare function formatResponseData<T>(data: unknown, fields: ResolvedApiResponseFields): T;
@@ -0,0 +1,3 @@
1
+ import type { ApiResponseFields, ResolvedApiResponseFields } from '../types';
2
+ export declare function resolveResponseFields(fields?: ApiResponseFields): ResolvedApiResponseFields;
3
+ export declare function readResponseField(source: Record<string, unknown>, key: string): unknown;
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@yl546/core-api",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "type": "module",
6
+ "keywords": [],
7
+ "author": "",
8
+ "license": "ISC",
9
+ "files": [
10
+ "dist",
11
+ "src"
12
+ ],
13
+ "main": "./dist/index.js",
14
+ "module": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ }
21
+ },
22
+ "dependencies": {
23
+ "axios": "^1.16.0",
24
+ "eventemitter3": "^5.0.4",
25
+ "query-string": "^9.3.1",
26
+ "uuid": "^14.0.0"
27
+ },
28
+ "peerDependencies": {
29
+ "vue": "3.5.33"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ }
34
+ }
package/src/Api.ts ADDED
@@ -0,0 +1,328 @@
1
+ import QS from 'query-string';
2
+ import Axios, { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
3
+ import EventEmitter from 'eventemitter3';
4
+ import { App, inject, InjectionKey } from 'vue';
5
+ import {
6
+ ApiEndpointConstructor,
7
+ ApiEvent,
8
+ ApiOptions,
9
+ ApiRequestConfig,
10
+ AuthAdapter,
11
+ LoginParams,
12
+ LoginResponse,
13
+ RawApiResponse,
14
+ VERSION,
15
+ } from './types';
16
+ import { ApiError } from './ApiError';
17
+ import { WebSocketClient, WebSocketEventRawListeners } from './WebSocketClient';
18
+ import { v4 as uuidv4 } from 'uuid';
19
+ import { AUTHORIZED_CODE, UNAUTHORIZED_CODE } from './constants';
20
+ import { formatResponseData } from './utils/formatResponseData';
21
+ import { readResponseField, resolveResponseFields } from './utils/resolveResponseFields';
22
+
23
+ export type ApiWithEndpoints<Instance extends Api, Endpoints extends ApiEndpoints = any> = Instance & Endpoints;
24
+
25
+ export declare interface ApiEndpoints {}
26
+
27
+ export type InitWebSocketOptions = {
28
+ override?: boolean;
29
+ protocols?: string | string[];
30
+ listeners?: WebSocketEventRawListeners;
31
+ };
32
+
33
+ export type ApiInstance = ApiWithEndpoints<Api, ApiEndpoints>;
34
+
35
+ declare module 'vue' {
36
+ interface ComponentCustomProperties {
37
+ $api: ApiInstance;
38
+ }
39
+ }
40
+
41
+ export const API_CONTEXT_KEY: InjectionKey<ApiInstance> = Symbol.for('API_CONTEXT_KEY');
42
+
43
+ /** 请求取消控制器映射 */
44
+ const abortControllerMap = new Map<string, AbortController>();
45
+
46
+ export const useApi = () => {
47
+ const $api = inject(API_CONTEXT_KEY);
48
+
49
+ if (!$api) {
50
+ throw new Error('Api not found');
51
+ }
52
+
53
+ return $api;
54
+ };
55
+
56
+ export class Api extends EventEmitter<ApiEvent> {
57
+ static VERSION = VERSION;
58
+
59
+ readonly version: string = VERSION;
60
+ readonly options: Readonly<ApiOptions>;
61
+ readonly auth: AuthAdapter;
62
+ readonly sockets: Record<string, WebSocketClient> = {};
63
+
64
+ private _axios: AxiosInstance;
65
+
66
+ static install = (app: App, options: ApiOptions): void => {
67
+ const api = new Api(options);
68
+
69
+ app.provide('$apiConfig', options);
70
+ app.provide(API_CONTEXT_KEY, api as ApiInstance);
71
+ app.config.globalProperties.$api = api as ApiInstance;
72
+ };
73
+
74
+ private _responseFields: ReturnType<typeof resolveResponseFields>;
75
+
76
+ constructor(options: ApiOptions) {
77
+ super();
78
+
79
+ this.auth = options.auth ?? {};
80
+ this.options = options;
81
+ this._responseFields = resolveResponseFields(options.responseFields);
82
+ this._axios = Axios.create({
83
+ baseURL: options.baseUrl ?? '',
84
+ timeout: 10000,
85
+ });
86
+
87
+ this._axios.interceptors.request.use(this._requestInterceptor.bind(this));
88
+ this._axios.interceptors.response.use(this._responseInterceptor.bind(this));
89
+
90
+ this.$request = this.$request.bind(this);
91
+ this.$post = this.$post.bind(this);
92
+ this.$get = this.$get.bind(this);
93
+ this.$put = this.$put.bind(this);
94
+ this.$delete = this.$delete.bind(this);
95
+
96
+ if (options.endpoints) {
97
+ this.use(options.endpoints);
98
+ }
99
+
100
+ if (options.version) {
101
+ this.version = options.version;
102
+ }
103
+ }
104
+
105
+ get host(): string {
106
+ return location.host;
107
+ }
108
+
109
+ use(EndPoint: ApiEndpointConstructor | Array<ApiEndpointConstructor>): ThisType<Api> {
110
+ if (Array.isArray(EndPoint)) {
111
+ EndPoint.forEach((p) => this.use(p));
112
+
113
+ return this;
114
+ }
115
+
116
+ if (EndPoint) {
117
+ const endPoint = new EndPoint(this);
118
+ const name = endPoint.name || EndPoint.name;
119
+
120
+ if (Object.prototype.hasOwnProperty.call(this, name)) {
121
+ return this;
122
+ }
123
+
124
+ this.addEndPoint(name, endPoint);
125
+ }
126
+
127
+ return this;
128
+ }
129
+
130
+ addEndPoint(name: string, endPoint: any, enumerable = true): ThisType<Api> {
131
+ Object.defineProperty(this, name, {
132
+ value: endPoint,
133
+ writable: false,
134
+ configurable: false,
135
+ enumerable,
136
+ });
137
+
138
+ return this;
139
+ }
140
+
141
+ /** 登录 */
142
+ async login(params: LoginParams): Promise<LoginResponse> {
143
+ try {
144
+ if (!this.auth.login) {
145
+ throw new Error('Login method not implemented');
146
+ }
147
+
148
+ const result = await this.auth.login(this, params);
149
+
150
+ this.auth.persist?.(result);
151
+ this.emit('login-success', result as LoginResponse);
152
+
153
+ return result as LoginResponse;
154
+ } catch (error) {
155
+ throw error;
156
+ }
157
+ }
158
+
159
+ /** 登出 */
160
+ async logout(): Promise<void> {
161
+ try {
162
+ if (!this.auth.logout) {
163
+ throw new Error('Logout method not implemented');
164
+ }
165
+
166
+ await this.auth.logout?.();
167
+ this.auth.clear?.();
168
+ this.emit('logout-success');
169
+ } catch (error) {
170
+ throw error;
171
+ }
172
+ }
173
+
174
+ /** 取消所有请求 */
175
+ cancelAllRequest() {
176
+ abortControllerMap.forEach((controller) => {
177
+ controller.abort();
178
+ });
179
+ abortControllerMap.clear();
180
+ }
181
+
182
+ async $request<D = any>(config: ApiRequestConfig): Promise<D> {
183
+ const controller = new AbortController();
184
+ const requestId = uuidv4();
185
+ abortControllerMap.set(requestId, controller);
186
+ let response: AxiosResponse<RawApiResponse<D>>;
187
+ const { query, url, ...conf } = config;
188
+
189
+ const urlObject = QS.parseUrl(url);
190
+
191
+ if (query) {
192
+ Object.assign(urlObject.query, query);
193
+ }
194
+
195
+ Object.assign(conf, {
196
+ url: QS.stringifyUrl(urlObject),
197
+ });
198
+
199
+ try {
200
+ response = await this._axios.request({
201
+ ...conf,
202
+ signal: controller.signal,
203
+ headers: {
204
+ ...conf.headers,
205
+ ...this.auth.getHeaders?.(),
206
+ },
207
+ });
208
+ } catch (error: any) {
209
+ if (Axios.isCancel(error)) {
210
+ throw error;
211
+ }
212
+
213
+ const codeMessage: any[] = error.message.split('status code ');
214
+ let code = '';
215
+ if (codeMessage && codeMessage.length) {
216
+ code = codeMessage[codeMessage.length - 1];
217
+ }
218
+ throw new ApiError(`请求错误,http状态码:${code}`, Number(code), {});
219
+ } finally {
220
+ abortControllerMap.delete(requestId);
221
+ }
222
+
223
+ const { data: rawData } = response;
224
+ const raw = rawData as Record<string, unknown>;
225
+ const fields = this._responseFields;
226
+
227
+ const code = Number(readResponseField(raw, fields.code));
228
+ const msg = String(readResponseField(raw, fields.msg) ?? '未知异常');
229
+ const data = readResponseField(raw, fields.data);
230
+
231
+ const authorizedCode = ([] as number[]).concat(this.auth.authorizedCode ?? AUTHORIZED_CODE);
232
+ const unauthorizedCodes = ([] as number[]).concat(this.auth.unauthorizedCodes ?? UNAUTHORIZED_CODE);
233
+
234
+ if (authorizedCode.includes(code)) {
235
+ return formatResponseData<D>(data, fields);
236
+ }
237
+
238
+ if (unauthorizedCodes.includes(code)) {
239
+ this.cancelAllRequest();
240
+ this.auth.clear?.();
241
+ this.emit('auth-failed');
242
+ }
243
+
244
+ throw new ApiError(msg, code);
245
+ }
246
+
247
+ $post<D = any>(url: string, data?: any, config?: Partial<ApiRequestConfig>): Promise<D> {
248
+ return this.$request<D>(
249
+ Object.assign({
250
+ ...config,
251
+ url,
252
+ data,
253
+ method: 'POST',
254
+ }),
255
+ );
256
+ }
257
+
258
+ $get<D = any, Q = any>(url: string, query?: Q, config?: Partial<ApiRequestConfig>): Promise<D> {
259
+ return this.$request<D>(
260
+ Object.assign({
261
+ ...config,
262
+ url,
263
+ query,
264
+ method: 'GET',
265
+ }),
266
+ );
267
+ }
268
+
269
+ $put<D = any>(url: string, data?: any, config?: Partial<ApiRequestConfig>): Promise<D> {
270
+ return this.$request<D>(
271
+ Object.assign({
272
+ ...config,
273
+ url,
274
+ data,
275
+ method: 'PUT',
276
+ }),
277
+ );
278
+ }
279
+
280
+ $delete<D = any, Q = any>(url: string, query?: Q, config?: Partial<ApiRequestConfig>): Promise<D> {
281
+ return this.$request<D>(
282
+ Object.assign({
283
+ ...config,
284
+ url,
285
+ query,
286
+ method: 'DELETE',
287
+ }),
288
+ );
289
+ }
290
+
291
+ private _requestInterceptor(config: InternalAxiosRequestConfig) {
292
+ if (this.options.onRequest) {
293
+ return this.options.onRequest(config, this);
294
+ }
295
+
296
+ return config;
297
+ }
298
+
299
+ private _responseInterceptor(response: AxiosResponse): AxiosResponse {
300
+ return response;
301
+ }
302
+
303
+ setWebSocket = async (name: string, endpoint: string, options_?: InitWebSocketOptions): Promise<WebSocketClient> => {
304
+ const options = Object.assign(
305
+ {
306
+ override: false,
307
+ },
308
+ options_,
309
+ );
310
+
311
+ if (this.sockets[name]) {
312
+ if (!options.override) {
313
+ throw new Error(`${name} has been initilized.`);
314
+ } else {
315
+ this.sockets[name].deactivate();
316
+ }
317
+ }
318
+
319
+ Object.defineProperty(this.sockets, name, {
320
+ value: new WebSocketClient(endpoint, options.protocols, options.listeners),
321
+ configurable: true,
322
+ enumerable: true,
323
+ writable: false,
324
+ });
325
+
326
+ return this.sockets[name]!;
327
+ };
328
+ }
@@ -0,0 +1,23 @@
1
+ import { AxiosResponse } from "axios";
2
+ import { ApiRequestConfig } from "./types";
3
+
4
+ export type ApiErrorOptions<D> = Partial<{
5
+ data: D;
6
+ requestConfig: ApiRequestConfig;
7
+ response: AxiosResponse;
8
+ }>;
9
+
10
+ export class ApiError<D> extends Error {
11
+ readonly code: number;
12
+ readonly requestConfig?: ApiRequestConfig;
13
+ readonly response?: AxiosResponse;
14
+ readonly data?: D;
15
+
16
+ constructor(message: string, code: number, options?: ApiErrorOptions<D>) {
17
+ super(message);
18
+
19
+ this.code = code;
20
+
21
+ Object.assign(this, options);
22
+ }
23
+ }
@@ -0,0 +1,189 @@
1
+ import EventEmitter from 'eventemitter3';
2
+
3
+ export enum EventSourceEventType {
4
+ Connected = 'connected',
5
+ Message = 'message',
6
+ RawMessage = 'raw-message',
7
+ Error = 'error',
8
+ Disconnected = 'disconnected',
9
+ }
10
+
11
+ export type EventSourceEventListener = (...args: any[]) => any;
12
+
13
+ export type EventSourceEventValue = `${EventSourceEventType}`;
14
+
15
+ export type EventSourceEventRawListeners = Partial<Record<EventSourceEventValue, Array<EventSourceEventListener>>>;
16
+
17
+ export type EventSourceClientOptions = {
18
+ withCredentials?: boolean;
19
+ reconnectInterval?: number;
20
+ /** 是否在实例化时自动连接,默认 true */
21
+ autoConnect?: boolean;
22
+ };
23
+
24
+ const DEFAULT_RECONNECT_INTERVAL = 3000;
25
+
26
+ export class EventSourceClient extends EventEmitter<EventSourceEventType> {
27
+ private _endPoint: string;
28
+ private _options: EventSourceClientOptions;
29
+ private _rawListeners?: EventSourceEventRawListeners;
30
+ private _source?: EventSource;
31
+ private _active: boolean;
32
+ private _reconnectTimer?: ReturnType<typeof setTimeout>;
33
+
34
+ constructor(endPoint: string, options: EventSourceClientOptions = {}, rawListeners?: EventSourceEventRawListeners) {
35
+ super();
36
+
37
+ this._endPoint = endPoint;
38
+ this._options = options;
39
+ this._rawListeners = rawListeners;
40
+ this._active = options.autoConnect !== false;
41
+
42
+ if (this._active) {
43
+ this._connect();
44
+ }
45
+ }
46
+
47
+ /** 手动建立连接(已连接或连接中时跳过) */
48
+ connect(): void {
49
+ this._active = true;
50
+ this._clearReconnectTimer();
51
+
52
+ const state = this._source?.readyState;
53
+ if (state === EventSource.OPEN || state === EventSource.CONNECTING) {
54
+ return;
55
+ }
56
+
57
+ this._destroySource();
58
+ this._connect();
59
+ }
60
+
61
+ /** 重新连接 */
62
+ reconnect(): void {
63
+ this._active = true;
64
+ this._clearReconnectTimer();
65
+ this._destroySource();
66
+ this._connect();
67
+ }
68
+ /** 禁用连接 */
69
+ deactivate(): void {
70
+ this._active = false;
71
+ this._clearReconnectTimer();
72
+ this._destroySource();
73
+ }
74
+
75
+ private _connect(): void {
76
+ if (!this._active) {
77
+ return;
78
+ }
79
+
80
+ this._source = new EventSource(this._endPoint, { withCredentials: this._options.withCredentials });
81
+
82
+ this._source.addEventListener('open', this._handleOpen);
83
+ this._source.addEventListener('message', this._handleMessage);
84
+ this._source.addEventListener('error', this._handleError);
85
+
86
+ this._bindRawListeners();
87
+ }
88
+
89
+ private _destroySource(): void {
90
+ if (!this._source) {
91
+ return;
92
+ }
93
+
94
+ this._source.removeEventListener('open', this._handleOpen);
95
+ this._source.removeEventListener('message', this._handleMessage);
96
+ this._source.removeEventListener('error', this._handleError);
97
+ this._source.close();
98
+ this._source = undefined;
99
+ }
100
+
101
+ private _clearReconnectTimer(): void {
102
+ if (this._reconnectTimer) {
103
+ clearTimeout(this._reconnectTimer);
104
+ this._reconnectTimer = undefined;
105
+ }
106
+ }
107
+
108
+ private _scheduleReconnect(): void {
109
+ if (!this._active || this._reconnectTimer) {
110
+ return;
111
+ }
112
+
113
+ this._reconnectTimer = setTimeout(() => {
114
+ this._reconnectTimer = undefined;
115
+ this.reconnect();
116
+ }, this._options.reconnectInterval ?? DEFAULT_RECONNECT_INTERVAL);
117
+ }
118
+
119
+ private _handleOpen = (ev: Event) => {
120
+ this.emit(EventSourceEventType.Connected, ev);
121
+ };
122
+
123
+ private _handleMessage = (ev: MessageEvent) => {
124
+ try {
125
+ this.emit(EventSourceEventType.Message, this._parseMessage(ev));
126
+ } catch {
127
+ this.emit(EventSourceEventType.RawMessage, ev);
128
+ }
129
+ };
130
+
131
+ private _handleError = (ev: Event) => {
132
+ this.emit(EventSourceEventType.Error, ev);
133
+
134
+ if (!this._source || this._source.readyState !== EventSource.CLOSED) {
135
+ return;
136
+ }
137
+
138
+ this.emit(EventSourceEventType.Disconnected, ev);
139
+ this._scheduleReconnect();
140
+ };
141
+
142
+ private _bindRawListeners = (): void => {
143
+ if (!this._rawListeners || !this._source) {
144
+ return;
145
+ }
146
+
147
+ const source = this._source;
148
+
149
+ (
150
+ Object.entries(this._rawListeners) as unknown as Array<[EventSourceEventValue, Array<EventSourceEventListener>]>
151
+ ).forEach(([evt, listeners]) => {
152
+ switch (evt) {
153
+ case EventSourceEventType.Connected:
154
+ listeners.forEach((fn) => source.addEventListener('open', fn));
155
+ break;
156
+ case EventSourceEventType.Message:
157
+ listeners.forEach((fn) => {
158
+ source.addEventListener('message', (ev: MessageEvent) => {
159
+ try {
160
+ fn(this._parseMessage(ev), ev);
161
+ } catch {
162
+ this._rawListeners?.[EventSourceEventType.RawMessage]?.forEach((rawFn) => rawFn(ev));
163
+ }
164
+ });
165
+ });
166
+ break;
167
+ case EventSourceEventType.RawMessage:
168
+ listeners.forEach((fn) => source.addEventListener('message', fn));
169
+ break;
170
+ case EventSourceEventType.Disconnected:
171
+ listeners.forEach((fn) => {
172
+ source.addEventListener('error', (ev: Event) => {
173
+ if (this._source?.readyState === EventSource.CLOSED) {
174
+ fn(ev);
175
+ }
176
+ });
177
+ });
178
+ break;
179
+ case EventSourceEventType.Error:
180
+ listeners.forEach((fn) => source.addEventListener('error', fn));
181
+ break;
182
+ }
183
+ });
184
+ };
185
+
186
+ private _parseMessage(evt: MessageEvent) {
187
+ return JSON.parse(evt.data);
188
+ }
189
+ }