befly 3.9.12 → 3.9.14
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 +1604 -0
- package/docs/cipher.md +580 -0
- package/docs/config.md +638 -0
- package/docs/database.md +147 -3
- package/docs/examples.md +892 -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 +618 -0
- package/loader/loadApis.ts +33 -36
- 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/hook.md
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
# Hook 钩子开发指南
|
|
2
|
+
|
|
3
|
+
> 本文档详细介绍 Befly 框架的 Hook 钩子系统,包括钩子结构、执行顺序、内置钩子及自定义钩子开发。
|
|
4
|
+
|
|
5
|
+
## 目录
|
|
6
|
+
|
|
7
|
+
- [Hook 钩子开发指南](#hook-钩子开发指南)
|
|
8
|
+
- [目录](#目录)
|
|
9
|
+
- [概述](#概述)
|
|
10
|
+
- [核心特性](#核心特性)
|
|
11
|
+
- [与插件的区别](#与插件的区别)
|
|
12
|
+
- [Hook 结构](#hook-结构)
|
|
13
|
+
- [基础结构](#基础结构)
|
|
14
|
+
- [完整类型定义](#完整类型定义)
|
|
15
|
+
- [执行顺序](#执行顺序)
|
|
16
|
+
- [order 值规范](#order-值规范)
|
|
17
|
+
- [执行流程图](#执行流程图)
|
|
18
|
+
- [内置钩子](#内置钩子)
|
|
19
|
+
- [cors - 跨域处理](#cors---跨域处理)
|
|
20
|
+
- [auth - 身份认证](#auth---身份认证)
|
|
21
|
+
- [parser - 参数解析](#parser---参数解析)
|
|
22
|
+
- [requestLogger - 请求日志](#requestlogger---请求日志)
|
|
23
|
+
- [validator - 参数验证](#validator---参数验证)
|
|
24
|
+
- [permission - 权限检查](#permission---权限检查)
|
|
25
|
+
- [自定义钩子开发](#自定义钩子开发)
|
|
26
|
+
- [基础钩子](#基础钩子)
|
|
27
|
+
- [请求拦截钩子](#请求拦截钩子)
|
|
28
|
+
- [限流钩子](#限流钩子)
|
|
29
|
+
- [审计日志钩子](#审计日志钩子)
|
|
30
|
+
- [中断请求](#中断请求)
|
|
31
|
+
- [禁用钩子](#禁用钩子)
|
|
32
|
+
- [最佳实践](#最佳实践)
|
|
33
|
+
- [1. 合理设置 order](#1-合理设置-order)
|
|
34
|
+
- [2. 提前检查 ctx.api](#2-提前检查-ctxapi)
|
|
35
|
+
- [3. 错误处理](#3-错误处理)
|
|
36
|
+
- [4. 避免重复处理](#4-避免重复处理)
|
|
37
|
+
- [5. 性能考虑](#5-性能考虑)
|
|
38
|
+
- [常见问题](#常见问题)
|
|
39
|
+
- [Q1: 钩子执行顺序如何确定?](#q1-钩子执行顺序如何确定)
|
|
40
|
+
- [Q2: 钩子可以访问插件吗?](#q2-钩子可以访问插件吗)
|
|
41
|
+
- [Q3: 如何在钩子间传递数据?](#q3-如何在钩子间传递数据)
|
|
42
|
+
- [Q4: 钩子抛出异常会怎样?](#q4-钩子抛出异常会怎样)
|
|
43
|
+
- [Q5: 可以动态修改钩子吗?](#q5-可以动态修改钩子吗)
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 概述
|
|
48
|
+
|
|
49
|
+
Befly Hook 系统是请求处理的中间件机制,采用串联模式依次执行。每个 Hook 可以访问请求上下文、修改数据、或提前中断请求。
|
|
50
|
+
|
|
51
|
+
### 核心特性
|
|
52
|
+
|
|
53
|
+
- **串联执行**:按 order 顺序依次执行,无 next 调用
|
|
54
|
+
- **可中断**:设置 `ctx.response` 可提前中断后续处理
|
|
55
|
+
- **上下文共享**:所有 Hook 共享同一个 RequestContext
|
|
56
|
+
- **可禁用**:通过配置禁用指定钩子
|
|
57
|
+
|
|
58
|
+
### 与插件的区别
|
|
59
|
+
|
|
60
|
+
| 特性 | Hook 钩子 | Plugin 插件 |
|
|
61
|
+
| -------- | -------------- | -------------------- |
|
|
62
|
+
| 执行时机 | 每次请求时执行 | 应用启动时初始化一次 |
|
|
63
|
+
| 作用范围 | 请求级别 | 应用级别 |
|
|
64
|
+
| 访问对象 | befly + ctx | befly |
|
|
65
|
+
| 主要用途 | 请求处理中间件 | 功能模块封装 |
|
|
66
|
+
| 排序方式 | order 数值 | after 依赖关系 |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Hook 结构
|
|
71
|
+
|
|
72
|
+
### 基础结构
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import type { Hook } from 'befly-core/types/hook';
|
|
76
|
+
|
|
77
|
+
const hook: Hook = {
|
|
78
|
+
// 执行顺序(数字越小越先执行)
|
|
79
|
+
order: 10,
|
|
80
|
+
|
|
81
|
+
// 处理函数
|
|
82
|
+
handler: async (befly, ctx) => {
|
|
83
|
+
// 处理逻辑
|
|
84
|
+
// 可以访问 befly 全局对象和 ctx 请求上下文
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export default hook;
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 完整类型定义
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
/**
|
|
95
|
+
* 钩子处理函数类型
|
|
96
|
+
*/
|
|
97
|
+
type HookHandler = (befly: BeflyContext, ctx: RequestContext) => Promise<void> | void;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 钩子配置类型
|
|
101
|
+
*/
|
|
102
|
+
interface Hook {
|
|
103
|
+
/** 钩子名称(运行时自动生成,无需手动设置) */
|
|
104
|
+
name?: string;
|
|
105
|
+
|
|
106
|
+
/** 依赖的钩子列表(在这些钩子之后执行) */
|
|
107
|
+
after?: string[];
|
|
108
|
+
|
|
109
|
+
/** 执行顺序(数字越小越先执行) */
|
|
110
|
+
order?: number;
|
|
111
|
+
|
|
112
|
+
/** 钩子处理函数 */
|
|
113
|
+
handler: HookHandler;
|
|
114
|
+
|
|
115
|
+
/** 钩子配置(可选) */
|
|
116
|
+
config?: Record<string, any>;
|
|
117
|
+
|
|
118
|
+
/** 钩子描述(可选) */
|
|
119
|
+
description?: string;
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 执行顺序
|
|
126
|
+
|
|
127
|
+
### order 值规范
|
|
128
|
+
|
|
129
|
+
| order | 钩子名称 | 职责说明 |
|
|
130
|
+
| ----- | ------------- | ---------------- |
|
|
131
|
+
| 2 | cors | CORS 跨域处理 |
|
|
132
|
+
| 3 | auth | JWT 身份认证 |
|
|
133
|
+
| 4 | parser | 请求参数解析 |
|
|
134
|
+
| 5 | requestLogger | 请求日志记录 |
|
|
135
|
+
| 6 | validator | 参数验证 |
|
|
136
|
+
| 6 | permission | 权限检查 |
|
|
137
|
+
| 10-99 | 自定义钩子 | 业务逻辑钩子 |
|
|
138
|
+
| 100+ | 后置钩子 | 响应处理、清理等 |
|
|
139
|
+
|
|
140
|
+
**建议**:
|
|
141
|
+
|
|
142
|
+
- 1-9:框架核心钩子
|
|
143
|
+
- 10-99:业务逻辑钩子
|
|
144
|
+
- 100+:后置处理钩子
|
|
145
|
+
|
|
146
|
+
### 执行流程图
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
请求进入
|
|
150
|
+
↓
|
|
151
|
+
┌─────────────────────────────────────────────────┐
|
|
152
|
+
│ cors (order: 2) │
|
|
153
|
+
│ - 设置 CORS 响应头 │
|
|
154
|
+
│ - 处理 OPTIONS 预检请求 → 可能中断 │
|
|
155
|
+
├─────────────────────────────────────────────────┤
|
|
156
|
+
│ auth (order: 3) │
|
|
157
|
+
│ - 解析 Authorization Header │
|
|
158
|
+
│ - 验证 JWT Token │
|
|
159
|
+
│ - 设置 ctx.user │
|
|
160
|
+
├─────────────────────────────────────────────────┤
|
|
161
|
+
│ parser (order: 4) │
|
|
162
|
+
│ - GET: 解析 URL 查询参数 │
|
|
163
|
+
│ - POST: 解析 JSON/XML 请求体 │
|
|
164
|
+
│ - 根据 fields 过滤字段 │
|
|
165
|
+
│ - 设置 ctx.body → 可能中断(格式错误) │
|
|
166
|
+
├─────────────────────────────────────────────────┤
|
|
167
|
+
│ requestLogger (order: 5) │
|
|
168
|
+
│ - 记录请求日志 │
|
|
169
|
+
├─────────────────────────────────────────────────┤
|
|
170
|
+
│ validator (order: 6) │
|
|
171
|
+
│ - 验证必填字段 │
|
|
172
|
+
│ - 验证类型、长度、正则 → 可能中断 │
|
|
173
|
+
├─────────────────────────────────────────────────┤
|
|
174
|
+
│ permission (order: 6) │
|
|
175
|
+
│ - 检查 auth 配置 │
|
|
176
|
+
│ - 验证登录状态 │
|
|
177
|
+
│ - 检查角色权限 → 可能中断 │
|
|
178
|
+
├─────────────────────────────────────────────────┤
|
|
179
|
+
│ [自定义钩子 order: 10-99] │
|
|
180
|
+
│ - 限流、审计、数据预处理等 │
|
|
181
|
+
├─────────────────────────────────────────────────┤
|
|
182
|
+
│ API Handler │
|
|
183
|
+
│ - 执行业务逻辑 │
|
|
184
|
+
│ - 返回响应结果 │
|
|
185
|
+
├─────────────────────────────────────────────────┤
|
|
186
|
+
│ FinalResponse │
|
|
187
|
+
│ - 格式化响应 │
|
|
188
|
+
│ - 记录请求日志 │
|
|
189
|
+
└─────────────────────────────────────────────────┘
|
|
190
|
+
↓
|
|
191
|
+
响应返回
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 内置钩子
|
|
197
|
+
|
|
198
|
+
### cors - 跨域处理
|
|
199
|
+
|
|
200
|
+
处理 CORS 跨域请求,设置响应头。
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
// hooks/cors.ts
|
|
204
|
+
const hook: Hook = {
|
|
205
|
+
order: 2,
|
|
206
|
+
handler: async (befly, ctx) => {
|
|
207
|
+
const req = ctx.req;
|
|
208
|
+
|
|
209
|
+
// 合并默认配置和用户配置
|
|
210
|
+
const defaultConfig: CorsConfig = {
|
|
211
|
+
origin: '*',
|
|
212
|
+
methods: 'GET, POST, PUT, DELETE, OPTIONS',
|
|
213
|
+
allowedHeaders: 'Content-Type, Authorization, authorization, token',
|
|
214
|
+
exposedHeaders: 'Content-Range, X-Content-Range, Authorization, authorization, token',
|
|
215
|
+
maxAge: 86400,
|
|
216
|
+
credentials: 'true'
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const corsConfig = { ...defaultConfig, ...(beflyConfig.cors || {}) };
|
|
220
|
+
|
|
221
|
+
// 设置 CORS 响应头
|
|
222
|
+
ctx.corsHeaders = setCorsOptions(req, corsConfig);
|
|
223
|
+
|
|
224
|
+
// 处理 OPTIONS 预检请求
|
|
225
|
+
if (req.method === 'OPTIONS') {
|
|
226
|
+
ctx.response = new Response(null, {
|
|
227
|
+
status: 204,
|
|
228
|
+
headers: ctx.corsHeaders
|
|
229
|
+
});
|
|
230
|
+
return; // 中断后续处理
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**配置**:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
// befly.local.json
|
|
240
|
+
{
|
|
241
|
+
"cors": {
|
|
242
|
+
"origin": "https://example.com",
|
|
243
|
+
"methods": "GET, POST",
|
|
244
|
+
"allowedHeaders": "Content-Type, Authorization",
|
|
245
|
+
"maxAge": 3600,
|
|
246
|
+
"credentials": "true"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
### auth - 身份认证
|
|
254
|
+
|
|
255
|
+
解析 JWT Token,设置用户信息。
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// hooks/auth.ts
|
|
259
|
+
const hook: Hook = {
|
|
260
|
+
order: 3,
|
|
261
|
+
handler: async (befly, ctx) => {
|
|
262
|
+
const authHeader = ctx.req.headers.get('authorization');
|
|
263
|
+
|
|
264
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
265
|
+
const token = authHeader.substring(7);
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const payload = await befly.jwt.verify(token);
|
|
269
|
+
ctx.user = payload;
|
|
270
|
+
} catch (error: any) {
|
|
271
|
+
ctx.user = {};
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
ctx.user = {};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**特点**:
|
|
281
|
+
|
|
282
|
+
- 不中断请求,仅设置 `ctx.user`
|
|
283
|
+
- Token 无效时设置空对象 `{}`
|
|
284
|
+
- 权限检查由 permission 钩子负责
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
### parser - 参数解析
|
|
289
|
+
|
|
290
|
+
解析请求参数,根据 API 字段定义过滤。
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
// hooks/parser.ts
|
|
294
|
+
const hook: Hook = {
|
|
295
|
+
order: 4,
|
|
296
|
+
handler: async (befly, ctx) => {
|
|
297
|
+
if (!ctx.api) return;
|
|
298
|
+
|
|
299
|
+
// GET 请求:解析查询参数
|
|
300
|
+
if (ctx.req.method === 'GET') {
|
|
301
|
+
const url = new URL(ctx.req.url);
|
|
302
|
+
const params = Object.fromEntries(url.searchParams);
|
|
303
|
+
|
|
304
|
+
if (ctx.api.rawBody) {
|
|
305
|
+
ctx.body = params;
|
|
306
|
+
} else if (isPlainObject(ctx.api.fields) && !isEmpty(ctx.api.fields)) {
|
|
307
|
+
ctx.body = pickFields(params, Object.keys(ctx.api.fields));
|
|
308
|
+
} else {
|
|
309
|
+
ctx.body = params;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// POST 请求:解析 JSON/XML
|
|
313
|
+
else if (ctx.req.method === 'POST') {
|
|
314
|
+
const contentType = ctx.req.headers.get('content-type') || '';
|
|
315
|
+
|
|
316
|
+
if (contentType.includes('application/json')) {
|
|
317
|
+
const body = await ctx.req.json();
|
|
318
|
+
// 过滤字段...
|
|
319
|
+
ctx.body = pickFields(body, Object.keys(ctx.api.fields));
|
|
320
|
+
} else if (contentType.includes('application/xml')) {
|
|
321
|
+
// XML 解析...
|
|
322
|
+
} else {
|
|
323
|
+
ctx.response = ErrorResponse(ctx, '无效的请求参数格式');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**支持格式**:
|
|
332
|
+
|
|
333
|
+
- `application/json`
|
|
334
|
+
- `application/xml` / `text/xml`
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
### requestLogger - 请求日志
|
|
339
|
+
|
|
340
|
+
在参数解析后记录请求日志。
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
// hooks/requestLogger.ts
|
|
344
|
+
const hook: Hook = {
|
|
345
|
+
order: 5,
|
|
346
|
+
handler: async (befly, ctx) => {
|
|
347
|
+
if (!ctx.api) return;
|
|
348
|
+
|
|
349
|
+
const logData = {
|
|
350
|
+
requestId: ctx.requestId,
|
|
351
|
+
route: ctx.route,
|
|
352
|
+
ip: ctx.ip,
|
|
353
|
+
userId: ctx.user?.id || '',
|
|
354
|
+
nickname: ctx.user?.nickname || '',
|
|
355
|
+
roleCode: ctx.user?.roleCode || ''
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// 截断大请求体
|
|
359
|
+
if (ctx.body && Object.keys(ctx.body).length > 0) {
|
|
360
|
+
logData.body = truncateBody(ctx.body);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
Logger.info(logData, '请求日志');
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**特点**:
|
|
369
|
+
|
|
370
|
+
- 自动截断超长字段(500 字符)
|
|
371
|
+
- 不记录非 API 请求
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
### validator - 参数验证
|
|
376
|
+
|
|
377
|
+
验证请求参数类型、必填、长度等。
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
// hooks/validator.ts
|
|
381
|
+
const hook: Hook = {
|
|
382
|
+
order: 6,
|
|
383
|
+
handler: async (befly, ctx) => {
|
|
384
|
+
if (!ctx.api) return;
|
|
385
|
+
if (!ctx.api.fields) return;
|
|
386
|
+
|
|
387
|
+
// 验证参数
|
|
388
|
+
const result = Validator.validate(ctx.body, ctx.api.fields, ctx.api.required || []);
|
|
389
|
+
|
|
390
|
+
if (result.code !== 0) {
|
|
391
|
+
ctx.response = ErrorResponse(ctx, result.firstError || '参数验证失败', 1, null, result.fieldErrors);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
**验证内容**:
|
|
399
|
+
|
|
400
|
+
- 必填字段检查
|
|
401
|
+
- 类型验证(string/number/text 等)
|
|
402
|
+
- 长度/范围验证(min/max)
|
|
403
|
+
- 正则验证(regex)
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
### permission - 权限检查
|
|
408
|
+
|
|
409
|
+
检查用户权限,验证 API 访问授权。
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
// hooks/permission.ts
|
|
413
|
+
const hook: Hook = {
|
|
414
|
+
order: 6,
|
|
415
|
+
handler: async (befly, ctx) => {
|
|
416
|
+
if (!ctx.api) return;
|
|
417
|
+
|
|
418
|
+
// 1. 接口无需权限
|
|
419
|
+
if (ctx.api.auth === false) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// 2. 用户未登录
|
|
424
|
+
if (!ctx.user || !ctx.user.id) {
|
|
425
|
+
ctx.response = ErrorResponse(ctx, '未登录');
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// 3. 开发者权限(最高权限)
|
|
430
|
+
if (ctx.user.roleCode === 'dev') {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 4. 角色权限检查
|
|
435
|
+
let hasPermission = false;
|
|
436
|
+
if (ctx.user.roleCode && befly.redis) {
|
|
437
|
+
const apiPath = `${ctx.req.method}${new URL(ctx.req.url).pathname}`;
|
|
438
|
+
const roleApisKey = RedisKeys.roleApis(ctx.user.roleCode);
|
|
439
|
+
hasPermission = await befly.redis.sismember(roleApisKey, apiPath);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (!hasPermission) {
|
|
443
|
+
ctx.response = ErrorResponse(ctx, '无权访问');
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**权限层级**:
|
|
451
|
+
|
|
452
|
+
1. `auth: false` - 公开接口,无需检查
|
|
453
|
+
2. 未登录 - 返回"未登录"
|
|
454
|
+
3. `roleCode: 'dev'` - 开发者,最高权限
|
|
455
|
+
4. Redis 权限集合 - 检查角色是否有 API 权限
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## 自定义钩子开发
|
|
460
|
+
|
|
461
|
+
### 基础钩子
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
// hooks/myHook.ts(项目钩子名:app_myHook)
|
|
465
|
+
import type { Hook } from 'befly-core/types/hook';
|
|
466
|
+
|
|
467
|
+
const hook: Hook = {
|
|
468
|
+
order: 10,
|
|
469
|
+
handler: async (befly, ctx) => {
|
|
470
|
+
befly.logger.info({ route: ctx.route }, '自定义钩子执行');
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
export default hook;
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
### 请求拦截钩子
|
|
480
|
+
|
|
481
|
+
拦截特定请求:
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
// hooks/blacklist.ts
|
|
485
|
+
import type { Hook } from 'befly-core/types/hook';
|
|
486
|
+
import { ErrorResponse } from 'befly-core/util';
|
|
487
|
+
|
|
488
|
+
const hook: Hook = {
|
|
489
|
+
order: 1, // 最先执行
|
|
490
|
+
handler: async (befly, ctx) => {
|
|
491
|
+
// IP 黑名单检查
|
|
492
|
+
const blacklist = ['192.168.1.100', '10.0.0.1'];
|
|
493
|
+
|
|
494
|
+
if (blacklist.includes(ctx.ip)) {
|
|
495
|
+
ctx.response = ErrorResponse(ctx, '您的 IP 已被禁止访问', 403);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
export default hook;
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
### 限流钩子
|
|
507
|
+
|
|
508
|
+
基于 Redis 的请求限流:
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
// hooks/rateLimit.ts
|
|
512
|
+
import type { Hook } from 'befly-core/types/hook';
|
|
513
|
+
import { ErrorResponse } from 'befly-core/util';
|
|
514
|
+
|
|
515
|
+
const hook: Hook = {
|
|
516
|
+
order: 7,
|
|
517
|
+
handler: async (befly, ctx) => {
|
|
518
|
+
if (!ctx.api || !befly.redis) return;
|
|
519
|
+
|
|
520
|
+
// 限流配置:10 次/60 秒
|
|
521
|
+
const limit = 10;
|
|
522
|
+
const window = 60;
|
|
523
|
+
|
|
524
|
+
// 限流 key:IP + 路由
|
|
525
|
+
const key = `ratelimit:${ctx.ip}:${ctx.route}`;
|
|
526
|
+
|
|
527
|
+
// 获取当前计数
|
|
528
|
+
const count = await befly.redis.incr(key);
|
|
529
|
+
|
|
530
|
+
// 首次请求设置过期时间
|
|
531
|
+
if (count === 1) {
|
|
532
|
+
await befly.redis.expire(key, window);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// 超过限制
|
|
536
|
+
if (count > limit) {
|
|
537
|
+
ctx.response = ErrorResponse(ctx, '请求过于频繁,请稍后再试', 429);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
export default hook;
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
### 审计日志钩子
|
|
549
|
+
|
|
550
|
+
记录操作审计日志:
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
// hooks/audit.ts
|
|
554
|
+
import type { Hook } from 'befly-core/types/hook';
|
|
555
|
+
|
|
556
|
+
const hook: Hook = {
|
|
557
|
+
order: 100, // 在 handler 执行后
|
|
558
|
+
handler: async (befly, ctx) => {
|
|
559
|
+
// 只记录写操作
|
|
560
|
+
if (!ctx.api || ctx.req.method === 'GET') return;
|
|
561
|
+
if (!ctx.user?.id) return;
|
|
562
|
+
|
|
563
|
+
// 记录审计日志
|
|
564
|
+
try {
|
|
565
|
+
await befly.db.insData({
|
|
566
|
+
table: 'audit_log',
|
|
567
|
+
data: {
|
|
568
|
+
userId: ctx.user.id,
|
|
569
|
+
username: ctx.user.username || '',
|
|
570
|
+
route: ctx.route,
|
|
571
|
+
method: ctx.req.method,
|
|
572
|
+
ip: ctx.ip,
|
|
573
|
+
requestBody: JSON.stringify(ctx.body).substring(0, 1000),
|
|
574
|
+
operateTime: Date.now()
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
} catch (error) {
|
|
578
|
+
befly.logger.error({ err: error }, '审计日志记录失败');
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
export default hook;
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
## 中断请求
|
|
589
|
+
|
|
590
|
+
设置 `ctx.response` 可以中断后续 Hook 和 API Handler 的执行:
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
import { ErrorResponse } from 'befly-core/util';
|
|
594
|
+
|
|
595
|
+
const hook: Hook = {
|
|
596
|
+
order: 5,
|
|
597
|
+
handler: async (befly, ctx) => {
|
|
598
|
+
// 条件判断
|
|
599
|
+
if (someCondition) {
|
|
600
|
+
// 设置 response 中断请求
|
|
601
|
+
ctx.response = ErrorResponse(ctx, '请求被拦截', 1);
|
|
602
|
+
return; // 必须 return
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// 继续执行后续钩子...
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
**ErrorResponse 函数**:
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
ErrorResponse(ctx, msg, code?, data?, detail?)
|
|
614
|
+
// ctx: RequestContext - 请求上下文
|
|
615
|
+
// msg: string - 错误消息
|
|
616
|
+
// code: number - 错误码(默认 1)
|
|
617
|
+
// data: any - 附加数据(默认 null)
|
|
618
|
+
// detail: any - 详细信息(默认 null)
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
## 禁用钩子
|
|
624
|
+
|
|
625
|
+
在配置文件中设置 `disableHooks` 数组:
|
|
626
|
+
|
|
627
|
+
```json
|
|
628
|
+
// befly.local.json
|
|
629
|
+
{
|
|
630
|
+
"disableHooks": ["requestLogger", "permission"]
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
被禁用的钩子不会被加载和执行。
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
## 最佳实践
|
|
639
|
+
|
|
640
|
+
### 1. 合理设置 order
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
// ✅ 推荐:使用适当的 order 值
|
|
644
|
+
const hook: Hook = {
|
|
645
|
+
order: 15, // 在核心钩子之后
|
|
646
|
+
handler: async (befly, ctx) => {
|
|
647
|
+
/* ... */
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
// ❌ 避免:使用过小的 order 值
|
|
652
|
+
const hook: Hook = {
|
|
653
|
+
order: 1, // 可能影响核心钩子
|
|
654
|
+
handler: async (befly, ctx) => {
|
|
655
|
+
/* ... */
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### 2. 提前检查 ctx.api
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
// ✅ 推荐:检查 ctx.api 是否存在
|
|
664
|
+
const hook: Hook = {
|
|
665
|
+
handler: async (befly, ctx) => {
|
|
666
|
+
if (!ctx.api) return; // 非 API 请求直接跳过
|
|
667
|
+
// ...
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
### 3. 错误处理
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
// ✅ 推荐:捕获异常避免影响请求
|
|
676
|
+
const hook: Hook = {
|
|
677
|
+
handler: async (befly, ctx) => {
|
|
678
|
+
try {
|
|
679
|
+
await someOperation();
|
|
680
|
+
} catch (error) {
|
|
681
|
+
befly.logger.error({ err: error }, '钩子执行失败');
|
|
682
|
+
// 根据业务决定是否中断请求
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
### 4. 避免重复处理
|
|
689
|
+
|
|
690
|
+
```typescript
|
|
691
|
+
// ✅ 推荐:检查是否已处理
|
|
692
|
+
const hook: Hook = {
|
|
693
|
+
handler: async (befly, ctx) => {
|
|
694
|
+
if (ctx.response) return; // 已有响应,跳过
|
|
695
|
+
// ...
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### 5. 性能考虑
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
// ✅ 推荐:异步操作使用 Promise
|
|
704
|
+
const hook: Hook = {
|
|
705
|
+
handler: async (befly, ctx) => {
|
|
706
|
+
// 可以并行的操作
|
|
707
|
+
const [result1, result2] = await Promise.all([operation1(), operation2()]);
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
---
|
|
713
|
+
|
|
714
|
+
## 常见问题
|
|
715
|
+
|
|
716
|
+
### Q1: 钩子执行顺序如何确定?
|
|
717
|
+
|
|
718
|
+
按 `order` 值从小到大排序执行,相同 `order` 值按文件名字母顺序。
|
|
719
|
+
|
|
720
|
+
### Q2: 钩子可以访问插件吗?
|
|
721
|
+
|
|
722
|
+
可以,通过 `befly` 参数访问所有插件:
|
|
723
|
+
|
|
724
|
+
```typescript
|
|
725
|
+
const hook: Hook = {
|
|
726
|
+
handler: async (befly, ctx) => {
|
|
727
|
+
await befly.db.getOne({
|
|
728
|
+
/* ... */
|
|
729
|
+
});
|
|
730
|
+
await befly.redis.get('key');
|
|
731
|
+
befly.logger.info('日志');
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
### Q3: 如何在钩子间传递数据?
|
|
737
|
+
|
|
738
|
+
通过 `ctx` 上下文传递:
|
|
739
|
+
|
|
740
|
+
```typescript
|
|
741
|
+
// 钩子 A(order: 10)
|
|
742
|
+
ctx.customData = { key: 'value' };
|
|
743
|
+
|
|
744
|
+
// 钩子 B(order: 20)
|
|
745
|
+
const data = ctx.customData;
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
### Q4: 钩子抛出异常会怎样?
|
|
749
|
+
|
|
750
|
+
未捕获的异常会被全局错误处理捕获,返回 500 错误响应。建议在钩子内部处理异常。
|
|
751
|
+
|
|
752
|
+
### Q5: 可以动态修改钩子吗?
|
|
753
|
+
|
|
754
|
+
不支持运行时动态修改,所有钩子在应用启动时加载。
|