@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.
- package/dist/Api.d.ts +48 -0
- package/dist/ApiError.d.ts +14 -0
- package/dist/EventSourceClient.d.ts +41 -0
- package/dist/WebSocketClient.d.ts +31 -0
- package/dist/constants.d.ts +14 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +588 -0
- package/dist/index.js.map +1 -0
- package/dist/types/auth.d.ts +19 -0
- package/dist/types/index.d.ts +66 -0
- package/dist/types/login.d.ts +23 -0
- package/dist/utils/formatResponseData.d.ts +3 -0
- package/dist/utils/resolveResponseFields.d.ts +3 -0
- package/package.json +34 -0
- package/src/Api.ts +328 -0
- package/src/ApiError.ts +23 -0
- package/src/EventSourceClient.ts +189 -0
- package/src/WebSocketClient.ts +170 -0
- package/src/constants.ts +15 -0
- package/src/index.ts +8 -0
- package/src/types/auth.ts +27 -0
- package/src/types/index.ts +77 -0
- package/src/types/login.ts +24 -0
- package/src/utils/formatResponseData.ts +29 -0
- package/src/utils/resolveResponseFields.ts +13 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import EventEmitter from 'eventemitter3';
|
|
2
|
+
|
|
3
|
+
// 定义存在的事件类型
|
|
4
|
+
export enum WebSocketEventType {
|
|
5
|
+
Connected = 'connected', // 连接成功
|
|
6
|
+
Message = 'message', // 接受信息,处理之后的信息
|
|
7
|
+
RawMessage = 'raw-message', // 原始信息
|
|
8
|
+
Error = 'error', //错误的时候
|
|
9
|
+
Disconnected = 'disconnected', //断开连接的时候
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type WebSocketEventListener = (...args: any[]) => any;
|
|
13
|
+
|
|
14
|
+
export type WebSocketEventValue = `${WebSocketEventType}`;
|
|
15
|
+
|
|
16
|
+
export type WebSocketEventRawListeners = Partial<Record<WebSocketEventValue, Array<WebSocketEventListener>>>;
|
|
17
|
+
|
|
18
|
+
export class WebSocketClient extends EventEmitter<WebSocketEventType> {
|
|
19
|
+
private _endPoint: string;
|
|
20
|
+
private _protocols?: string | Array<string>;
|
|
21
|
+
private _rawListeners?: WebSocketEventRawListeners;
|
|
22
|
+
private _socket: WebSocket;
|
|
23
|
+
private _loopAnimationRequestId?: number;
|
|
24
|
+
private _loopSocketHeartBeatTimestamp = 0;
|
|
25
|
+
private _socketHeartBeatInterval = 5000;
|
|
26
|
+
|
|
27
|
+
constructor(endPoint: string, protocols?: string | Array<string>, rawListeners?: WebSocketEventRawListeners) {
|
|
28
|
+
super();
|
|
29
|
+
|
|
30
|
+
this._endPoint = endPoint;
|
|
31
|
+
this._protocols = protocols;
|
|
32
|
+
this._rawListeners = rawListeners;
|
|
33
|
+
this._socket = new WebSocket(endPoint);
|
|
34
|
+
|
|
35
|
+
this._socket.addEventListener('open', this._handleOpen);
|
|
36
|
+
this._socket.addEventListener('message', this._handleMessage);
|
|
37
|
+
this._socket.addEventListener('error', this._handleError);
|
|
38
|
+
this._socket.addEventListener('close', this._handleClose);
|
|
39
|
+
|
|
40
|
+
this._initListeners();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
reconnect(): void {
|
|
44
|
+
this._socket = new WebSocket(this._endPoint);
|
|
45
|
+
|
|
46
|
+
this._socket.addEventListener('open', this._handleOpen);
|
|
47
|
+
this._socket.addEventListener('message', this._handleMessage);
|
|
48
|
+
this._socket.addEventListener('error', this._handleError);
|
|
49
|
+
this._socket.addEventListener('close', this._handleClose);
|
|
50
|
+
|
|
51
|
+
this._initListeners();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
deactivate(): void {
|
|
55
|
+
this._socket.close();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
send(val: string): void {
|
|
59
|
+
this._socket.send(val);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private _handleOpen = (ev: Event) => {
|
|
63
|
+
this.emit(WebSocketEventType.Connected, ev);
|
|
64
|
+
|
|
65
|
+
this._loop();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
private _handleMessage = async (ev: MessageEvent) => {
|
|
69
|
+
try {
|
|
70
|
+
const data = await this._resolveMessageEvent(ev);
|
|
71
|
+
this.emit(WebSocketEventType.Message, data);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
this.emit(WebSocketEventType.RawMessage, ev);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
private _handleError = (ev: Event) => {
|
|
78
|
+
this.emit(WebSocketEventType.Error, ev);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
private _handleClose = (ev: CloseEvent) => {
|
|
82
|
+
if (this._loopAnimationRequestId) {
|
|
83
|
+
cancelAnimationFrame(this._loopAnimationRequestId);
|
|
84
|
+
}
|
|
85
|
+
this.reconnect();
|
|
86
|
+
this.emit(WebSocketEventType.Disconnected, ev);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
private _initListeners = (): void => {
|
|
90
|
+
if (this._rawListeners) {
|
|
91
|
+
(
|
|
92
|
+
Object.entries(this._rawListeners) as unknown as Array<[WebSocketEventValue, Array<WebSocketEventListener>]>
|
|
93
|
+
).forEach(([evt, listeners]) => {
|
|
94
|
+
switch (evt) {
|
|
95
|
+
case WebSocketEventType.Connected:
|
|
96
|
+
listeners.forEach((fn) => {
|
|
97
|
+
this._socket.addEventListener('open', fn);
|
|
98
|
+
});
|
|
99
|
+
break;
|
|
100
|
+
case WebSocketEventType.Message:
|
|
101
|
+
listeners.forEach((fn) => {
|
|
102
|
+
this._socket.addEventListener('message', async (ev: MessageEvent) => {
|
|
103
|
+
try {
|
|
104
|
+
const data = await this._resolveMessageEvent(ev);
|
|
105
|
+
fn(data, ev);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (listeners[WebSocketEventType.RawMessage]) {
|
|
108
|
+
listeners[WebSocketEventType.RawMessage].forEach((fn_) => fn_(ev));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
break;
|
|
114
|
+
case WebSocketEventType.RawMessage:
|
|
115
|
+
listeners.forEach((fn) => {
|
|
116
|
+
this._socket.addEventListener('message', fn);
|
|
117
|
+
});
|
|
118
|
+
break;
|
|
119
|
+
case WebSocketEventType.Disconnected:
|
|
120
|
+
listeners.forEach((fn) => {
|
|
121
|
+
this._socket.addEventListener('close', fn);
|
|
122
|
+
});
|
|
123
|
+
break;
|
|
124
|
+
case WebSocketEventType.Error:
|
|
125
|
+
listeners.forEach((fn) => {
|
|
126
|
+
this._socket.addEventListener('error', fn);
|
|
127
|
+
});
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
private _resolveMessageEvent = async (evt: MessageEvent) => {
|
|
135
|
+
try {
|
|
136
|
+
const data = JSON.parse(evt.data);
|
|
137
|
+
return data;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
return Promise.reject(err);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
//保证websocket出于连接状态
|
|
144
|
+
private _loop = (): void => {
|
|
145
|
+
if (this._loopAnimationRequestId) {
|
|
146
|
+
cancelAnimationFrame(this._loopAnimationRequestId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
|
|
151
|
+
if (this._socket) {
|
|
152
|
+
switch (this._socket.readyState) {
|
|
153
|
+
case WebSocket.OPEN: //已连接并且可以通讯
|
|
154
|
+
if (this._loopSocketHeartBeatTimestamp + this._socketHeartBeatInterval < now) {
|
|
155
|
+
this._loopSocketHeartBeatTimestamp = now;
|
|
156
|
+
|
|
157
|
+
this._socket.send('ping');
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
case WebSocket.CLOSED: // 连接已关闭或者没有链接成功
|
|
161
|
+
this.reconnect();
|
|
162
|
+
case WebSocket.CLOSING: // 正在关闭
|
|
163
|
+
case WebSocket.CONNECTING: // 正在连接
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this._loopAnimationRequestId = requestAnimationFrame(this._loop);
|
|
169
|
+
};
|
|
170
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** 认证成功业务码 */
|
|
2
|
+
export const AUTHORIZED_CODE = 200;
|
|
3
|
+
/** 认证失败业务码 */
|
|
4
|
+
export const UNAUTHORIZED_CODE = 401;
|
|
5
|
+
|
|
6
|
+
/** 响应字段默认取值 key */
|
|
7
|
+
export const DEFAULT_RESPONSE_FIELDS = {
|
|
8
|
+
code: 'code',
|
|
9
|
+
msg: 'msg',
|
|
10
|
+
data: 'data',
|
|
11
|
+
items: 'items',
|
|
12
|
+
total: 'total',
|
|
13
|
+
pageNum: 'pageNum',
|
|
14
|
+
pageSize: 'pageSize',
|
|
15
|
+
} as const;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './Api';
|
|
2
|
+
export * from './ApiError';
|
|
3
|
+
export * from './constants';
|
|
4
|
+
export * from './types';
|
|
5
|
+
export * from './utils/formatResponseData';
|
|
6
|
+
export * from './utils/resolveResponseFields';
|
|
7
|
+
export * from './WebSocketClient';
|
|
8
|
+
export * from './EventSourceClient';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Api } from '../Api';
|
|
2
|
+
|
|
3
|
+
export interface AuthAdapter<TLoginParams = unknown, TLoginResult = unknown, TCredentials = Record<string, string>> {
|
|
4
|
+
/** 是否已登录 */
|
|
5
|
+
isAuthenticated?: () => boolean;
|
|
6
|
+
|
|
7
|
+
/** 登录 */
|
|
8
|
+
login?: (api: Api, params: TLoginParams) => Promise<TLoginResult>;
|
|
9
|
+
|
|
10
|
+
/** 主动登出 */
|
|
11
|
+
logout?: () => Promise<void>;
|
|
12
|
+
|
|
13
|
+
/** 每次请求附加的鉴权 headers */
|
|
14
|
+
getHeaders?: () => Record<string, string | null>;
|
|
15
|
+
|
|
16
|
+
/** 登录成功后持久化凭据 */
|
|
17
|
+
persist?: (result: TLoginResult) => TCredentials;
|
|
18
|
+
|
|
19
|
+
/** 清除凭据(401 等鉴权失败时也会调用) */
|
|
20
|
+
clear?: () => void;
|
|
21
|
+
|
|
22
|
+
/** 触发 clear 的业务码,默认 [401] */
|
|
23
|
+
unauthorizedCodes?: number | number[];
|
|
24
|
+
|
|
25
|
+
/** 认证成功业务码 */
|
|
26
|
+
authorizedCode?: number | number[];
|
|
27
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
|
|
7
|
+
export * from './auth';
|
|
8
|
+
export * from './login';
|
|
9
|
+
|
|
10
|
+
export const VERSION = '1.0.0';
|
|
11
|
+
|
|
12
|
+
export type ApiResponseFields = Partial<typeof DEFAULT_RESPONSE_FIELDS>;
|
|
13
|
+
|
|
14
|
+
export type ResolvedApiResponseFields = typeof DEFAULT_RESPONSE_FIELDS;
|
|
15
|
+
|
|
16
|
+
export type ApiRequestInterceptor = (config: InternalAxiosRequestConfig, api: Api) => InternalAxiosRequestConfig;
|
|
17
|
+
|
|
18
|
+
export interface ApiEvent {
|
|
19
|
+
/** 登录成功 */
|
|
20
|
+
'login-success': (response: LoginResponse) => void;
|
|
21
|
+
/** 认证失败 */
|
|
22
|
+
'auth-failed': () => void;
|
|
23
|
+
/** 登出成功 */
|
|
24
|
+
'logout-success': () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ApiEndpointConstructor {
|
|
28
|
+
new (api: Api): any;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ApiOptions = {
|
|
32
|
+
readonly baseUrl?: string;
|
|
33
|
+
/** 鉴权适配器,由各应用自行配置 */
|
|
34
|
+
readonly auth?: AuthAdapter;
|
|
35
|
+
readonly version?: string;
|
|
36
|
+
readonly endpoints?: Array<ApiEndpointConstructor>;
|
|
37
|
+
/** 响应字段取值 key 配置 */
|
|
38
|
+
readonly responseFields?: ApiResponseFields;
|
|
39
|
+
/** 请求拦截,用于修改入参 */
|
|
40
|
+
readonly onRequest?: ApiRequestInterceptor;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type ApiRequestConfig = AxiosRequestConfig & {
|
|
44
|
+
url: string;
|
|
45
|
+
query?: { [key: string]: any };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type RawApiResponse<D> = {
|
|
49
|
+
code: number;
|
|
50
|
+
msg: string;
|
|
51
|
+
data: D;
|
|
52
|
+
time: number;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type Pagination = {
|
|
56
|
+
/** 总条数 */
|
|
57
|
+
total: number;
|
|
58
|
+
/** 当前页码 */
|
|
59
|
+
pageNum: number;
|
|
60
|
+
/** 每页条数 */
|
|
61
|
+
pageSize: number;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 分页查询参数
|
|
66
|
+
*/
|
|
67
|
+
export type PaginationQueryParams = {
|
|
68
|
+
/** 当前页码 */
|
|
69
|
+
pageNum: number;
|
|
70
|
+
/** 每页条数 */
|
|
71
|
+
pageSize: number;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type PaginationData<T> = {
|
|
75
|
+
items: T[];
|
|
76
|
+
pagination: Pagination;
|
|
77
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface LoginParams {
|
|
2
|
+
/** 用户名 */
|
|
3
|
+
username: string;
|
|
4
|
+
/** 密码 */
|
|
5
|
+
password: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** 登录响应 */
|
|
9
|
+
export interface LoginResponse {
|
|
10
|
+
/** 组织ID */
|
|
11
|
+
organId: number;
|
|
12
|
+
/** 员工ID */
|
|
13
|
+
workerId: number;
|
|
14
|
+
/** 令牌 */
|
|
15
|
+
token: string;
|
|
16
|
+
/** 昵称 */
|
|
17
|
+
nickname: string;
|
|
18
|
+
/** 头像 */
|
|
19
|
+
image: string;
|
|
20
|
+
/** 经度 */
|
|
21
|
+
x: string;
|
|
22
|
+
/** 纬度 */
|
|
23
|
+
y: string;
|
|
24
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ResolvedApiResponseFields } from '../types';
|
|
2
|
+
|
|
3
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
4
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** 将接口 data 格式化为调用方声明的业务类型 T(实体 / 列表 / PaginationData 等) */
|
|
8
|
+
export function formatResponseData<T>(data: unknown, fields: ResolvedApiResponseFields): T {
|
|
9
|
+
if (Array.isArray(data)) {
|
|
10
|
+
return data as T;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (isPlainObject(data)) {
|
|
14
|
+
const items = data[fields.items];
|
|
15
|
+
|
|
16
|
+
if (Array.isArray(items)) {
|
|
17
|
+
return {
|
|
18
|
+
items,
|
|
19
|
+
pagination: {
|
|
20
|
+
total: Number(data[fields.total] ?? 0),
|
|
21
|
+
pageNum: Number(data[fields.pageNum] ?? 1),
|
|
22
|
+
pageSize: Number(data[fields.pageSize] ?? 10),
|
|
23
|
+
},
|
|
24
|
+
} as T;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return data as T;
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DEFAULT_RESPONSE_FIELDS } from '../constants';
|
|
2
|
+
import type { ApiResponseFields, ResolvedApiResponseFields } from '../types';
|
|
3
|
+
|
|
4
|
+
export function resolveResponseFields(fields?: ApiResponseFields): ResolvedApiResponseFields {
|
|
5
|
+
return {
|
|
6
|
+
...DEFAULT_RESPONSE_FIELDS,
|
|
7
|
+
...fields,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function readResponseField(source: Record<string, unknown>, key: string) {
|
|
12
|
+
return source[key];
|
|
13
|
+
}
|