react-track-hooks 1.0.1 → 1.0.3
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 +388 -4
- package/dist/index.cjs.js +384 -71
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +15 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +384 -72
- package/dist/index.esm.js.map +1 -1
- package/dist/trackHooks.d.ts +8 -7
- package/dist/trackHooks.d.ts.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +6 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,10 +1,394 @@
|
|
|
1
1
|
# react-track-hooks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/react-track-hooks)
|
|
4
|
+
[](https://github.com/PassingTraveller111/react-track-hooks/blob/main/LICENSE)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
一个轻量、易用的 React 埋点 Hooks 库,支持点击埋点、曝光埋点、页面停留时长埋点、自定义埋点,内置**智能批量上报**和**增强型失败重试**机制,适配 React/Next.js 项目。
|
|
7
|
+
|
|
8
|
+
## 特性
|
|
9
|
+
- 🚀 开箱即用:提供常用埋点场景的 Hooks,无需重复封装
|
|
10
|
+
- 📦 智能批量上报:支持埋点批量入队、定时/定量触发上报,减少网络请求
|
|
11
|
+
- 🔄 增强型失败重试:内置 localStorage 缓存 + 指数退避算法,批量/单条自适应重试,确保埋点不丢失
|
|
12
|
+
- 🎯 精准控制:曝光埋点支持可见比例、单次触发配置
|
|
13
|
+
- ⚡ 轻量无依赖:体积小,不引入额外冗余依赖
|
|
14
|
+
- 📝 完整 TypeScript 类型:提供完善的类型声明,开发更友好
|
|
15
|
+
- 🌐 框架适配:兼容 React 16+、Next.js(App Router/Pages Router)
|
|
6
16
|
|
|
17
|
+
## 安装
|
|
7
18
|
```bash
|
|
19
|
+
# npm
|
|
8
20
|
npm install react-track-hooks --save
|
|
9
|
-
|
|
10
|
-
yarn
|
|
21
|
+
|
|
22
|
+
# yarn
|
|
23
|
+
yarn add react-track-hooks
|
|
24
|
+
|
|
25
|
+
# pnpm
|
|
26
|
+
pnpm add react-track-hooks
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 快速开始
|
|
30
|
+
|
|
31
|
+
### 1. 全局配置(项目入口)
|
|
32
|
+
在 React/Next.js 项目的入口文件(如 `App.tsx`/`layout.tsx`)中配置全局参数:
|
|
33
|
+
|
|
34
|
+
#### React 项目
|
|
35
|
+
```tsx
|
|
36
|
+
import { setTrackGlobalConfig, useTrackRetryListener } from 'react-track-hooks';
|
|
37
|
+
|
|
38
|
+
function App() {
|
|
39
|
+
// 全局埋点配置(只执行一次)
|
|
40
|
+
setTrackGlobalConfig({
|
|
41
|
+
trackUrl: 'https://api.yourdomain.com/track', // 单条埋点上报接口
|
|
42
|
+
batchTrackUrl: 'https://api.yourdomain.com/track/batch', // 批量埋点上报接口(可选)
|
|
43
|
+
enable: process.env.NODE_ENV === 'production', // 生产环境开启
|
|
44
|
+
enableBatch: true, // 全局开启批量上报
|
|
45
|
+
retryConfig: {
|
|
46
|
+
maxRetryTimes: 5, // 最大重试次数
|
|
47
|
+
initialDelay: 1000, // 初始重试延迟(ms)
|
|
48
|
+
delayMultiplier: 2, // 延迟倍数(指数退避)
|
|
49
|
+
},
|
|
50
|
+
batchConfig: {
|
|
51
|
+
batchSize: 10, // 队列达到10条时触发批量上报
|
|
52
|
+
batchInterval: 5000, // 每5秒触发一次批量上报
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 启用失败埋点自动重试监听(全局只执行一次)
|
|
57
|
+
useTrackRetryListener();
|
|
58
|
+
|
|
59
|
+
return <>{/* 你的应用内容 */}</>;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### Next.js App Router
|
|
64
|
+
```tsx
|
|
65
|
+
// app/components/TrackProvider.tsx (客户端组件)
|
|
66
|
+
'use client';
|
|
67
|
+
import { setTrackGlobalConfig, useTrackRetryListener } from 'react-track-hooks';
|
|
68
|
+
|
|
69
|
+
export const TrackProvider = () => {
|
|
70
|
+
setTrackGlobalConfig({
|
|
71
|
+
trackUrl: 'https://api.yourdomain.com/track',
|
|
72
|
+
batchTrackUrl: 'https://api.yourdomain.com/track/batch',
|
|
73
|
+
enable: process.env.NODE_ENV === 'production',
|
|
74
|
+
enableBatch: true, // 全局开启批量上报
|
|
75
|
+
batchConfig: {
|
|
76
|
+
batchSize: 15,
|
|
77
|
+
batchInterval: 3000
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
useTrackRetryListener();
|
|
82
|
+
return null;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// app/layout.tsx (根布局)
|
|
86
|
+
import { TrackProvider } from './components/TrackProvider';
|
|
87
|
+
|
|
88
|
+
export default function RootLayout({ children }) {
|
|
89
|
+
return (
|
|
90
|
+
<html>
|
|
91
|
+
<body>
|
|
92
|
+
<TrackProvider />
|
|
93
|
+
{children}
|
|
94
|
+
</body>
|
|
95
|
+
</html>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 2. 业务组件中使用埋点 Hooks
|
|
101
|
+
|
|
102
|
+
#### 点击埋点
|
|
103
|
+
```tsx
|
|
104
|
+
import { useTrackClick } from 'react-track-hooks';
|
|
105
|
+
|
|
106
|
+
function ButtonComponent() {
|
|
107
|
+
// 初始化点击埋点
|
|
108
|
+
const handleClick = useTrackClick(
|
|
109
|
+
'button_click', // 埋点事件名
|
|
110
|
+
{ button_type: 'primary', page: 'home' }, // 基础参数
|
|
111
|
+
{
|
|
112
|
+
enable: true,
|
|
113
|
+
enableBatch: false // 单个埋点关闭批量上报(覆盖全局配置)
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
// 点击时可追加动态参数
|
|
119
|
+
<button onClick={(e) => handleClick(e, { click_pos: 'top' })}>
|
|
120
|
+
测试点击埋点
|
|
121
|
+
</button>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### 曝光埋点
|
|
127
|
+
```tsx
|
|
128
|
+
import { useTrackExposure } from 'react-track-hooks';
|
|
129
|
+
|
|
130
|
+
function CardComponent() {
|
|
131
|
+
// 初始化曝光埋点(返回 ref 绑定到目标元素)
|
|
132
|
+
const exposureRef = useTrackExposure<HTMLDivElement>(
|
|
133
|
+
'card_exposure', // 埋点事件名
|
|
134
|
+
{ card_id: '123456', card_type: 'product' }, // 基础参数
|
|
135
|
+
{
|
|
136
|
+
exposureThreshold: 0.8, // 元素可见比例≥80%时触发
|
|
137
|
+
exposureOnce: true, // 仅触发一次曝光
|
|
138
|
+
enableBatch: true // 启用批量上报
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div ref={exposureRef} style={{ width: '300px', height: '200px' }}>
|
|
144
|
+
这是一个曝光埋点卡片
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### 页面停留时长埋点
|
|
151
|
+
```tsx
|
|
152
|
+
import { useTrackPageStay } from 'react-track-hooks';
|
|
153
|
+
|
|
154
|
+
function HomePage() {
|
|
155
|
+
// 初始化页面停留埋点(组件挂载时自动监听)
|
|
156
|
+
useTrackPageStay(
|
|
157
|
+
'page_stay', // 埋点事件名
|
|
158
|
+
{ page_path: '/home', platform: 'web' }, // 基础参数
|
|
159
|
+
{ enableBatch: true } // 启用批量上报
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
return <div>首页内容</div>;
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### 自定义埋点
|
|
167
|
+
```tsx
|
|
168
|
+
import { useTrackCustom } from 'react-track-hooks';
|
|
169
|
+
|
|
170
|
+
function FormComponent() {
|
|
171
|
+
// 初始化自定义埋点
|
|
172
|
+
const triggerCustomTrack = useTrackCustom(
|
|
173
|
+
'form_submit', // 埋点事件名
|
|
174
|
+
{ form_id: 'login_form' }, // 基础参数
|
|
175
|
+
{ enableBatch: true } // 启用批量上报
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const handleSubmit = () => {
|
|
179
|
+
// 手动触发自定义埋点,可追加动态参数
|
|
180
|
+
triggerCustomTrack({ submit_time: Date.now(), status: 'success' });
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return <button onClick={handleSubmit}>提交表单</button>;
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 3. 手动重试失败埋点
|
|
188
|
+
```tsx
|
|
189
|
+
import { retryFailedTracks } from 'react-track-hooks';
|
|
190
|
+
|
|
191
|
+
function RetryButton() {
|
|
192
|
+
const handleRetry = async () => {
|
|
193
|
+
// 手动触发失败埋点重试(force: true 强制立即重试,忽略指数退避时间)
|
|
194
|
+
await retryFailedTracks(true);
|
|
195
|
+
alert('失败埋点重试流程已执行!');
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return <button onClick={handleRetry}>重试失败埋点</button>;
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## API 文档
|
|
203
|
+
|
|
204
|
+
### 全局配置
|
|
205
|
+
#### setTrackGlobalConfig(config: TrackGlobalConfig)
|
|
206
|
+
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
207
|
+
|------|------|------|--------|------|
|
|
208
|
+
| trackUrl | string | 是 | - | 单条埋点上报接口地址 |
|
|
209
|
+
| batchTrackUrl | string | 否 | /api/track/batch | 批量埋点上报接口地址 |
|
|
210
|
+
| enable | boolean | 否 | true | 是否开启埋点 |
|
|
211
|
+
| enableBatch | boolean | 否 | true | 是否开启批量上报 |
|
|
212
|
+
| retryConfig | RetryConfig | 否 | 见下方 | 重试配置 |
|
|
213
|
+
| batchConfig | BatchConfig | 否 | 见下方 | 批量上报配置 |
|
|
214
|
+
|
|
215
|
+
#### RetryConfig 类型
|
|
216
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
217
|
+
|------|------|--------|------|
|
|
218
|
+
| maxRetryTimes | number | 3 | 最大重试次数(超过则清理埋点) |
|
|
219
|
+
| initialDelay | number | 1000 | 初始重试延迟(ms) |
|
|
220
|
+
| delayMultiplier | number | 2 | 延迟倍数(指数退避算法) |
|
|
221
|
+
|
|
222
|
+
#### BatchConfig 类型
|
|
223
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
224
|
+
|------|------|--------|------|
|
|
225
|
+
| batchSize | number | 10 | 触发批量上报的队列容量上限 |
|
|
226
|
+
| batchInterval | number | 5000 | 触发批量上报的时间间隔(ms) |
|
|
227
|
+
|
|
228
|
+
### Hooks
|
|
229
|
+
#### useTrackRetryListener()
|
|
230
|
+
- 作用:全局监听页面状态(初始化/切回标签页/浏览器空闲),自动触发失败埋点重试
|
|
231
|
+
- 特性:内置防并发机制,避免重复执行重试流程
|
|
232
|
+
- 注意:全局只需调用一次,建议放在项目入口
|
|
233
|
+
|
|
234
|
+
#### useTrackClick(eventName, baseParams?, config?)
|
|
235
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
236
|
+
|------|------|------|------|
|
|
237
|
+
| eventName | string | 是 | 埋点事件名 |
|
|
238
|
+
| baseParams | TrackParams | 否 | 基础业务参数 |
|
|
239
|
+
| config | TrackConfig | 否 | 单个埋点配置(可覆盖全局批量/重试配置) |
|
|
240
|
+
| 返回值 | (e?, extraParams?) => void | - | 点击事件处理函数,可追加动态参数 |
|
|
241
|
+
|
|
242
|
+
#### useTrackExposure<T extends HTMLElement>(eventName, baseParams?, config?)
|
|
243
|
+
通用曝光埋点 Hook,返回泛型 ref,可绑定到任意 DOM 元素,元素进入视口时触发埋点上报。
|
|
244
|
+
|
|
245
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
246
|
+
|------|------|------|------|
|
|
247
|
+
| eventName | string | 是 | 埋点事件名 |
|
|
248
|
+
| baseParams | TrackParams | 否 | 基础业务参数,会和曝光自动采集参数合并上报 |
|
|
249
|
+
| config | TrackConfig | 否 | 曝光配置 + 批量/重试配置 |
|
|
250
|
+
| 泛型 T | T extends HTMLElement | 否 | 可选,指定 ref 绑定的 DOM 元素类型(默认 `HTMLElement`) |
|
|
251
|
+
| 返回值 | React.RefObject<T> | - | 需绑定到目标元素的 ref,类型与泛型 T 一致 |
|
|
252
|
+
|
|
253
|
+
#### useTrackPageStay(eventName, baseParams?, config?)
|
|
254
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
255
|
+
|------|------|------|------|
|
|
256
|
+
| eventName | string | 是 | 埋点事件名 |
|
|
257
|
+
| baseParams | TrackParams | 否 | 基础业务参数 |
|
|
258
|
+
| config | TrackConfig | 否 | 单个埋点配置(可覆盖全局批量/重试配置) |
|
|
259
|
+
|
|
260
|
+
#### useTrackCustom(eventName, baseParams?, config?)
|
|
261
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
262
|
+
|------|------|------|------|
|
|
263
|
+
| eventName | string | 是 | 埋点事件名 |
|
|
264
|
+
| baseParams | TrackParams | 否 | 基础业务参数 |
|
|
265
|
+
| config | TrackConfig | 否 | 单个埋点配置(可覆盖全局批量/重试配置) |
|
|
266
|
+
| 返回值 | (extraParams?) => void | - | 手动触发埋点的函数 |
|
|
267
|
+
|
|
268
|
+
### 工具函数
|
|
269
|
+
#### retryFailedTracks(force?: boolean): Promise<void>
|
|
270
|
+
增强型失败埋点重试函数,支持批量/单条自适应重试,内置指数退避算法和防并发机制。
|
|
271
|
+
|
|
272
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
273
|
+
|------|------|--------|------|
|
|
274
|
+
| force | boolean | false | 是否强制立即重试(忽略指数退避时间) |
|
|
275
|
+
| 返回值 | Promise<void> | - | 重试流程完成的 Promise |
|
|
276
|
+
|
|
277
|
+
### 通用类型
|
|
278
|
+
#### TrackParams
|
|
279
|
+
```ts
|
|
280
|
+
interface TrackParams {
|
|
281
|
+
eventName: string;
|
|
282
|
+
type: 'click' | 'exposure' | 'page_stay' | 'custom';
|
|
283
|
+
[key: string]: any; // 自定义业务参数
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
#### TrackConfig
|
|
288
|
+
```ts
|
|
289
|
+
interface TrackConfig extends Partial<TrackGlobalConfig> {
|
|
290
|
+
exposureOnce?: boolean; // 曝光埋点仅生效一次(默认 true)
|
|
291
|
+
exposureThreshold?: number; // 曝光埋点触发阈值(0-1,默认 0.5)
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
#### TrackGlobalConfig(完整类型定义)
|
|
296
|
+
```ts
|
|
297
|
+
export interface TrackGlobalConfig {
|
|
298
|
+
// 埋点上报接口 URL
|
|
299
|
+
trackUrl: string;
|
|
300
|
+
// 批量上报接口 URL
|
|
301
|
+
batchTrackUrl?: string;
|
|
302
|
+
// 是否开启埋点
|
|
303
|
+
enable?: boolean;
|
|
304
|
+
// 是否开启批量上报
|
|
305
|
+
enableBatch?: boolean
|
|
306
|
+
// 重试配置
|
|
307
|
+
retryConfig?: {
|
|
308
|
+
maxRetryTimes: number;
|
|
309
|
+
initialDelay: number;
|
|
310
|
+
delayMultiplier: number;
|
|
311
|
+
};
|
|
312
|
+
// 批量上报配置
|
|
313
|
+
batchConfig?: {
|
|
314
|
+
batchSize: number, // 队列容量上限
|
|
315
|
+
batchInterval: number, // 触发上报间隔
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## 核心能力说明
|
|
321
|
+
### 批量上报机制
|
|
322
|
+
1. **入队规则**:开启批量上报后,埋点参数先进入内存队列,而非直接发送请求
|
|
323
|
+
2. **触发条件**:满足以下任一条件即触发批量上报:
|
|
324
|
+
- 队列长度达到 `batchSize`(默认 10)
|
|
325
|
+
- 距离上次上报超过 `batchInterval`(默认 5000ms)
|
|
326
|
+
3. **异常处理**:批量上报失败时,所有埋点会自动转入失败队列,参与重试逻辑
|
|
327
|
+
4. **优先级**:单个埋点配置的 `enableBatch` 优先级高于全局配置
|
|
328
|
+
|
|
329
|
+
### 增强型失败重试机制
|
|
330
|
+
#### 核心流程
|
|
331
|
+
1. **失败存储**:上报失败的埋点会存入 localStorage,避免页面刷新丢失
|
|
332
|
+
2. **前置清理**:重试前自动清理超过 `maxRetryTimes` 的过期埋点,避免内存膨胀
|
|
333
|
+
3. **智能筛选**:基于**指数退避算法**筛选可重试埋点:
|
|
334
|
+
```
|
|
335
|
+
重试延迟时间 = initialDelay * (delayMultiplier ^ 当前重试次数)
|
|
336
|
+
```
|
|
337
|
+
例如:初始延迟 1s,倍数 2 → 第1次重试延迟 1s,第2次 2s,第3次 4s...
|
|
338
|
+
4. **自适应重试**:
|
|
339
|
+
- 开启批量时:调用 `batchTrackUrl` 一次性重试所有符合条件的埋点
|
|
340
|
+
- 关闭批量时:逐条调用 `trackUrl` 重试,失败单条不影响其他
|
|
341
|
+
5. **状态更新**:
|
|
342
|
+
- 重试成功:从失败队列移除对应埋点
|
|
343
|
+
- 重试失败:自动更新 `retryCount` 和 `retryTime`,等待下次重试
|
|
344
|
+
6. **重试时机**:
|
|
345
|
+
- 首屏渲染 3 秒后自动重试
|
|
346
|
+
- 页面从不可见变为可见时重试
|
|
347
|
+
- 浏览器空闲时周期性重试(最迟 30 秒一次)
|
|
348
|
+
- 埋点上报成功后自动触发重试
|
|
349
|
+
- 可通过 `retryFailedTracks` 手动触发
|
|
350
|
+
|
|
351
|
+
#### 防并发保护
|
|
352
|
+
- 内置 `isRetryRunning` 状态标记,避免同时执行多个重试流程
|
|
353
|
+
- 所有异常被统一捕获,确保 `isRetryRunning` 能正常重置
|
|
354
|
+
|
|
355
|
+
## 适配说明
|
|
356
|
+
- React 版本:支持 React 16.8+(Hooks 最低兼容版本)
|
|
357
|
+
- Next.js 版本:支持 Next.js 13+(App Router/Pages Router)
|
|
358
|
+
- 浏览器兼容:支持所有现代浏览器,IE 需自行兼容 Promise/IntersectionObserver/requestIdleCallback
|
|
359
|
+
|
|
360
|
+
## 常见问题
|
|
361
|
+
### Q1: TS7016 类型声明找不到?
|
|
362
|
+
A: 确保安装的是最新版本,若仍报错,可在项目中添加类型声明文件:
|
|
363
|
+
```ts
|
|
364
|
+
// types/react-track-hooks.d.ts
|
|
365
|
+
declare module 'react-track-hooks';
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Q2: 曝光埋点不触发?
|
|
369
|
+
A: 检查:
|
|
370
|
+
1. 元素是否绑定 ref;
|
|
371
|
+
2. 可见比例是否达到 `exposureThreshold`;
|
|
372
|
+
3. 元素是否为固定定位/脱离文档流(需确保 IntersectionObserver 能检测到);
|
|
373
|
+
4. 全局/单个埋点的 `enable` 是否为 `true`。
|
|
374
|
+
|
|
375
|
+
### Q3: 批量上报不生效?
|
|
376
|
+
A: 检查:
|
|
377
|
+
1. 全局/单个埋点的 `enableBatch` 是否为 `true`;
|
|
378
|
+
2. `batchTrackUrl` 是否配置正确;
|
|
379
|
+
3. 队列长度是否未达到 `batchSize` 且未到 `batchInterval` 时间。
|
|
380
|
+
|
|
381
|
+
### Q4: 埋点上报失败不重试?
|
|
382
|
+
A: 确保:
|
|
383
|
+
1. 已调用 `useTrackRetryListener()`;
|
|
384
|
+
2. 重试次数未超过 `maxRetryTimes`;
|
|
385
|
+
3. localStorage 未被禁用(失败埋点依赖 localStorage 存储);
|
|
386
|
+
4. 重试时间未到(可通过 `retryFailedTracks(true)` 强制重试验证)。
|
|
387
|
+
|
|
388
|
+
### Q5: 批量重试后部分埋点仍显示失败?
|
|
389
|
+
A: 批量重试为原子操作:
|
|
390
|
+
- 接口返回 2xx → 所有埋点视为成功,从失败队列移除
|
|
391
|
+
- 接口返回非 2xx/网络错误 → 所有埋点视为失败,更新重试次数
|
|
392
|
+
|
|
393
|
+
## 许可证
|
|
394
|
+
MIT © [liujingmin](https://github.com/PassingTraveller111)
|