wok-server 0.1.7 → 0.1.9
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/cache/cache.js +6 -6
- package/dist/cache/index.js +4 -4
- package/dist/cache/purge-task.js +3 -13
- package/dist/log/config.js +21 -20
- package/dist/log/file.js +4 -4
- package/dist/log/index.js +27 -31
- package/dist/log/level.js +6 -12
- package/dist/mvc/index.js +6 -1
- package/dist/task/task.js +4 -1
- package/documentation/zh-cn/mvc.md +154 -6
- package/package.json +1 -1
- package/types/cache/cache.d.ts +2 -2
- package/types/cache/purge-task.d.ts +1 -1
- package/types/log/config.d.ts +7 -7
- package/types/log/level.d.ts +4 -5
package/dist/cache/cache.js
CHANGED
|
@@ -34,15 +34,15 @@ class Cache {
|
|
|
34
34
|
get(key) {
|
|
35
35
|
const content = this.valueMap.get(key);
|
|
36
36
|
if (!content) {
|
|
37
|
-
this.stat
|
|
37
|
+
this.stat?.addGet(false);
|
|
38
38
|
return undefined;
|
|
39
39
|
}
|
|
40
40
|
if (content.expireAt < new Date().getTime()) {
|
|
41
|
-
this.stat
|
|
41
|
+
this.stat?.addGet(false);
|
|
42
42
|
this.valueMap.delete(key);
|
|
43
43
|
return undefined;
|
|
44
44
|
}
|
|
45
|
-
this.stat
|
|
45
|
+
this.stat?.addGet(true);
|
|
46
46
|
return content.val;
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
@@ -50,7 +50,7 @@ class Cache {
|
|
|
50
50
|
*/
|
|
51
51
|
clear() {
|
|
52
52
|
this.valueMap.clear();
|
|
53
|
-
this.stat
|
|
53
|
+
this.stat?.clear();
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* 删除.
|
|
@@ -68,10 +68,10 @@ class Cache {
|
|
|
68
68
|
async computeIfAbsent(key, provider, expiresInSeconds) {
|
|
69
69
|
const content = this.valueMap.get(key);
|
|
70
70
|
if (content && content.expireAt >= new Date().getTime()) {
|
|
71
|
-
this.stat
|
|
71
|
+
this.stat?.addGet(true);
|
|
72
72
|
return content.val;
|
|
73
73
|
}
|
|
74
|
-
this.stat
|
|
74
|
+
this.stat?.addGet(false);
|
|
75
75
|
// 如果已经在处理中,则直接返回 promise
|
|
76
76
|
const ep = this.promiseMap.get(key);
|
|
77
77
|
if (ep) {
|
package/dist/cache/index.js
CHANGED
|
@@ -7,17 +7,17 @@ const config_1 = require("./config");
|
|
|
7
7
|
const purge_task_1 = require("./purge-task");
|
|
8
8
|
const stat_1 = require("./stat");
|
|
9
9
|
const valueMap = new Map();
|
|
10
|
-
const stat = new stat_1.CacheStat(valueMap);
|
|
10
|
+
const stat = config_1.config.statTaskEnabled ? new stat_1.CacheStat(valueMap) : undefined;
|
|
11
11
|
const cache = new cache_1.Cache(valueMap, stat);
|
|
12
12
|
// 清理任务
|
|
13
13
|
(0, task_1.scheduleWithFixedDelay)(config_1.config.cleaningInterval, config_1.config.cleaningInterval, new purge_task_1.PurgeTask(valueMap));
|
|
14
14
|
// 统计任务
|
|
15
15
|
if (config_1.config.statTaskEnabled) {
|
|
16
16
|
(0, task_1.scheduleWithFixedDelay)(config_1.config.statInterval, config_1.config.statInterval, {
|
|
17
|
-
name: '
|
|
17
|
+
name: 'Cache statistics',
|
|
18
18
|
async run() {
|
|
19
|
-
stat
|
|
20
|
-
stat
|
|
19
|
+
stat?.log();
|
|
20
|
+
stat?.clear();
|
|
21
21
|
}
|
|
22
22
|
});
|
|
23
23
|
}
|
package/dist/cache/purge-task.js
CHANGED
|
@@ -8,7 +8,7 @@ const config_1 = require("./config");
|
|
|
8
8
|
*/
|
|
9
9
|
class PurgeTask {
|
|
10
10
|
valueMap;
|
|
11
|
-
name = '
|
|
11
|
+
name = 'Cache purge';
|
|
12
12
|
constructor(valueMap) {
|
|
13
13
|
this.valueMap = valueMap;
|
|
14
14
|
}
|
|
@@ -22,12 +22,7 @@ class PurgeTask {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
if (removeCount > 0) {
|
|
25
|
-
|
|
26
|
-
(0, log_1.getLogger)().info(`A total of ${removeCount} expired cache records have been cleared.`);
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
(0, log_1.getLogger)().info(`A total of ${removeCount} expired cache record has been cleared.`);
|
|
30
|
-
}
|
|
25
|
+
(0, log_1.getLogger)().info(`A total of ${removeCount} expired cache ${removeCount > 1 ? 'records' : 'record'} have been cleared.`);
|
|
31
26
|
}
|
|
32
27
|
// 如果过期的都清理掉,但是元素总数仍然超出,则删除多余元素,删除是从头开始删除的
|
|
33
28
|
// 看上去是按put顺序清理最早的,实际上随机的,重复 set 相同的 key,key 的位置是不会变的,不保证 key 的顺序
|
|
@@ -44,12 +39,7 @@ class PurgeTask {
|
|
|
44
39
|
break;
|
|
45
40
|
}
|
|
46
41
|
}
|
|
47
|
-
|
|
48
|
-
(0, log_1.getLogger)().info(`A total of ${evictedCount} cache records have been evicted.`);
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
(0, log_1.getLogger)().info(`A total of ${evictedCount} cache record has been evicted.`);
|
|
52
|
-
}
|
|
42
|
+
(0, log_1.getLogger)().info(`A total of ${evictedCount} cache ${evictedCount > 1 ? 'records' : 'record'} have been evicted.`);
|
|
53
43
|
}
|
|
54
44
|
}
|
|
55
45
|
}
|
package/dist/log/config.js
CHANGED
|
@@ -2,27 +2,28 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.config = void 0;
|
|
4
4
|
const path_1 = require("path");
|
|
5
|
+
const config_1 = require("../config");
|
|
6
|
+
const validation_1 = require("../validation");
|
|
5
7
|
const level_1 = require("./level");
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* 默认配置信息.
|
|
10
|
-
*/
|
|
11
|
-
const config = {
|
|
12
|
-
file: process.env.LOG_FILE === 'true',
|
|
13
|
-
fileDir: process.env.LOG_FILE_DIR || 'logs',
|
|
8
|
+
const envConfig = (0, config_1.registerConfig)({
|
|
9
|
+
file: false,
|
|
10
|
+
fileDir: 'logs',
|
|
14
11
|
fileMaxDays: 30,
|
|
15
|
-
level:
|
|
16
|
-
}
|
|
17
|
-
|
|
12
|
+
level: 'INFO'
|
|
13
|
+
}, 'LOG', {
|
|
14
|
+
file: [(0, validation_1.notNull)()],
|
|
15
|
+
fileDir: [(0, validation_1.notBlank)()],
|
|
16
|
+
fileMaxDays: [(0, validation_1.min)(1)],
|
|
17
|
+
level: [(0, validation_1.notBlank)()]
|
|
18
|
+
});
|
|
19
|
+
let { fileDir, level } = envConfig;
|
|
18
20
|
// 目录处理
|
|
19
|
-
if (!(0, path_1.isAbsolute)(
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
// 天数
|
|
23
|
-
if (process.env.LOG_FILE_MAX_DAYS) {
|
|
24
|
-
const days = parseInt(process.env.LOG_FILE_MAX_DAYS);
|
|
25
|
-
if (!isNaN(days)) {
|
|
26
|
-
config.fileMaxDays = days;
|
|
27
|
-
}
|
|
21
|
+
if (!(0, path_1.isAbsolute)(fileDir)) {
|
|
22
|
+
fileDir = (0, path_1.resolve)(process.cwd(), fileDir);
|
|
28
23
|
}
|
|
24
|
+
exports.config = Object.freeze({
|
|
25
|
+
file: envConfig.file,
|
|
26
|
+
fileDir,
|
|
27
|
+
fileMaxDays: envConfig.fileMaxDays,
|
|
28
|
+
level: (0, level_1.parseLogLevel)(level)
|
|
29
|
+
});
|
package/dist/log/file.js
CHANGED
|
@@ -53,24 +53,24 @@ if (config_1.config.file) {
|
|
|
53
53
|
* 清理任务.
|
|
54
54
|
*/
|
|
55
55
|
(0, task_1.scheduleDailyTask)(3, 0, {
|
|
56
|
-
name: '
|
|
56
|
+
name: 'Log files clear',
|
|
57
57
|
async run() {
|
|
58
58
|
const files = await (0, promises_1.readdir)(config_1.config.fileDir);
|
|
59
59
|
const now = new Date().getTime();
|
|
60
60
|
for (const file of files) {
|
|
61
61
|
const dotIdx = file.indexOf('.');
|
|
62
62
|
if (dotIdx === -1) {
|
|
63
|
-
console.warn(
|
|
63
|
+
console.warn(`Unable to process the log file: ${file}`);
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
const dateStr = file.substring(0, dotIdx);
|
|
67
67
|
const timestamp = Date.parse(dateStr);
|
|
68
68
|
if (isNaN(timestamp)) {
|
|
69
|
-
console.warn(
|
|
69
|
+
console.warn(`Unable to process the log file: ${file}`);
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
72
|
if (timestamp + config_1.config.fileMaxDays * 24 * 3600 * 1000 < now) {
|
|
73
|
-
console.warn(
|
|
73
|
+
console.warn(`Remove log file: ${file}`);
|
|
74
74
|
await (0, promises_1.rm)((0, path_1.resolve)(config_1.config.fileDir, file));
|
|
75
75
|
}
|
|
76
76
|
}
|
package/dist/log/index.js
CHANGED
|
@@ -5,13 +5,9 @@ const tslib_1 = require("tslib");
|
|
|
5
5
|
const os_1 = require("os");
|
|
6
6
|
const config_1 = require("./config");
|
|
7
7
|
const date_1 = require("./date");
|
|
8
|
+
const file_1 = require("./file");
|
|
8
9
|
const level_1 = require("./level");
|
|
9
10
|
const store_1 = require("./store");
|
|
10
|
-
const file_1 = require("./file");
|
|
11
|
-
/**
|
|
12
|
-
* 日志级别的值
|
|
13
|
-
*/
|
|
14
|
-
const LOG_LEVEL_VAL = level_1.LogLevelVals[config_1.config.level];
|
|
15
11
|
/**
|
|
16
12
|
* 文件存储
|
|
17
13
|
*/
|
|
@@ -25,31 +21,31 @@ if (config_1.config.file) {
|
|
|
25
21
|
* @param error
|
|
26
22
|
*/
|
|
27
23
|
function log(level, message, error) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
if (level < config_1.config.level) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const date = new Date();
|
|
28
|
+
// 控制台输出日志
|
|
29
|
+
let msg = `[${(0, date_1.formatDateTime)(date)}][${level_1.LogLevel[level]}]${message}`;
|
|
30
|
+
console.log(msg);
|
|
31
|
+
if (error) {
|
|
32
|
+
console.log(error);
|
|
33
|
+
}
|
|
34
|
+
// 存储中输出日志
|
|
35
|
+
const store = (0, store_1.getLogStore)();
|
|
36
|
+
if (store) {
|
|
34
37
|
if (error) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
else if (error.message) {
|
|
45
|
-
msg += os_1.EOL + error.message;
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
msg += os_1.EOL + error;
|
|
49
|
-
}
|
|
38
|
+
if (error.stack) {
|
|
39
|
+
msg += os_1.EOL + error.stack;
|
|
40
|
+
}
|
|
41
|
+
else if (error.message) {
|
|
42
|
+
msg += os_1.EOL + error.message;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
msg += os_1.EOL + error;
|
|
50
46
|
}
|
|
51
|
-
store(msg);
|
|
52
47
|
}
|
|
48
|
+
store(msg);
|
|
53
49
|
}
|
|
54
50
|
}
|
|
55
51
|
const logger = Object.freeze({
|
|
@@ -60,7 +56,7 @@ const logger = Object.freeze({
|
|
|
60
56
|
log(level_1.LogLevel.DEBUG, message);
|
|
61
57
|
},
|
|
62
58
|
isDebugEnabled() {
|
|
63
|
-
return level_1.
|
|
59
|
+
return level_1.LogLevel.DEBUG >= config_1.config.level;
|
|
64
60
|
},
|
|
65
61
|
/**
|
|
66
62
|
* info 日志
|
|
@@ -70,7 +66,7 @@ const logger = Object.freeze({
|
|
|
70
66
|
log(level_1.LogLevel.INFO, message);
|
|
71
67
|
},
|
|
72
68
|
isInfoEnabled() {
|
|
73
|
-
return level_1.
|
|
69
|
+
return level_1.LogLevel.INFO >= config_1.config.level;
|
|
74
70
|
},
|
|
75
71
|
/**
|
|
76
72
|
* 警告日志
|
|
@@ -81,7 +77,7 @@ const logger = Object.freeze({
|
|
|
81
77
|
log(level_1.LogLevel.WARN, message, error);
|
|
82
78
|
},
|
|
83
79
|
isWarnEnabled() {
|
|
84
|
-
return level_1.
|
|
80
|
+
return level_1.LogLevel.WARN >= config_1.config.level;
|
|
85
81
|
},
|
|
86
82
|
/**
|
|
87
83
|
* 错误日志
|
|
@@ -92,7 +88,7 @@ const logger = Object.freeze({
|
|
|
92
88
|
log(level_1.LogLevel.ERROR, message, error);
|
|
93
89
|
},
|
|
94
90
|
isErrorEnabled() {
|
|
95
|
-
return level_1.
|
|
91
|
+
return level_1.LogLevel.ERROR >= config_1.config.level;
|
|
96
92
|
}
|
|
97
93
|
});
|
|
98
94
|
/**
|
package/dist/log/level.js
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseLogLevel = exports.
|
|
3
|
+
exports.parseLogLevel = exports.LogLevel = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* 日志级别.
|
|
6
6
|
*/
|
|
7
7
|
var LogLevel;
|
|
8
8
|
(function (LogLevel) {
|
|
9
|
-
LogLevel["DEBUG"] = "DEBUG";
|
|
10
|
-
LogLevel["INFO"] = "INFO";
|
|
11
|
-
LogLevel["WARN"] = "WARN";
|
|
12
|
-
LogLevel["ERROR"] = "ERROR";
|
|
9
|
+
LogLevel[LogLevel["DEBUG"] = 1] = "DEBUG";
|
|
10
|
+
LogLevel[LogLevel["INFO"] = 2] = "INFO";
|
|
11
|
+
LogLevel[LogLevel["WARN"] = 3] = "WARN";
|
|
12
|
+
LogLevel[LogLevel["ERROR"] = 4] = "ERROR";
|
|
13
13
|
})(LogLevel = exports.LogLevel || (exports.LogLevel = {}));
|
|
14
|
-
exports.LogLevelVals = Object.freeze({
|
|
15
|
-
[LogLevel.DEBUG]: 1,
|
|
16
|
-
[LogLevel.INFO]: 2,
|
|
17
|
-
[LogLevel.WARN]: 3,
|
|
18
|
-
[LogLevel.ERROR]: 4
|
|
19
|
-
});
|
|
20
14
|
/**
|
|
21
15
|
* 解析日志级别
|
|
22
16
|
* @param level
|
|
@@ -33,7 +27,7 @@ function parseLogLevel(level) {
|
|
|
33
27
|
case 'ERROR':
|
|
34
28
|
return LogLevel.ERROR;
|
|
35
29
|
default:
|
|
36
|
-
throw new Error(
|
|
30
|
+
throw new Error(`Unknown log level :${level}`);
|
|
37
31
|
}
|
|
38
32
|
}
|
|
39
33
|
exports.parseLogLevel = parseLogLevel;
|
package/dist/mvc/index.js
CHANGED
|
@@ -278,7 +278,12 @@ async function startWebServer(opts) {
|
|
|
278
278
|
});
|
|
279
279
|
console.log('App running at: ');
|
|
280
280
|
getIpv4List().forEach(ip => {
|
|
281
|
-
|
|
281
|
+
if (config_1.config.port === 80) {
|
|
282
|
+
console.log(`http://${ip}`);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
console.log(`http://${ip}:${config_1.config.port}`);
|
|
286
|
+
}
|
|
282
287
|
});
|
|
283
288
|
process.on('beforeExit', () => {
|
|
284
289
|
if (server.listening) {
|
package/dist/task/task.js
CHANGED
|
@@ -23,7 +23,7 @@ exports.TaskController = TaskController;
|
|
|
23
23
|
async function execTask(task) {
|
|
24
24
|
const start = new Date().getTime();
|
|
25
25
|
try {
|
|
26
|
-
(0, log_1.getLogger)().
|
|
26
|
+
(0, log_1.getLogger)().debug(`START TASK:${task.name}`);
|
|
27
27
|
await task.run();
|
|
28
28
|
}
|
|
29
29
|
catch (e) {
|
|
@@ -34,6 +34,9 @@ async function execTask(task) {
|
|
|
34
34
|
if (cost > 1000 * 60 * 5) {
|
|
35
35
|
(0, log_1.getLogger)().warn(`Task "${task.name}" takes too long ,cost ${cost}ms`);
|
|
36
36
|
}
|
|
37
|
+
else {
|
|
38
|
+
(0, log_1.getLogger)().debug(`Task "${task.name}" has finished, taking a total of 388 milliseconds.`);
|
|
39
|
+
}
|
|
37
40
|
return { start, cost, end };
|
|
38
41
|
}
|
|
39
42
|
exports.execTask = execTask;
|
|
@@ -118,15 +118,99 @@ await startWebServer({
|
|
|
118
118
|
exchange.respondErrMsg(e.errMsg, 400)
|
|
119
119
|
return
|
|
120
120
|
}
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
|
|
121
|
+
// 对于不能处理的异常,可直接抛出
|
|
122
|
+
// 框架最终会响应 500 状态码,并记录错误信息到日志里以便于线上排查
|
|
123
|
+
throw e
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
]
|
|
127
127
|
})
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
+
### 异常处理
|
|
131
|
+
|
|
132
|
+
对于业务处理失败的情况下,推荐的做法是抛出异常来中止处理流程,然后由拦截器来统一处理进行响应。
|
|
133
|
+
但是这并非是强制的,框架也没有预置任何相关的类型或函数,需要自己根据实际情况来扩展,下面是一个例子。
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
/**
|
|
137
|
+
* 自定义业务异常.
|
|
138
|
+
*/
|
|
139
|
+
export class BusinessException {
|
|
140
|
+
constructor(
|
|
141
|
+
/**
|
|
142
|
+
* 提示信息.
|
|
143
|
+
*/
|
|
144
|
+
readonly message: string,
|
|
145
|
+
/**
|
|
146
|
+
* 自定义状态码,默认 400
|
|
147
|
+
*/
|
|
148
|
+
readonly status?: number
|
|
149
|
+
) {}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 全局异常拦截器,对一些特定的异常做处理,给予合适的响应信息.
|
|
154
|
+
* @param exchange
|
|
155
|
+
* @param next
|
|
156
|
+
*/
|
|
157
|
+
export async function globalErrorInterceptor(
|
|
158
|
+
exchange: ServerExchange,
|
|
159
|
+
next: () => Promise<void>
|
|
160
|
+
): Promise<void> {
|
|
161
|
+
try {
|
|
162
|
+
await next()
|
|
163
|
+
} catch (e) {
|
|
164
|
+
// 处理自定义业务异常
|
|
165
|
+
if (e instanceof BusinessException) {
|
|
166
|
+
const status = typeof e.status === 'number' ? e.status : 400
|
|
167
|
+
const message = e.message || ''
|
|
168
|
+
exchange.respondErrMsg(message, status)
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
// 处理校验异常
|
|
172
|
+
if (e instanceof ValidationException) {
|
|
173
|
+
exchange.respondErrMsg(e.errMsg, 400)
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
// 其它异常不需要处理的直接抛出,框架默认会响应 500 状态码
|
|
177
|
+
throw e
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
实际开发中可根据情况设置多个业务异常,分别响应不同的 JSON 格式,
|
|
183
|
+
respondErrMsg 方法响应的 json 格式是框架预设的,可通过 respondJson 方法响应自定义的格式。
|
|
184
|
+
|
|
185
|
+
将错误处理拦截器设置为第一个拦截器,在使用请求处理过程中使用自定义异常。
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
// 启动服务
|
|
189
|
+
await startWebServer({
|
|
190
|
+
interceptors: [
|
|
191
|
+
// 将错误处理拦截器设置为第一个拦截器
|
|
192
|
+
globalErrorInterceptor,
|
|
193
|
+
// 接下来是其它的拦截器
|
|
194
|
+
authInterceptor
|
|
195
|
+
],
|
|
196
|
+
// 路由
|
|
197
|
+
routers: {
|
|
198
|
+
'/order/cancel': createJsonHandler<{ id: string }>({
|
|
199
|
+
validation: { id: [notBlank()] },
|
|
200
|
+
async handle(body, exchange) {
|
|
201
|
+
const order = await findOrderById(body.id)
|
|
202
|
+
if (!order) {
|
|
203
|
+
// 使用自定义业务异常来中断处理流程
|
|
204
|
+
throw new BusinessException('找不到订单')
|
|
205
|
+
}
|
|
206
|
+
// 继续处理 ...
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
// 省略路由配置代码
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
```
|
|
213
|
+
|
|
130
214
|
### 针对不同的方法进行处理
|
|
131
215
|
|
|
132
216
|
组件提供了 restful 函数,可以在配置路由时按请求方法进行分发。
|
|
@@ -159,11 +243,12 @@ await startWebServer({
|
|
|
159
243
|
请求和响应都是 json 格式,响应和请求都可以为空。请求为空可以填入类型 {} 来表示空对象,响应为空可以填入类型 void 。
|
|
160
244
|
|
|
161
245
|
```ts
|
|
162
|
-
//
|
|
246
|
+
// json 格式请求正文定义
|
|
163
247
|
interface Form {
|
|
164
248
|
name: string
|
|
165
249
|
age: number
|
|
166
250
|
}
|
|
251
|
+
// json 格式响应信息定义
|
|
167
252
|
interface Resp {
|
|
168
253
|
id: string
|
|
169
254
|
}
|
|
@@ -190,7 +275,71 @@ export const userCreateHandler = createJsonHandler<Form, Resp>({
|
|
|
190
275
|
|
|
191
276
|
校验时会自动根据消息头 `accept-language` 切换校验器的语言,对于没有自定义错误信息的校验使用切换后的语言给予默认的提示。
|
|
192
277
|
|
|
193
|
-
###
|
|
278
|
+
### 二进制(Binary)上传
|
|
279
|
+
|
|
280
|
+
二制进上传也就是将文件以二进制的形式写入请求正文,请求正文仅存储文件内容没有别的信息,
|
|
281
|
+
请求的格式(Content-type)是 application/octet-stream 。
|
|
282
|
+
|
|
283
|
+
这种形式带来的好处就是简单,性能更好,因为服务器端拿到请求正文就是文件,不像 multipart/form-data 格式过需要解析内容来获取信息。
|
|
284
|
+
但是由于请求正文仅存储了文件,其它的业务参数只能通过查询字符串(Query String)带入。
|
|
285
|
+
|
|
286
|
+
框架提供了 createUploadHandler 函数带创建处理二进制格式请求的路由处理器,响应 JSON 格式,下面是示例。
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
interface Resp {
|
|
290
|
+
/**
|
|
291
|
+
* 新头像访问地址
|
|
292
|
+
*/
|
|
293
|
+
url: string
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 演示场景: 上传头像
|
|
298
|
+
*/
|
|
299
|
+
export const uploadAvatar = createUploadHandler<Resp>({
|
|
300
|
+
async handle(body, exchange) {
|
|
301
|
+
// 通过 Query String 获取用户id参数
|
|
302
|
+
const userId = exchange.query.getStr('userId') as string
|
|
303
|
+
// 校验参数
|
|
304
|
+
validate(
|
|
305
|
+
{ userId },
|
|
306
|
+
{
|
|
307
|
+
userId: [notBlank(), maxLength(64)]
|
|
308
|
+
}
|
|
309
|
+
)
|
|
310
|
+
// 判定文件大小
|
|
311
|
+
if (body.byteLength > 2 * 1024 * 1024) {
|
|
312
|
+
// BusinessException 是自定义的业务异常,抛出后由拦截器统一处理
|
|
313
|
+
throw new BusinessException('文件大小不得超过 2MB')
|
|
314
|
+
}
|
|
315
|
+
const user = await getMysqlManager().findById(tableUser, userId)
|
|
316
|
+
if (!user) {
|
|
317
|
+
throw new BusinessException('找不到用户')
|
|
318
|
+
}
|
|
319
|
+
// 模拟上传到文件服务器的场景,oss 是对象存储服务 sdk
|
|
320
|
+
const key = `users/${userId}/avatar`
|
|
321
|
+
await oss.putObject(key, body)
|
|
322
|
+
await getMysqlManager().partialUpdate(tableUser, { id: userId, avatar_key: key })
|
|
323
|
+
return { url: oss.getUrl(key) }
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
如果需要更高的自由度,比如不想返回 json 格式,定义普通的路由处理器即可,通过 exchage 上的 bodyBuffer 方法即可获取请求正文内容。
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
export const uploadAvatar: RouterHandler = async exchange => {
|
|
332
|
+
// 读取文件
|
|
333
|
+
const file = await exchange.bodyBuffer()
|
|
334
|
+
// 获取 Query String 上的参数
|
|
335
|
+
const query = exchange.parseQueryString()
|
|
336
|
+
const userId = query.getStr('userId')
|
|
337
|
+
|
|
338
|
+
// 校验参数和存储文件等流程省略...
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### multipart/form-data 格式上传文件处理
|
|
194
343
|
|
|
195
344
|
组件目前尚未实现对 multipart/form-data 类型请求的处理,如有需要可通一些第三方库来解析文件上传的请求内容。
|
|
196
345
|
|
|
@@ -250,7 +399,6 @@ await startWebServer({
|
|
|
250
399
|
add({ tag: 'h1', children: ['个人中心'] })
|
|
251
400
|
// 在 user 有值和无值的情况下分别渲染不同的元素
|
|
252
401
|
if (user) {
|
|
253
|
-
1
|
|
254
402
|
add({ tag: 'p', children: [`用户名:${user.account}`] })
|
|
255
403
|
} else {
|
|
256
404
|
add({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wok-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"packageManager": "pnpm@8.9.0",
|
|
5
5
|
"description": "一个基于 NodeJs 和 TypeScript 的后端框架,轻量级、克制、简洁。A lightweight, restrained, and concise backend framework based on Node.js and TypeScript.",
|
|
6
6
|
"scripts": {
|
package/types/cache/cache.d.ts
CHANGED
|
@@ -14,12 +14,12 @@ export interface CacheContent {
|
|
|
14
14
|
}
|
|
15
15
|
export declare class Cache {
|
|
16
16
|
private readonly valueMap;
|
|
17
|
-
private readonly stat
|
|
17
|
+
private readonly stat?;
|
|
18
18
|
/**
|
|
19
19
|
* promise 表,作用是处理异步并发问题,如果有相同的 key 同时请求,保证异步的 provider 只执行一次
|
|
20
20
|
*/
|
|
21
21
|
private promiseMap;
|
|
22
|
-
constructor(valueMap: Map<string, CacheContent>, stat
|
|
22
|
+
constructor(valueMap: Map<string, CacheContent>, stat?: CacheStat | undefined);
|
|
23
23
|
/**
|
|
24
24
|
* 放入缓存
|
|
25
25
|
* @param key
|
|
@@ -5,7 +5,7 @@ import { CacheContent } from './cache';
|
|
|
5
5
|
*/
|
|
6
6
|
export declare class PurgeTask implements Task {
|
|
7
7
|
private readonly valueMap;
|
|
8
|
-
readonly name = "
|
|
8
|
+
readonly name = "Cache purge";
|
|
9
9
|
constructor(valueMap: Map<string, CacheContent>);
|
|
10
10
|
run(): Promise<void>;
|
|
11
11
|
}
|
package/types/log/config.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { LogLevel } from './level';
|
|
|
2
2
|
/**
|
|
3
3
|
* 日志配置的定义
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
interface EnvConfig {
|
|
6
6
|
/**
|
|
7
7
|
* 是否输出到文件,如果为 true ,则会在根目录下生成 logs 目录存放日志文件.
|
|
8
8
|
*/
|
|
@@ -18,10 +18,10 @@ export interface LogConfig {
|
|
|
18
18
|
/**
|
|
19
19
|
* 要输出的日志级别.
|
|
20
20
|
*/
|
|
21
|
-
level:
|
|
21
|
+
level: string;
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
declare const config: LogConfig;
|
|
27
|
-
export {
|
|
23
|
+
export type LogConfig = Omit<EnvConfig, 'level'> & {
|
|
24
|
+
level: LogLevel;
|
|
25
|
+
};
|
|
26
|
+
export declare const config: LogConfig;
|
|
27
|
+
export {};
|
package/types/log/level.d.ts
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
* 日志级别.
|
|
3
3
|
*/
|
|
4
4
|
export declare enum LogLevel {
|
|
5
|
-
DEBUG =
|
|
6
|
-
INFO =
|
|
7
|
-
WARN =
|
|
8
|
-
ERROR =
|
|
5
|
+
DEBUG = 1,
|
|
6
|
+
INFO = 2,
|
|
7
|
+
WARN = 3,
|
|
8
|
+
ERROR = 4
|
|
9
9
|
}
|
|
10
|
-
export declare const LogLevelVals: Record<LogLevel, number>;
|
|
11
10
|
/**
|
|
12
11
|
* 解析日志级别
|
|
13
12
|
* @param level
|