befly 3.9.12 → 3.9.13
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/docs/README.md +85 -0
- package/docs/addon.md +512 -0
- package/docs/api.md +1368 -0
- package/docs/cipher.md +580 -0
- package/docs/config.md +638 -0
- package/docs/examples.md +799 -0
- package/docs/hook.md +754 -0
- package/docs/logger.md +495 -0
- package/docs/plugin.md +978 -0
- package/docs/quickstart.md +331 -0
- package/docs/sync.md +586 -0
- package/docs/table.md +765 -0
- package/docs/validator.md +588 -0
- package/package.json +3 -3
- package/sync/syncDb/apply.ts +1 -1
- package/sync/syncDb/constants.ts +0 -11
- package/sync/syncDb/helpers.ts +0 -1
- package/sync/syncDb/table.ts +2 -2
- package/sync/syncDb/tableCreate.ts +3 -3
- package/tests/syncDb-constants.test.ts +1 -23
- package/tests/syncDb-helpers.test.ts +0 -1
- package/types/database.d.ts +0 -2
package/docs/plugin.md
ADDED
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
# 插件开发指南
|
|
2
|
+
|
|
3
|
+
> 本文档详细介绍 Befly 框架的插件系统,包括插件结构、生命周期、内置插件及自定义插件开发。
|
|
4
|
+
|
|
5
|
+
## 目录
|
|
6
|
+
|
|
7
|
+
- [插件开发指南](#插件开发指南)
|
|
8
|
+
- [目录](#目录)
|
|
9
|
+
- [概述](#概述)
|
|
10
|
+
- [核心特性](#核心特性)
|
|
11
|
+
- [插件类型](#插件类型)
|
|
12
|
+
- [插件结构](#插件结构)
|
|
13
|
+
- [基础结构](#基础结构)
|
|
14
|
+
- [完整类型定义](#完整类型定义)
|
|
15
|
+
- [插件存放位置](#插件存放位置)
|
|
16
|
+
- [插件命名规则](#插件命名规则)
|
|
17
|
+
- [插件加载流程](#插件加载流程)
|
|
18
|
+
- [加载顺序](#加载顺序)
|
|
19
|
+
- [依赖排序](#依赖排序)
|
|
20
|
+
- [内置插件](#内置插件)
|
|
21
|
+
- [logger - 日志插件](#logger---日志插件)
|
|
22
|
+
- [config - 配置插件](#config---配置插件)
|
|
23
|
+
- [db - 数据库插件](#db---数据库插件)
|
|
24
|
+
- [redis - Redis 插件](#redis---redis-插件)
|
|
25
|
+
- [jwt - JWT 插件](#jwt---jwt-插件)
|
|
26
|
+
- [cipher - 加密插件](#cipher---加密插件)
|
|
27
|
+
- [cache - 缓存插件](#cache---缓存插件)
|
|
28
|
+
- [tool - 工具插件](#tool---工具插件)
|
|
29
|
+
- [自定义插件开发](#自定义插件开发)
|
|
30
|
+
- [基础插件](#基础插件)
|
|
31
|
+
- [带依赖的插件](#带依赖的插件)
|
|
32
|
+
- [异步初始化插件](#异步初始化插件)
|
|
33
|
+
- [带配置的插件](#带配置的插件)
|
|
34
|
+
- [完整插件示例](#完整插件示例)
|
|
35
|
+
- [插件访问方式](#插件访问方式)
|
|
36
|
+
- [禁用插件](#禁用插件)
|
|
37
|
+
- [Addon 插件](#addon-插件)
|
|
38
|
+
- [Addon 插件示例](#addon-插件示例)
|
|
39
|
+
- [最佳实践](#最佳实践)
|
|
40
|
+
- [常见问题](#常见问题)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 概述
|
|
45
|
+
|
|
46
|
+
Befly 插件系统是框架的核心扩展机制,允许开发者封装和复用功能模块。插件在应用启动时自动加载并初始化,其实例会挂载到 `befly` 全局对象上供 API 和其他插件使用。
|
|
47
|
+
|
|
48
|
+
### 核心特性
|
|
49
|
+
|
|
50
|
+
- **自动发现**:自动扫描指定目录加载插件
|
|
51
|
+
- **依赖管理**:支持声明插件依赖关系,按依赖顺序初始化
|
|
52
|
+
- **全局访问**:插件实例挂载到 `befly` 对象,API 中通过 `befly.插件名` 访问
|
|
53
|
+
- **生命周期**:支持异步初始化,可访问框架上下文
|
|
54
|
+
- **可禁用**:通过配置禁用指定插件
|
|
55
|
+
|
|
56
|
+
### 插件类型
|
|
57
|
+
|
|
58
|
+
| 类型 | 目录位置 | 插件名格式 |
|
|
59
|
+
| ---------- | ------------------------------- | ------------------------------ |
|
|
60
|
+
| 核心插件 | `packages/core/plugins/` | `{fileName}` |
|
|
61
|
+
| Addon 插件 | `packages/addon{Name}/plugins/` | `addon_{addonName}_{fileName}` |
|
|
62
|
+
| 项目插件 | `项目根目录/plugins/` | `app_{fileName}` |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 插件结构
|
|
67
|
+
|
|
68
|
+
### 基础结构
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import type { Plugin } from 'befly-core/types/plugin';
|
|
72
|
+
|
|
73
|
+
const plugin: Plugin = {
|
|
74
|
+
// 依赖的插件列表(可选)
|
|
75
|
+
after: ['logger', 'db'],
|
|
76
|
+
|
|
77
|
+
// 初始化函数(必填)
|
|
78
|
+
handler: (befly) => {
|
|
79
|
+
// 返回插件实例
|
|
80
|
+
return {
|
|
81
|
+
someMethod: () => {
|
|
82
|
+
/* ... */
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export default plugin;
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 完整类型定义
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
interface Plugin {
|
|
95
|
+
/** 插件名称(运行时自动生成,无需手动设置) */
|
|
96
|
+
name?: string;
|
|
97
|
+
|
|
98
|
+
/** 依赖的插件列表(在这些插件之后执行) */
|
|
99
|
+
after?: string[];
|
|
100
|
+
|
|
101
|
+
/** 插件初始化函数 */
|
|
102
|
+
handler?: (context: BeflyContext) => any | Promise<any>;
|
|
103
|
+
|
|
104
|
+
/** 插件描述(可选) */
|
|
105
|
+
description?: string;
|
|
106
|
+
|
|
107
|
+
/** 插件版本(可选) */
|
|
108
|
+
version?: string;
|
|
109
|
+
|
|
110
|
+
/** 插件作者(可选) */
|
|
111
|
+
author?: string;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 插件存放位置
|
|
118
|
+
|
|
119
|
+
| 类型 | 目录 | 说明 |
|
|
120
|
+
| ---------- | ------------------------------- | ------------------------- |
|
|
121
|
+
| 核心插件 | `packages/core/plugins/` | 框架内置插件,随框架发布 |
|
|
122
|
+
| Addon 插件 | `packages/addon{Name}/plugins/` | 组件包插件,随 Addon 发布 |
|
|
123
|
+
| 项目插件 | `项目根目录/plugins/` | 项目自定义插件 |
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 插件命名规则
|
|
128
|
+
|
|
129
|
+
插件名称由 **文件名** 自动生成(小驼峰转换),**禁止在插件对象中定义 `name` 属性**。
|
|
130
|
+
|
|
131
|
+
| 文件名 | 生成的插件名 | 访问方式 |
|
|
132
|
+
| -------------- | ------------ | ---------------- |
|
|
133
|
+
| `db.ts` | `db` | `befly.db` |
|
|
134
|
+
| `redis.ts` | `redis` | `befly.redis` |
|
|
135
|
+
| `myPlugin.ts` | `myPlugin` | `befly.myPlugin` |
|
|
136
|
+
| `my_plugin.ts` | `myPlugin` | `befly.myPlugin` |
|
|
137
|
+
|
|
138
|
+
**Addon 插件命名规则**:
|
|
139
|
+
|
|
140
|
+
| Addon 名称 | 文件名 | 生成的插件名 |
|
|
141
|
+
| ------------ | ----------- | ------------------------ |
|
|
142
|
+
| `addonAdmin` | `email.ts` | `addon_addonAdmin_email` |
|
|
143
|
+
| `addonPay` | `wechat.ts` | `addon_addonPay_wechat` |
|
|
144
|
+
|
|
145
|
+
**项目插件命名规则**:
|
|
146
|
+
|
|
147
|
+
| 文件名 | 生成的插件名 |
|
|
148
|
+
| -------- | ------------ |
|
|
149
|
+
| `sms.ts` | `app_sms` |
|
|
150
|
+
| `oss.ts` | `app_oss` |
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 插件加载流程
|
|
155
|
+
|
|
156
|
+
### 加载顺序
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
1. 核心插件(core/plugins/)
|
|
160
|
+
↓
|
|
161
|
+
2. Addon 插件(addon{Name}/plugins/)
|
|
162
|
+
↓
|
|
163
|
+
3. 项目插件(项目/plugins/)
|
|
164
|
+
↓
|
|
165
|
+
4. 过滤禁用的插件
|
|
166
|
+
↓
|
|
167
|
+
5. 按依赖关系排序
|
|
168
|
+
↓
|
|
169
|
+
6. 依次初始化并挂载到 befly
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 依赖排序
|
|
173
|
+
|
|
174
|
+
使用 `after` 属性声明依赖关系:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// redis.ts - 依赖 logger
|
|
178
|
+
const plugin: Plugin = {
|
|
179
|
+
after: ['logger'], // 在 logger 插件之后初始化
|
|
180
|
+
handler: () => {
|
|
181
|
+
/* ... */
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**依赖检查**:
|
|
187
|
+
|
|
188
|
+
- 如果依赖的插件不存在,启动时报错
|
|
189
|
+
- 如果存在循环依赖,启动时报错
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 内置插件
|
|
194
|
+
|
|
195
|
+
### logger - 日志插件
|
|
196
|
+
|
|
197
|
+
提供全局日志功能。
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// 插件源码
|
|
201
|
+
const loggerPlugin: Plugin = {
|
|
202
|
+
after: [],
|
|
203
|
+
async handler(): Promise<typeof Logger> {
|
|
204
|
+
if (beflyConfig.logger) {
|
|
205
|
+
Logger.configure(beflyConfig.logger);
|
|
206
|
+
}
|
|
207
|
+
return Logger;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**使用方式**:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
befly.logger.info('信息日志');
|
|
216
|
+
befly.logger.warn('警告日志');
|
|
217
|
+
befly.logger.error({ err: error }, '错误日志');
|
|
218
|
+
befly.logger.debug('调试日志');
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
### config - 配置插件
|
|
224
|
+
|
|
225
|
+
提供访问项目配置的能力。
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// 插件源码
|
|
229
|
+
const plugin: Plugin = {
|
|
230
|
+
handler: () => {
|
|
231
|
+
return beflyConfig;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**使用方式**:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
const port = befly.config.appPort;
|
|
240
|
+
const dbConfig = befly.config.database;
|
|
241
|
+
const redisConfig = befly.config.redis;
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
### db - 数据库插件
|
|
247
|
+
|
|
248
|
+
提供数据库操作能力,依赖 `logger` 插件。
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
// 插件源码
|
|
252
|
+
const dbPlugin: Plugin = {
|
|
253
|
+
after: ['logger'],
|
|
254
|
+
async handler(befly: BeflyContext): Promise<DbHelper> {
|
|
255
|
+
const sql = await Connect.connectSql();
|
|
256
|
+
return new DbHelper(befly, sql);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**使用方式**:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// 查询
|
|
265
|
+
const user = await befly.db.getOne({ table: 'user', where: { id: 1 } });
|
|
266
|
+
|
|
267
|
+
// 插入
|
|
268
|
+
const id = await befly.db.insData({ table: 'user', data: { name: '张三' } });
|
|
269
|
+
|
|
270
|
+
// 更新
|
|
271
|
+
await befly.db.updData({ table: 'user', data: { name: '李四' }, where: { id: 1 } });
|
|
272
|
+
|
|
273
|
+
// 删除
|
|
274
|
+
await befly.db.delData({ table: 'user', where: { id: 1 } });
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
> 详细用法请参考 [database.md](./database.md)
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
### redis - Redis 插件
|
|
282
|
+
|
|
283
|
+
提供 Redis 缓存操作能力,依赖 `logger` 插件。
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
// 插件源码
|
|
287
|
+
const redisPlugin: Plugin = {
|
|
288
|
+
after: ['logger'],
|
|
289
|
+
async handler(): Promise<RedisHelper | Record<string, never>> {
|
|
290
|
+
await Connect.connectRedis();
|
|
291
|
+
return new RedisHelper(redisConfig.prefix);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**使用方式**:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
// 字符串操作
|
|
300
|
+
await befly.redis.setString('key', 'value', 3600);
|
|
301
|
+
const value = await befly.redis.getString('key');
|
|
302
|
+
|
|
303
|
+
// 对象操作
|
|
304
|
+
await befly.redis.setObject('user:1', { name: '张三' });
|
|
305
|
+
const user = await befly.redis.getObject('user:1');
|
|
306
|
+
|
|
307
|
+
// 集合操作
|
|
308
|
+
await befly.redis.sadd('set:key', 'member1', 'member2');
|
|
309
|
+
const isMember = await befly.redis.sismember('set:key', 'member1');
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
> 详细用法请参考 [redis.md](./redis.md)
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
### jwt - JWT 插件
|
|
317
|
+
|
|
318
|
+
提供 JWT Token 签发和验证功能。
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
// 插件源码
|
|
322
|
+
const jwtPlugin: Plugin = {
|
|
323
|
+
handler: () => {
|
|
324
|
+
return new Jwt(beflyConfig.auth);
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**使用方式**:
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
// 签发 Token
|
|
333
|
+
const token = await befly.jwt.sign(
|
|
334
|
+
{
|
|
335
|
+
id: user.id,
|
|
336
|
+
roleCode: user.roleCode
|
|
337
|
+
},
|
|
338
|
+
{ expiresIn: '7d' }
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// 验证 Token
|
|
342
|
+
const payload = await befly.jwt.verify(token);
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
### cipher - 加密插件
|
|
348
|
+
|
|
349
|
+
提供密码哈希、加密解密等功能。
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
// 插件源码
|
|
353
|
+
const plugin: Plugin = {
|
|
354
|
+
handler: () => {
|
|
355
|
+
return Cipher;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**使用方式**:
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
// 密码哈希
|
|
364
|
+
const hashedPassword = await befly.cipher.hashPassword('123456');
|
|
365
|
+
|
|
366
|
+
// 密码验证
|
|
367
|
+
const isValid = await befly.cipher.verifyPassword('123456', hashedPassword);
|
|
368
|
+
|
|
369
|
+
// AES 加密
|
|
370
|
+
const encrypted = befly.cipher.encrypt('敏感数据');
|
|
371
|
+
|
|
372
|
+
// AES 解密
|
|
373
|
+
const decrypted = befly.cipher.decrypt(encrypted);
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
### cache - 缓存插件
|
|
379
|
+
|
|
380
|
+
提供应用级缓存管理功能。
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
// 插件源码
|
|
384
|
+
const cachePlugin: Plugin = {
|
|
385
|
+
after: [],
|
|
386
|
+
async handler(befly: BeflyContext): Promise<CacheHelper> {
|
|
387
|
+
return new CacheHelper(befly);
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**使用方式**:
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
// 刷新角色权限缓存
|
|
396
|
+
await befly.cache.refreshRoleApis('admin');
|
|
397
|
+
|
|
398
|
+
// 获取菜单缓存
|
|
399
|
+
const menus = await befly.cache.getMenus();
|
|
400
|
+
|
|
401
|
+
// 刷新所有缓存
|
|
402
|
+
await befly.cache.refreshAll();
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
### tool - 工具插件
|
|
408
|
+
|
|
409
|
+
提供 API 响应辅助函数。
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
// 插件源码
|
|
413
|
+
const plugin: Plugin = {
|
|
414
|
+
handler: () => {
|
|
415
|
+
return {
|
|
416
|
+
Yes: Yes,
|
|
417
|
+
No: No
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**使用方式**:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
// 成功响应
|
|
427
|
+
return befly.tool.Yes('操作成功', { id: 1 });
|
|
428
|
+
// 返回: { code: 0, msg: '操作成功', data: { id: 1 } }
|
|
429
|
+
|
|
430
|
+
// 失败响应
|
|
431
|
+
return befly.tool.No('操作失败');
|
|
432
|
+
// 返回: { code: 1, msg: '操作失败', data: null }
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## 自定义插件开发
|
|
438
|
+
|
|
439
|
+
### 基础插件
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
// plugins/hello.ts
|
|
443
|
+
import type { Plugin } from 'befly-core/types/plugin';
|
|
444
|
+
|
|
445
|
+
const plugin: Plugin = {
|
|
446
|
+
handler: () => {
|
|
447
|
+
return {
|
|
448
|
+
sayHello: (name: string) => `Hello, ${name}!`
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
export default plugin;
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**使用**:
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
// 项目插件名为 app_hello
|
|
460
|
+
const greeting = befly.app_hello.sayHello('World');
|
|
461
|
+
// 返回: "Hello, World!"
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
### 带依赖的插件
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
// plugins/userService.ts
|
|
470
|
+
import type { Plugin } from 'befly-core/types/plugin';
|
|
471
|
+
import type { BeflyContext } from 'befly-core/types/befly';
|
|
472
|
+
|
|
473
|
+
const plugin: Plugin = {
|
|
474
|
+
after: ['db', 'redis'], // 依赖数据库和 Redis
|
|
475
|
+
handler: (befly: BeflyContext) => {
|
|
476
|
+
return {
|
|
477
|
+
async getUser(id: number) {
|
|
478
|
+
// 先从缓存获取
|
|
479
|
+
const cacheKey = `user:${id}`;
|
|
480
|
+
let user = await befly.redis.getObject(cacheKey);
|
|
481
|
+
|
|
482
|
+
if (!user) {
|
|
483
|
+
// 缓存不存在,从数据库查询
|
|
484
|
+
user = await befly.db.getOne({
|
|
485
|
+
table: 'user',
|
|
486
|
+
where: { id: id }
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// 写入缓存
|
|
490
|
+
if (user) {
|
|
491
|
+
await befly.redis.setObject(cacheKey, user, 3600);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return user;
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
export default plugin;
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
### 异步初始化插件
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
// plugins/elastic.ts
|
|
510
|
+
import type { Plugin } from 'befly-core/types/plugin';
|
|
511
|
+
import { Client } from '@elastic/elasticsearch';
|
|
512
|
+
|
|
513
|
+
const plugin: Plugin = {
|
|
514
|
+
after: ['logger', 'config'],
|
|
515
|
+
async handler(befly) {
|
|
516
|
+
const config = befly.config.elasticsearch || {};
|
|
517
|
+
|
|
518
|
+
const client = new Client({
|
|
519
|
+
node: config.node || 'http://localhost:9200',
|
|
520
|
+
auth: config.auth
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// 测试连接
|
|
524
|
+
try {
|
|
525
|
+
await client.ping();
|
|
526
|
+
befly.logger.info('Elasticsearch 连接成功');
|
|
527
|
+
} catch (error) {
|
|
528
|
+
befly.logger.error({ err: error }, 'Elasticsearch 连接失败');
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
client: client,
|
|
534
|
+
async search(index: string, query: object) {
|
|
535
|
+
return client.search({ index: index, body: query });
|
|
536
|
+
},
|
|
537
|
+
async index(index: string, document: object) {
|
|
538
|
+
return client.index({ index: index, body: document });
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
export default plugin;
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
### 带配置的插件
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
// plugins/sms.ts
|
|
553
|
+
import type { Plugin } from 'befly-core/types/plugin';
|
|
554
|
+
import type { BeflyContext } from 'befly-core/types/befly';
|
|
555
|
+
|
|
556
|
+
interface SmsConfig {
|
|
557
|
+
accessKeyId: string;
|
|
558
|
+
accessKeySecret: string;
|
|
559
|
+
signName: string;
|
|
560
|
+
templateCode: string;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
class SmsHelper {
|
|
564
|
+
private config: SmsConfig;
|
|
565
|
+
private befly: BeflyContext;
|
|
566
|
+
|
|
567
|
+
constructor(befly: BeflyContext, config: SmsConfig) {
|
|
568
|
+
this.befly = befly;
|
|
569
|
+
this.config = config;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async send(phone: string, params: Record<string, string>) {
|
|
573
|
+
// 实现短信发送逻辑
|
|
574
|
+
this.befly.logger.info({ phone: phone }, '发送短信');
|
|
575
|
+
// ...
|
|
576
|
+
return { success: true };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const plugin: Plugin = {
|
|
581
|
+
after: ['logger', 'config'],
|
|
582
|
+
handler: (befly: BeflyContext) => {
|
|
583
|
+
const smsConfig = befly.config.sms || {};
|
|
584
|
+
return new SmsHelper(befly, smsConfig);
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
export default plugin;
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
### 完整插件示例
|
|
594
|
+
|
|
595
|
+
以 Addon 邮件插件为例:
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
// addonAdmin/plugins/email.ts
|
|
599
|
+
import nodemailer from 'nodemailer';
|
|
600
|
+
import type { Transporter } from 'nodemailer';
|
|
601
|
+
import type { Plugin } from 'befly-core/types/plugin';
|
|
602
|
+
import type { BeflyContext } from 'befly-core/types/befly';
|
|
603
|
+
|
|
604
|
+
/** 邮件配置 */
|
|
605
|
+
interface EmailConfig {
|
|
606
|
+
host: string;
|
|
607
|
+
port: number;
|
|
608
|
+
secure: boolean;
|
|
609
|
+
user: string;
|
|
610
|
+
pass: string;
|
|
611
|
+
fromName?: string;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** 发送邮件参数 */
|
|
615
|
+
interface SendEmailOptions {
|
|
616
|
+
to: string;
|
|
617
|
+
subject: string;
|
|
618
|
+
text?: string;
|
|
619
|
+
html?: string;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** 发送结果 */
|
|
623
|
+
interface SendEmailResult {
|
|
624
|
+
success: boolean;
|
|
625
|
+
messageId?: string;
|
|
626
|
+
error?: string;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* 邮件助手类
|
|
631
|
+
*/
|
|
632
|
+
class EmailHelper {
|
|
633
|
+
private config: EmailConfig;
|
|
634
|
+
private transporter: Transporter | null = null;
|
|
635
|
+
private befly: BeflyContext;
|
|
636
|
+
|
|
637
|
+
constructor(befly: BeflyContext, config: EmailConfig) {
|
|
638
|
+
this.befly = befly;
|
|
639
|
+
this.config = config;
|
|
640
|
+
|
|
641
|
+
if (this.config.user && this.config.pass) {
|
|
642
|
+
this.transporter = nodemailer.createTransport({
|
|
643
|
+
host: this.config.host,
|
|
644
|
+
port: this.config.port,
|
|
645
|
+
secure: this.config.secure,
|
|
646
|
+
auth: {
|
|
647
|
+
user: this.config.user,
|
|
648
|
+
pass: this.config.pass
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async send(options: SendEmailOptions): Promise<SendEmailResult> {
|
|
655
|
+
if (!this.transporter) {
|
|
656
|
+
return { success: false, error: '邮件服务未配置' };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
try {
|
|
660
|
+
const info = await this.transporter.sendMail({
|
|
661
|
+
from: `"${this.config.fromName}" <${this.config.user}>`,
|
|
662
|
+
to: options.to,
|
|
663
|
+
subject: options.subject,
|
|
664
|
+
text: options.text,
|
|
665
|
+
html: options.html
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
return { success: true, messageId: info.messageId };
|
|
669
|
+
} catch (error: any) {
|
|
670
|
+
return { success: false, error: error.message };
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async verify(): Promise<boolean> {
|
|
675
|
+
if (!this.transporter) return false;
|
|
676
|
+
try {
|
|
677
|
+
await this.transporter.verify();
|
|
678
|
+
return true;
|
|
679
|
+
} catch {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* 邮件插件
|
|
687
|
+
*/
|
|
688
|
+
const emailPlugin: Plugin = {
|
|
689
|
+
after: ['db', 'logger', 'config'],
|
|
690
|
+
async handler(befly: BeflyContext): Promise<EmailHelper> {
|
|
691
|
+
const emailConfig = befly.config?.addons?.admin?.email || {};
|
|
692
|
+
return new EmailHelper(befly, emailConfig);
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
export default emailPlugin;
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
**使用方式**:
|
|
700
|
+
|
|
701
|
+
```typescript
|
|
702
|
+
// Addon 插件名为 addon_addonAdmin_email
|
|
703
|
+
const result = await befly.addon_addonAdmin_email.send({
|
|
704
|
+
to: 'user@example.com',
|
|
705
|
+
subject: '验证码',
|
|
706
|
+
html: '<p>您的验证码是:123456</p>'
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
if (result.success) {
|
|
710
|
+
return befly.tool.Yes('邮件发送成功');
|
|
711
|
+
} else {
|
|
712
|
+
return befly.tool.No(result.error || '邮件发送失败');
|
|
713
|
+
}
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
---
|
|
717
|
+
|
|
718
|
+
## 插件访问方式
|
|
719
|
+
|
|
720
|
+
插件初始化后会挂载到 `befly` 全局对象:
|
|
721
|
+
|
|
722
|
+
```typescript
|
|
723
|
+
// 在 API handler 中
|
|
724
|
+
export default {
|
|
725
|
+
name: '示例接口',
|
|
726
|
+
handler: async (befly, ctx) => {
|
|
727
|
+
// 访问内置插件
|
|
728
|
+
const user = await befly.db.getOne({ table: 'user', where: { id: 1 } });
|
|
729
|
+
befly.logger.info({ user: user }, '查询用户');
|
|
730
|
+
|
|
731
|
+
// 访问项目插件
|
|
732
|
+
const result = await befly.app_sms.send('13800138000', { code: '123456' });
|
|
733
|
+
|
|
734
|
+
// 访问 Addon 插件
|
|
735
|
+
await befly.addon_addonAdmin_email.send({
|
|
736
|
+
to: 'admin@example.com',
|
|
737
|
+
subject: '通知'
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
return befly.tool.Yes('成功');
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
---
|
|
746
|
+
|
|
747
|
+
## 禁用插件
|
|
748
|
+
|
|
749
|
+
在配置文件中设置 `disablePlugins` 数组:
|
|
750
|
+
|
|
751
|
+
```json
|
|
752
|
+
// befly.local.json
|
|
753
|
+
{
|
|
754
|
+
"disablePlugins": ["redis", "app_sms"]
|
|
755
|
+
}
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
被禁用的插件不会被加载和初始化。
|
|
759
|
+
|
|
760
|
+
---
|
|
761
|
+
|
|
762
|
+
## Addon 插件
|
|
763
|
+
|
|
764
|
+
Addon 插件是组件包中的扩展功能,用于为 Addon 提供特定的服务能力。
|
|
765
|
+
|
|
766
|
+
### Addon 插件示例
|
|
767
|
+
|
|
768
|
+
```typescript
|
|
769
|
+
// packages/addonPay/plugins/wechat.ts
|
|
770
|
+
import type { Plugin } from 'befly-core/types/plugin';
|
|
771
|
+
import type { BeflyContext } from 'befly-core/types/befly';
|
|
772
|
+
|
|
773
|
+
class WechatPayHelper {
|
|
774
|
+
private befly: BeflyContext;
|
|
775
|
+
private config: any;
|
|
776
|
+
|
|
777
|
+
constructor(befly: BeflyContext) {
|
|
778
|
+
this.befly = befly;
|
|
779
|
+
this.config = befly.config?.addons?.pay?.wechat || {};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async createOrder(params: { orderId: string; amount: number; description: string }) {
|
|
783
|
+
// 调用微信支付 API 创建订单
|
|
784
|
+
// ...
|
|
785
|
+
return { prepayId: 'xxx', nonceStr: 'xxx' };
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
async queryOrder(orderId: string) {
|
|
789
|
+
// 查询订单状态
|
|
790
|
+
// ...
|
|
791
|
+
return { status: 'SUCCESS' };
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
async refund(orderId: string, amount: number) {
|
|
795
|
+
// 申请退款
|
|
796
|
+
// ...
|
|
797
|
+
return { refundId: 'xxx' };
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const plugin: Plugin = {
|
|
802
|
+
after: ['logger', 'config'],
|
|
803
|
+
handler: (befly: BeflyContext) => {
|
|
804
|
+
return new WechatPayHelper(befly);
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
export default plugin;
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
**使用方式**:
|
|
812
|
+
|
|
813
|
+
```typescript
|
|
814
|
+
// 插件名:addon_addonPay_wechat
|
|
815
|
+
const order = await befly.addon_addonPay_wechat.createOrder({
|
|
816
|
+
orderId: '202312010001',
|
|
817
|
+
amount: 100,
|
|
818
|
+
description: '商品购买'
|
|
819
|
+
});
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
---
|
|
823
|
+
|
|
824
|
+
## 最佳实践
|
|
825
|
+
|
|
826
|
+
### 1. 使用 TypeScript 类封装
|
|
827
|
+
|
|
828
|
+
```typescript
|
|
829
|
+
// ✅ 推荐:使用 class 封装
|
|
830
|
+
class MyHelper {
|
|
831
|
+
private befly: BeflyContext;
|
|
832
|
+
|
|
833
|
+
constructor(befly: BeflyContext) {
|
|
834
|
+
this.befly = befly;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
async doSomething() {
|
|
838
|
+
/* ... */
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const plugin: Plugin = {
|
|
843
|
+
handler: (befly) => new MyHelper(befly)
|
|
844
|
+
};
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
### 2. 正确声明依赖
|
|
848
|
+
|
|
849
|
+
```typescript
|
|
850
|
+
// ✅ 推荐:明确声明所有依赖
|
|
851
|
+
const plugin: Plugin = {
|
|
852
|
+
after: ['logger', 'db', 'redis'], // 声明所有使用的插件
|
|
853
|
+
handler: (befly) => {
|
|
854
|
+
/* ... */
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
// ❌ 避免:使用未声明的依赖
|
|
859
|
+
const plugin: Plugin = {
|
|
860
|
+
handler: (befly) => {
|
|
861
|
+
befly.db.getOne({
|
|
862
|
+
/* ... */
|
|
863
|
+
}); // 可能 db 还未初始化!
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
### 3. 错误处理
|
|
869
|
+
|
|
870
|
+
```typescript
|
|
871
|
+
// ✅ 推荐:初始化时处理错误
|
|
872
|
+
const plugin: Plugin = {
|
|
873
|
+
async handler(befly) {
|
|
874
|
+
try {
|
|
875
|
+
const client = await connectToService();
|
|
876
|
+
return client;
|
|
877
|
+
} catch (error) {
|
|
878
|
+
befly.logger.error({ err: error }, '服务连接失败');
|
|
879
|
+
throw error; // 抛出错误会终止应用启动
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
### 4. 配置验证
|
|
886
|
+
|
|
887
|
+
```typescript
|
|
888
|
+
// ✅ 推荐:验证必要配置
|
|
889
|
+
const plugin: Plugin = {
|
|
890
|
+
handler: (befly) => {
|
|
891
|
+
const config = befly.config.myService;
|
|
892
|
+
|
|
893
|
+
if (!config?.apiKey) {
|
|
894
|
+
throw new Error('myService.apiKey 配置缺失');
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return new MyService(config);
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
### 5. 资源清理
|
|
903
|
+
|
|
904
|
+
```typescript
|
|
905
|
+
// ✅ 推荐:提供清理方法
|
|
906
|
+
class DatabasePool {
|
|
907
|
+
private pool: any;
|
|
908
|
+
|
|
909
|
+
async connect() {
|
|
910
|
+
/* ... */
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
async close() {
|
|
914
|
+
if (this.pool) {
|
|
915
|
+
await this.pool.end();
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
---
|
|
922
|
+
|
|
923
|
+
## 常见问题
|
|
924
|
+
|
|
925
|
+
### Q1: 插件加载顺序如何确定?
|
|
926
|
+
|
|
927
|
+
插件按以下顺序加载:
|
|
928
|
+
|
|
929
|
+
1. 核心插件 → Addon 插件 → 项目插件
|
|
930
|
+
2. 同一类型内根据 `after` 依赖关系排序
|
|
931
|
+
3. 无依赖的插件按文件名字母顺序
|
|
932
|
+
|
|
933
|
+
### Q2: 如何在插件中访问其他插件?
|
|
934
|
+
|
|
935
|
+
通过 `befly` 上下文访问:
|
|
936
|
+
|
|
937
|
+
```typescript
|
|
938
|
+
const plugin: Plugin = {
|
|
939
|
+
after: ['db'], // 声明依赖
|
|
940
|
+
handler: (befly) => {
|
|
941
|
+
return {
|
|
942
|
+
async getUser(id: number) {
|
|
943
|
+
return befly.db.getOne({ table: 'user', where: { id: id } });
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
### Q3: 插件初始化失败会怎样?
|
|
951
|
+
|
|
952
|
+
插件初始化失败(抛出错误)会导致应用启动终止,并输出错误日志。
|
|
953
|
+
|
|
954
|
+
### Q4: 可以动态加载插件吗?
|
|
955
|
+
|
|
956
|
+
目前不支持运行时动态加载插件,所有插件在应用启动时加载。
|
|
957
|
+
|
|
958
|
+
### Q5: 如何测试插件?
|
|
959
|
+
|
|
960
|
+
```typescript
|
|
961
|
+
// tests/myPlugin.test.ts
|
|
962
|
+
import { describe, test, expect } from 'bun:test';
|
|
963
|
+
|
|
964
|
+
describe('MyPlugin', () => {
|
|
965
|
+
test('应该正确初始化', async () => {
|
|
966
|
+
const mockBefly = {
|
|
967
|
+
config: { myService: { apiKey: 'test' } },
|
|
968
|
+
logger: { info: () => {}, error: () => {} }
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
const plugin = (await import('../plugins/myPlugin.ts')).default;
|
|
972
|
+
const instance = await plugin.handler(mockBefly);
|
|
973
|
+
|
|
974
|
+
expect(instance).toBeDefined();
|
|
975
|
+
expect(typeof instance.doSomething).toBe('function');
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
```
|