lakutata 3.0.0 → 3.1.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/README.md +50 -30
- package/README.zh-CN.md +130 -0
- package/dist/cjs/exports/QueueComponent.d.ts +1 -0
- package/dist/cjs/exports/QueueComponent.js +20 -0
- package/dist/esm/exports/QueueComponent.js +1 -0
- package/dist/types/exports/QueueComponent.d.ts +1 -0
- package/doc/en/Application.md +107 -0
- package/doc/en/CLI.md +274 -0
- package/doc/en/Cache.md +379 -0
- package/doc/en/Command.md +110 -0
- package/doc/en/ContainerdIntegration.md +17 -16
- package/doc/en/Controllers.md +256 -0
- package/doc/en/DBus.md +291 -0
- package/doc/en/DTOValidation.md +72 -43
- package/doc/en/Database.md +188 -0
- package/doc/en/DatabaseDrivers.md +287 -0
- package/doc/en/Decorator.md +52 -40
- package/doc/en/DockerIntegration.md +7 -6
- package/doc/en/Entities.md +240 -0
- package/doc/en/Entrypoint.md +216 -0
- package/doc/en/Exceptions.md +174 -0
- package/doc/en/GRPC.md +251 -0
- package/doc/en/GettingStarted.md +165 -0
- package/doc/en/HTTP.md +507 -0
- package/doc/en/HelperFunctions.md +11 -11
- package/doc/en/I18n.md +119 -115
- package/doc/en/Injection.md +144 -0
- package/doc/en/Installers.md +280 -0
- package/doc/en/Logger.md +206 -0
- package/doc/en/MCP.md +293 -0
- package/doc/en/Middlewares.md +213 -0
- package/doc/en/Migrations.md +202 -0
- package/doc/en/MongoEmbedded.md +149 -0
- package/doc/en/Monitor.md +164 -0
- package/doc/en/Mutex.md +27 -25
- package/doc/en/NATS.md +246 -0
- package/doc/en/NativeAddons.md +212 -0
- package/doc/en/Objects.md +143 -0
- package/doc/en/Overview.md +62 -85
- package/doc/en/Packaging.md +193 -0
- package/doc/en/ProjectStructure.md +124 -0
- package/doc/en/Queue.md +269 -0
- package/doc/en/Schedules.md +217 -0
- package/doc/en/Security.md +10 -9
- package/doc/en/ServiceCalls.md +504 -0
- package/doc/en/Time.md +19 -20
- package/doc/en/UpgradingFrom1.md +435 -0
- package/doc/en/UpgradingFrom2.md +315 -0
- package/doc/en/Workspaces.md +189 -0
- package/doc/en/examples/BackgroundJobs.md +677 -0
- package/doc/en/examples/CommandLineTool.md +664 -0
- package/doc/en/examples/DesktopApp.md +809 -0
- package/doc/en/examples/DeviceService.md +607 -0
- package/doc/en/examples/MCPServer.md +653 -0
- package/doc/en/examples/README.md +16 -0
- package/doc/en/examples/RestAPI.md +490 -0
- package/doc/en/examples/ServiceCalls.md +829 -0
- package/doc/en/examples/WebApp.md +568 -0
- package/doc/zh/Application.md +75 -0
- package/doc/zh/CLI.md +250 -0
- package/doc/zh/Cache.md +328 -0
- package/doc/zh/Command.md +78 -0
- package/doc/zh/ContainerdIntegration.md +9 -9
- package/doc/zh/Controllers.md +228 -0
- package/doc/zh/DBus.md +257 -0
- package/doc/zh/DTOValidation.md +49 -19
- package/doc/zh/Database.md +175 -0
- package/doc/zh/DatabaseDrivers.md +261 -0
- package/doc/zh/Decorator.md +25 -18
- package/doc/zh/DockerIntegration.md +6 -6
- package/doc/zh/Entities.md +224 -0
- package/doc/zh/Entrypoint.md +193 -0
- package/doc/zh/Exceptions.md +158 -0
- package/doc/zh/GRPC.md +226 -0
- package/doc/zh/GettingStarted.md +148 -0
- package/doc/zh/HTTP.md +445 -0
- package/doc/zh/HelperFunctions.md +2 -2
- package/doc/zh/I18n.md +26 -26
- package/doc/zh/Injection.md +129 -0
- package/doc/zh/Installers.md +232 -0
- package/doc/zh/Logger.md +188 -0
- package/doc/zh/MCP.md +265 -0
- package/doc/zh/Middlewares.md +198 -0
- package/doc/zh/Migrations.md +178 -0
- package/doc/zh/MongoEmbedded.md +118 -0
- package/doc/zh/Monitor.md +145 -0
- package/doc/zh/Mutex.md +10 -10
- package/doc/zh/NATS.md +207 -0
- package/doc/zh/NativeAddons.md +180 -0
- package/doc/zh/Objects.md +119 -0
- package/doc/zh/Overview.md +46 -71
- package/doc/zh/Packaging.md +154 -0
- package/doc/zh/ProjectStructure.md +102 -0
- package/doc/zh/Queue.md +214 -0
- package/doc/zh/Schedules.md +193 -0
- package/doc/zh/Security.md +6 -6
- package/doc/zh/ServiceCalls.md +459 -0
- package/doc/zh/Time.md +2 -2
- package/doc/zh/UpgradingFrom1.md +347 -0
- package/doc/zh/UpgradingFrom2.md +230 -0
- package/doc/zh/Workspaces.md +150 -0
- package/doc/zh/examples/BackgroundJobs.md +618 -0
- package/doc/zh/examples/CommandLineTool.md +618 -0
- package/doc/zh/examples/DesktopApp.md +748 -0
- package/doc/zh/examples/DeviceService.md +546 -0
- package/doc/zh/examples/MCPServer.md +604 -0
- package/doc/zh/examples/README.md +14 -0
- package/doc/zh/examples/RestAPI.md +458 -0
- package/doc/zh/examples/ServiceCalls.md +765 -0
- package/doc/zh/examples/WebApp.md +520 -0
- package/package.json +46 -17
- package/doc/en/ActionPattern.md +0 -408
- package/doc/en/Core.md +0 -312
- package/doc/en/ORMIntegration.md +0 -341
- package/doc/zh/ActionPattern.md +0 -334
- package/doc/zh/Core.md +0 -256
- package/doc/zh/ORMIntegration.md +0 -319
package/doc/zh/CLI.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Lakutata 命令行
|
|
2
|
+
|
|
3
|
+
命令行适配器把控制器里标了 `@CLIAction` 的动作变成命令行工具的命令,底层用的是 [Commander](https://github.com/tj/commander.js)。除了 `lakutata`,还要安装 `commander`(12 到 15 版);如果希望工具在缺少输入时向用户提问,再安装 `@inquirer/prompts`(7 或 8 版):
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install commander @inquirer/prompts
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## 1. 第一个命令行工具
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import {Application, DTO} from 'lakutata'
|
|
13
|
+
import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
|
|
14
|
+
import {CLIAction} from 'lakutata/decorator/ctrl'
|
|
15
|
+
import {Expect} from 'lakutata/decorator/dto'
|
|
16
|
+
import {commanderCLIEntrypoint} from 'lakutata/adapter/commander'
|
|
17
|
+
|
|
18
|
+
class GreetDTO extends DTO {
|
|
19
|
+
@Expect(DTO.String().required().description('The name to greet'))
|
|
20
|
+
public name: string
|
|
21
|
+
|
|
22
|
+
@Expect(DTO.Number().integer().min(1).default(1).description('How many times'))
|
|
23
|
+
public times: number
|
|
24
|
+
|
|
25
|
+
@Expect(DTO.Boolean().default(false).description('Greet in capitals'))
|
|
26
|
+
public loud: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class ToolController extends Controller {
|
|
30
|
+
@CLIAction('greet <name>', GreetDTO, {description: 'Greet someone'})
|
|
31
|
+
public async greet(input: GreetDTO): Promise<string> {
|
|
32
|
+
const text: string = `Hello, ${input.name}`
|
|
33
|
+
return Array(input.times).fill(input.loud ? text.toUpperCase() : text).join('\n')
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Application.run(() => ({
|
|
38
|
+
id: 'tool.app',
|
|
39
|
+
name: 'Tool',
|
|
40
|
+
components: {
|
|
41
|
+
entrypoint: buildEntrypoints({
|
|
42
|
+
controllers: [ToolController],
|
|
43
|
+
cli: commanderCLIEntrypoint({bin: 'tool', description: 'A small tool', version: '1.0.0'})
|
|
44
|
+
})
|
|
45
|
+
},
|
|
46
|
+
bootstrap: ['entrypoint']
|
|
47
|
+
}))
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
$ node dist/index.js greet Ada --times 2 --loud
|
|
52
|
+
HELLO, ADA
|
|
53
|
+
HELLO, ADA
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
运行过程如下:
|
|
57
|
+
|
|
58
|
+
1. 应用启动。入口必须列在 `bootstrap` 里,否则没有谁会创建它,任何命令都不会执行。
|
|
59
|
+
2. 应用启动完成后(`bootstrap` 里的对象都已创建,其他适配器也已启动),适配器读取命令行,执行对应的动作。
|
|
60
|
+
3. 结果写到 stdout,错误写到 stderr。
|
|
61
|
+
4. 应用随后停止,进程退出:命令成功时退出码为 0,失败时为 1。
|
|
62
|
+
|
|
63
|
+
`bin` 是帮助信息里显示的程序名。不设置时,适配器取脚本文件名,`node dist/index.js` 得到的就是 `index`。
|
|
64
|
+
|
|
65
|
+
## 2. 命令与参数
|
|
66
|
+
|
|
67
|
+
`@CLIAction(command, DTO?, options?)`(来自 `lakutata/decorator/ctrl`)声明一个命令。`command` 字符串是命令名加上它的位置参数,写法与 Commander 相同:
|
|
68
|
+
|
|
69
|
+
| 写法 | 含义 |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `<name>` | 必填参数 |
|
|
72
|
+
| `[name]` | 可选参数 |
|
|
73
|
+
| `<files...>` | 一个或多个参数,收集成数组(`[files...]` 表示零个或多个) |
|
|
74
|
+
|
|
75
|
+
每个参数会按同名 DTO 属性的类型转换后交给该属性。多余的参数会报错。
|
|
76
|
+
|
|
77
|
+
**分组。** 命令名前面的词是分组。`@CLIAction('file copy <files...>', CopyDTO)` 是分组 `file` 下的命令 `copy`,用 `tool file copy a.txt b.txt` 执行。分组可以嵌套(`'pack linux deb'`)。适配器的 `groups` 选项为分组写说明,`tool file --help` 会列出分组下的命令。下面是一个包含命令 `greet` 和分组 `file` 的工具的帮助:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
$ tool --help
|
|
81
|
+
Usage: tool [options] [command]
|
|
82
|
+
|
|
83
|
+
A small tool
|
|
84
|
+
|
|
85
|
+
Options:
|
|
86
|
+
-V, --version output the version number
|
|
87
|
+
-h, --help display help for command
|
|
88
|
+
|
|
89
|
+
Commands:
|
|
90
|
+
greet [options] <name> Greet someone
|
|
91
|
+
file Work on files
|
|
92
|
+
help [command] display help for command
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**帮助文字。** 动作选项 `description` 是命令在帮助里的说明。不写的话,帮助里显示动作名(`ToolController:greet`)。
|
|
96
|
+
|
|
97
|
+
**前缀。** 在控制器上加 `@Prefix({command: 'user'})`,前缀会用冒号接在它的每个命令前面:`@CLIAction('get <id>')` 变成 `tool user:get 42`。前缀是命令名的一部分,不是分组。
|
|
98
|
+
|
|
99
|
+
## 3. 选项
|
|
100
|
+
|
|
101
|
+
DTO 的其他属性都是选项。选项名是属性名的短横线形式(`keepTimes` 对应 `--keep-times`),选项的形式由属性的类型决定:
|
|
102
|
+
|
|
103
|
+
| 属性类型 | 选项形式 | 示例 |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| 布尔值 | 开关 | `--loud` |
|
|
106
|
+
| 默认值为 `true` 的布尔值 | 开关及其反向开关 | `--keep-times`、`--no-keep-times` |
|
|
107
|
+
| `DTO.Alternatives(DTO.Boolean(), …)` | 可带值的开关 | `--color`、`--color always` |
|
|
108
|
+
| 数字 | 一个数字 | `--times 2` |
|
|
109
|
+
| 数组 | 一个或多个值,重复写同一选项也会累加 | `--ports 80 443` |
|
|
110
|
+
| 对象 | 一个 JSON 值 | `--filter '{"active":true}'` |
|
|
111
|
+
| 其他 | 一个字符串 | `--mode fast` |
|
|
112
|
+
|
|
113
|
+
帮助里会显示每个属性的 `.description()`、默认值和允许的取值(`.valid()`)。不在允许范围内的取值、不是数字的数字,都会在动作执行前被拒绝:
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
$ tool file copy --help
|
|
117
|
+
Usage: tool file copy [options] <files...>
|
|
118
|
+
|
|
119
|
+
Copy files
|
|
120
|
+
|
|
121
|
+
Options:
|
|
122
|
+
--mode <value> The copy mode (choices: "fast", "safe", default: "safe")
|
|
123
|
+
--keep-times Keep the timestamps (default: true)
|
|
124
|
+
--no-keep-times Turn off --keep-times
|
|
125
|
+
--ports <values...> The ports
|
|
126
|
+
-h, --help display help for command
|
|
127
|
+
|
|
128
|
+
$ tool file copy a.txt --mode slow
|
|
129
|
+
error: option '--mode <value>' argument 'slow' is invalid. Allowed choices are "fast", "safe".
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- 没有默认值的必填属性必须给出,除非工具会向用户提问(见第 5 节)。
|
|
133
|
+
- 选项没有 `-m` 这样的短写形式。
|
|
134
|
+
- 没有 DTO 的命令不接受任何选项,它的参数以字符串的形式交给动作。
|
|
135
|
+
|
|
136
|
+
之后 DTO 会像校验其他动作一样校验输入:补上默认值,去掉未知的键(见 [DTO 校验](DTOValidation.md))。
|
|
137
|
+
|
|
138
|
+
## 4. 输出与退出码
|
|
139
|
+
|
|
140
|
+
动作的返回值写到 stdout:
|
|
141
|
+
|
|
142
|
+
| 返回值 | 输出 |
|
|
143
|
+
|---|---|
|
|
144
|
+
| 字符串 | 原样输出,末尾没有换行时补一个换行 |
|
|
145
|
+
| `null`、`undefined` | 什么都不输出 |
|
|
146
|
+
| `Uint8Array`(`Buffer`) | 原样输出字节 |
|
|
147
|
+
| 其他值 | 带缩进的 JSON |
|
|
148
|
+
| `Result.file(path)`(也可以是 `Buffer` 或流) | 文件内容 |
|
|
149
|
+
| `Result.events(producer)` | 每个事件的数据,随发随写 |
|
|
150
|
+
| `Result.status(status, data)` | 只输出数据 |
|
|
151
|
+
|
|
152
|
+
`Result` 来自 `lakutata/com/entrypoint`。`format` 选项可以替换这套格式:它接收每个结果(以及每个事件),返回字符串、字节,或者返回 `undefined` 表示不输出。
|
|
153
|
+
|
|
154
|
+
各种情况下的输出位置和退出码:
|
|
155
|
+
|
|
156
|
+
| 情况 | 输出 | 退出码 |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| 命令成功 | 结果写到 stdout | 0 |
|
|
159
|
+
| 动作、中间件、校验或访问控制失败 | stderr 上的 `error: <消息>` | 1 |
|
|
160
|
+
| 命令不存在、缺少参数、取值不合法、参数多余 | stderr 上 Commander 的提示 | 1 |
|
|
161
|
+
| `--help`、`--version` | stdout | 0 |
|
|
162
|
+
| 没有给出任何命令 | 帮助写到 stderr | 1 |
|
|
163
|
+
|
|
164
|
+
```text
|
|
165
|
+
$ tool greet
|
|
166
|
+
error: missing required argument 'name'
|
|
167
|
+
$ tool greet Ada --times x
|
|
168
|
+
error: option '--times <value>' argument 'x' is invalid. Not a number.
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
在动作里,`this.context` 是一个 `CLIContext`:它的 `command` 是声明时的命令(含前缀,如 `'user:get <id>'`),`data` 是从命令行读到的参数和选项(尚未校验)。
|
|
172
|
+
|
|
173
|
+
## 5. 提示输入
|
|
174
|
+
|
|
175
|
+
工具运行在终端里、并且装了 `@inquirer/prompts` 时,缺少的值会改为向用户提问,而不是直接报错。它会依次询问每个没有值、也没有默认值的必填属性。缺少的必填位置参数(`<name>`)不会询问,而是直接报错。
|
|
176
|
+
|
|
177
|
+
| 属性 | 提问方式 |
|
|
178
|
+
|---|---|
|
|
179
|
+
| 布尔值 | 是 / 否 |
|
|
180
|
+
| 有允许取值(`.valid()`) | 从列表中选一个 |
|
|
181
|
+
| 由允许取值组成的数组 | 多选框 |
|
|
182
|
+
| 数字 | 输入数字 |
|
|
183
|
+
| 名字像密码、密钥、令牌、API key 或凭据 | 隐藏输入,显示为 `*` |
|
|
184
|
+
| 其他 | 输入文本;数组用逗号分隔,对象写成 JSON |
|
|
185
|
+
|
|
186
|
+
每个回答都会立即按该属性的校验规则检查:不合法时显示错误并重新提问。问题的文字是属性的 `.description()` 加上属性名。
|
|
187
|
+
|
|
188
|
+
`interactive` 选项决定什么时候提问:
|
|
189
|
+
|
|
190
|
+
- `'auto'`(默认):stdin 和 stdout 都是终端时才提问。在脚本里通过管道输入或输出时,缺少的值直接报错,而不是等待回答。
|
|
191
|
+
- `true`:总是提问(前提是装了 `@inquirer/prompts`)。
|
|
192
|
+
- `false`:从不提问。
|
|
193
|
+
|
|
194
|
+
## 6. 适配器的选项
|
|
195
|
+
|
|
196
|
+
| 选项 | 默认值 | 含义 |
|
|
197
|
+
|---|---|---|
|
|
198
|
+
| `bin` | 脚本文件名 | 帮助里显示的程序名 |
|
|
199
|
+
| `description` | | 帮助里显示的程序说明 |
|
|
200
|
+
| `version` | | 加上 `-V, --version`,输出这个版本号 |
|
|
201
|
+
| `groups` | | 分组的说明:`{file: 'Work on files', 'pack linux': 'Linux packages'}` |
|
|
202
|
+
| `argv` | `process.argv.slice(2)` | 要读取的参数 |
|
|
203
|
+
| `interactive` | `'auto'` | 什么时候向用户提问(见第 5 节) |
|
|
204
|
+
| `stdin`、`stdout`、`stderr` | 进程的标准流 | 提问从哪里读,结果和错误写到哪里 |
|
|
205
|
+
| `format` | 见第 4 节 | 把结果转换成要输出的文本或字节 |
|
|
206
|
+
| `noCommand` | `'help'` | 没有参数时:`'help'` 输出帮助并以退出码 1 退出,`'ignore'` 什么都不执行,应用继续运行 |
|
|
207
|
+
| `exit` | `'application'` | 命令执行完之后:`'application'` 停止应用并以命令的退出码退出,`'process'` 只设置 `process.exitCode`,`'none'` 什么都不做,传入函数则接收退出码 |
|
|
208
|
+
| `configure` | | `(program) => void`,在命令添加完、读取参数之前,用 Commander 的 program 调用:可以加全局选项、钩子 |
|
|
209
|
+
| `translate` | | 翻译帮助和提问(见 [I18n](I18n.md#命令行程序的帮助和提示)) |
|
|
210
|
+
| `middlewares` | | 这个适配器的命令专用的中间件,在入口的中间件之后执行 |
|
|
211
|
+
| `name` | | 适配器的名字,用来区分多个适配器 |
|
|
212
|
+
|
|
213
|
+
## 7. 服务端应用里的命令
|
|
214
|
+
|
|
215
|
+
一个应用可以同时提供 HTTP 服务和命令。但默认情况下,命令行适配器执行完命令就会停止应用,没有参数时会输出帮助并退出。如果希望不带参数运行时启动服务、带命令运行时执行命令,就设置 `noCommand: 'ignore'`:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import {Application} from 'lakutata'
|
|
219
|
+
import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
|
|
220
|
+
import {CLIAction, GET} from 'lakutata/decorator/ctrl'
|
|
221
|
+
import {commanderCLIEntrypoint} from 'lakutata/adapter/commander'
|
|
222
|
+
import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
|
|
223
|
+
|
|
224
|
+
class AppController extends Controller {
|
|
225
|
+
@GET('/hello')
|
|
226
|
+
public async hello(): Promise<string> {
|
|
227
|
+
return 'Hello'
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@CLIAction('migrate', {description: 'Migrate the database'})
|
|
231
|
+
public async migrate(): Promise<string> {
|
|
232
|
+
return 'Migrated'
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
Application.run(() => ({
|
|
237
|
+
id: 'shop.app',
|
|
238
|
+
name: 'Shop',
|
|
239
|
+
components: {
|
|
240
|
+
entrypoint: buildEntrypoints({
|
|
241
|
+
controllers: [AppController],
|
|
242
|
+
cli: commanderCLIEntrypoint({bin: 'shop', noCommand: 'ignore'}),
|
|
243
|
+
http: expressHTTPEntrypoint({port: 8080})
|
|
244
|
+
})
|
|
245
|
+
},
|
|
246
|
+
bootstrap: ['entrypoint']
|
|
247
|
+
}))
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`node dist/index.js` 启动服务并持续运行;`node dist/index.js migrate` 在服务启动后执行命令,然后停止应用。
|
package/doc/zh/Cache.md
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# Lakutata 缓存
|
|
2
|
+
|
|
3
|
+
**Cacher 组件**(`lakutata/com/cacher`,`@lakutata/cache` 包)是一个键值缓存,其中的条目会过期。条目可以保存在进程的内存中、一个文件中,或者应用的各个实例共用的服务器上:Redis、Memcache、MongoDB、SQLite、PostgreSQL 或 MySQL。多个存储组成分层,最快的在最前面。它的 `wrap()` 方法返回缓存的值;值不存在时,先计算出值并缓存起来,再返回。
|
|
4
|
+
|
|
5
|
+
它不是数据库的查询缓存(`Database` 的 `cache` 选项,见 [Database](Database.md))。
|
|
6
|
+
|
|
7
|
+
## 1. 第一个缓存
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import {Application, Component} from 'lakutata'
|
|
11
|
+
import {Inject} from 'lakutata/decorator/di'
|
|
12
|
+
import {buildCacherOptions, type Cacher} from 'lakutata/com/cacher'
|
|
13
|
+
|
|
14
|
+
type Product = { id: number, name: string }
|
|
15
|
+
|
|
16
|
+
class Catalog extends Component {
|
|
17
|
+
@Inject('cache')
|
|
18
|
+
protected readonly cache: Cacher
|
|
19
|
+
|
|
20
|
+
//只加载一次,之后从缓存读取,直到条目过期
|
|
21
|
+
public async product(id: number): Promise<Product> {
|
|
22
|
+
return await this.cache.wrap(`product:${id}`, (): Promise<Product> => this.load(id))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async rename(id: number, name: string): Promise<void> {
|
|
26
|
+
//先在这里更新数据库,再删除缓存中的副本
|
|
27
|
+
await this.cache.del(`product:${id}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected async load(id: number): Promise<Product> {
|
|
31
|
+
return {id: id, name: `Product ${id}`}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Application
|
|
36
|
+
.run(() => ({
|
|
37
|
+
id: 'catalog.app',
|
|
38
|
+
name: 'Catalog',
|
|
39
|
+
components: {
|
|
40
|
+
//保存在内存中;条目 5 分钟后过期
|
|
41
|
+
cache: buildCacherOptions({ttl: 300000}),
|
|
42
|
+
catalog: {class: Catalog}
|
|
43
|
+
}
|
|
44
|
+
}))
|
|
45
|
+
.onLaunched(async (app: Application): Promise<void> => {
|
|
46
|
+
const catalog: Catalog = await app.getObject<Catalog>('catalog')
|
|
47
|
+
console.log(await catalog.product(1))
|
|
48
|
+
const cache: Cacher = await app.getObject<Cacher>('cache')
|
|
49
|
+
//这个条目 30 秒后过期
|
|
50
|
+
await cache.set('greeting', 'Hello', 30000)
|
|
51
|
+
console.log(await cache.get<string>('greeting'))
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- `buildCacherOptions` 用你选定的名字声明组件;其他对象按这个名字注入它(`@Inject('cache')`),或者用 `getObject('cache')` 获取。整个应用共用一个实例。
|
|
56
|
+
- 没有 `stores` 时,缓存把条目保存在进程的内存中。
|
|
57
|
+
- 时长都是以毫秒为单位的数字。
|
|
58
|
+
- 队列可以把消息的结果保存在缓存中(见 [Queue](Queue.md) 第 1 节)。
|
|
59
|
+
|
|
60
|
+
## 2. 选项
|
|
61
|
+
|
|
62
|
+
| 选项 | 默认值 | 含义 |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `stores` | 一个内存存储 | 一个存储,或者存储的列表,最快的在最前面(第 5 节和第 6 节) |
|
|
65
|
+
| `ttl` | 无:条目永不过期 | 写入没有给出 TTL 时,条目保留多久,单位毫秒(正整数) |
|
|
66
|
+
| `refreshThreshold` | 无 | `wrap()` 后台刷新的默认阈值,单位毫秒(第 4 节) |
|
|
67
|
+
| `refreshAllStores` | `false` | 后台刷新是否把新值写入所有存储(第 5 节) |
|
|
68
|
+
| `nonBlocking` | `false` | 写入和删除是否不等待存储就返回(第 5 节) |
|
|
69
|
+
|
|
70
|
+
`cacheId` 选项可以填写,但不会被使用。
|
|
71
|
+
|
|
72
|
+
- 选项无效时,应用启动失败,抛出 `InvalidObjectOptionsException`。
|
|
73
|
+
- 存储在组件创建时连接。组件列在 `bootstrap` 里时,它在应用启动过程中创建,某个存储连不上会让启动失败;否则它在第一次被用到时才创建,存储的错误会让那次 `getObject()` 或注入失败。
|
|
74
|
+
- 应用停止时,缓存先等待正在进行的后台刷新完成,再断开存储(文件存储会先保存最后的修改)。
|
|
75
|
+
|
|
76
|
+
## 3. 读取与写入
|
|
77
|
+
|
|
78
|
+
| 方法 | 作用 | 返回 |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `set(key, value, ttl?)` | 把值写入所有存储,替换原来的值 | 写入的值 |
|
|
81
|
+
| `get(key)` | 读取一个值 | 值;键不存在或已过期时为 `undefined` |
|
|
82
|
+
| `multipleSet([{key, value, ttl?}])` | 写入多个值 | 写入的条目 |
|
|
83
|
+
| `multipleGet(keys)` | 读取多个值 | 各个值,顺序与键相同 |
|
|
84
|
+
| `getTTL(key)` | 读取条目什么时候过期 | 以毫秒计的时间戳,或者 `-1` |
|
|
85
|
+
| `wrap(key, fn, options?)` | 读取一个值,或者计算出值并缓存(第 4 节) | 值 |
|
|
86
|
+
| `del(key)` | 从所有存储中删除一个键 | `true` |
|
|
87
|
+
| `multipleDel(keys)` | 删除多个键 | `true` |
|
|
88
|
+
| `clear()` | 删除每个存储的所有条目,或者其命名空间中的条目(第 6 节) | `true` |
|
|
89
|
+
|
|
90
|
+
- **TTL。** 写入时给出的 TTL 会替代 `ttl` 选项,`0` 表示条目永不过期。`ttl` 选项本身不能为 `0`:要让条目永不过期,就不设置它。TTL 只能是数字,不能写成 `'1h'` 这样的文本。
|
|
91
|
+
- **`get()` 不会失败。** 存储读取失败算作未命中,它的错误作为 `'error'` 事件发出(第 8 节)。缓存的值是 `undefined` 时,无法与未命中区分。
|
|
92
|
+
- **`getTTL()` 返回的是一个时间点**,而不是剩余时间:剩余时间是 `expires - Date.now()`。键不存在、已经过期,或者条目永不过期时,它返回 `-1`。
|
|
93
|
+
- **`multipleSet()` 和 `multipleDel()` 不发出 `'set'` 和 `'del'` 事件**,这一点与 `set()`、`wrap()` 和 `del()` 不同。
|
|
94
|
+
- 即使键不存在,`del()` 和 `multipleDel()` 也返回 `true`。
|
|
95
|
+
- 存储写入或删除失败时,调用也会失败,除非设置了 `nonBlocking`(第 5 节)。
|
|
96
|
+
|
|
97
|
+
## 4. `wrap()`
|
|
98
|
+
|
|
99
|
+
`wrap(key, fn, options)` 返回键的缓存值。键不存在或已经过期时,它调用 `fn`,把结果缓存起来再返回。缓存查询结果或者对其他服务的调用结果,通常都用这种方式:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import type {Cacher} from 'lakutata/com/cacher'
|
|
103
|
+
|
|
104
|
+
declare const cache: Cacher
|
|
105
|
+
declare function loadRates(): Promise<Record<string, number>>
|
|
106
|
+
|
|
107
|
+
//每小时最多加载一次;在最后 5 分钟内,先返回缓存的值,同时在后台重新加载
|
|
108
|
+
const rates: Record<string, number> = await cache.wrap('rates', loadRates, {ttl: 3600000, refreshThreshold: 300000})
|
|
109
|
+
|
|
110
|
+
type User = { id: number, guest: boolean }
|
|
111
|
+
//TTL 取决于值
|
|
112
|
+
const user: User = await cache.wrap('user:1', async (): Promise<User> => ({id: 1, guest: true}), {
|
|
113
|
+
ttl: (value: User): number => value.guest ? 60000 : 3600000
|
|
114
|
+
})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- 第三个参数是一个 TTL(`wrap(key, fn, 60000)`),或者 `{ttl, refreshThreshold}`,两者都可以是数字,也可以是以值为参数的函数。它们默认取 `ttl` 和 `refreshThreshold` 选项。
|
|
118
|
+
- 某个键的一次 `wrap()` 进行期间,对这个键的其他调用共用这一次调用及其结果。这只在一个组件之内成立:应用的两个实例可能都会计算这个值。如果它只能被计算一次,就要加锁(见 [Mutex](Mutex.md))。
|
|
119
|
+
- `fn` 失败时,`wrap()` 以同一个错误失败,不缓存任何内容。结果为 `undefined` 时,也会像其他值一样被缓存。
|
|
120
|
+
- **后台刷新。** 缓存的值剩余的时间少于刷新阈值时,`wrap()` 立即返回它,同时在后台调用 `fn`,每个键同一时间只有一次刷新。新值替换旧值,并由 `'refresh'` 事件报告;`fn` 失败时,事件报告错误,旧值保留。永不过期的条目从不刷新;阈值大于 TTL 时,每次调用都会刷新。
|
|
121
|
+
- 有多个存储时,在较低层存储中找到的值会被复制到它上面的存储,保留它剩余的时间(第 5 节)。
|
|
122
|
+
|
|
123
|
+
## 5. 多个存储
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import {Application} from 'lakutata'
|
|
127
|
+
import {buildCacherOptions} from 'lakutata/com/cacher'
|
|
128
|
+
|
|
129
|
+
Application.run(() => ({
|
|
130
|
+
id: 'tiers.app',
|
|
131
|
+
name: 'Tiers',
|
|
132
|
+
components: {
|
|
133
|
+
cache: buildCacherOptions({
|
|
134
|
+
//每个实例在内存中保留副本,放在所有实例共用的 Redis 前面
|
|
135
|
+
stores: [
|
|
136
|
+
{type: 'memory'},
|
|
137
|
+
{type: 'redis', host: '127.0.0.1', namespace: 'tiers'}
|
|
138
|
+
],
|
|
139
|
+
ttl: 60000
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
}))
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- 读取按顺序询问各个存储,返回找到的第一个条目。`multipleGet()` 只向下一个存储询问仍未找到的键。
|
|
146
|
+
- 写入和删除同时发往所有存储。
|
|
147
|
+
- 只有 `wrap()` 会把在较低层存储中找到的条目复制到它上面的存储;`get()` 不会。
|
|
148
|
+
- 后台刷新把新值写入原来保存旧值的存储及其上面的存储;设置 `refreshAllStores: true` 时,写入所有存储。
|
|
149
|
+
- 一个实例内存中的副本会一直保留到条目过期,即使另一个实例修改或删除了这个键。如果每个实例都要很快看到修改,就把 TTL 设短一些。
|
|
150
|
+
- **`nonBlocking: true`。** 写入和删除不等待存储,立即返回,并且从不失败:失败作为事件发出(第 8 节)。有多个存储时,读取同时询问所有存储,返回最先找到的条目,它不一定来自第一个存储。
|
|
151
|
+
- 所有存储在缓存创建时连接。其中一个连接失败时,其他存储断开连接,创建以这个错误失败。
|
|
152
|
+
|
|
153
|
+
## 6. 存储
|
|
154
|
+
|
|
155
|
+
| `type` | 条目保存在哪里 | 需要安装 |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| `'memory'` | 进程的内存中(没有 `stores` 时缓存使用的存储) | 无 |
|
|
158
|
+
| `'file'` | 内存中,并保存到一个 JSON 文件 | 无 |
|
|
159
|
+
| `'redis'` | Redis 或 Valkey | `redis`(4 或 5) |
|
|
160
|
+
| `'memcache'` | memcached | `memjs` |
|
|
161
|
+
| `'mongo'` | MongoDB 的一个集合 | `mongodb` |
|
|
162
|
+
| `'sqlite'` | SQLite 文件中的一张表 | `better-sqlite3`(12);Bun 上无需安装 |
|
|
163
|
+
| `'postgres'` | PostgreSQL 的一张表 | `pg` |
|
|
164
|
+
| `'mysql'` | MySQL 或 MariaDB 的一张表 | `mysql2` |
|
|
165
|
+
|
|
166
|
+
- 存储所需的包没有安装时,缓存创建失败,抛出 `CacheDriverNotFoundException`,错误信息会写明要安装的包。
|
|
167
|
+
- 除了内存存储,其他存储都以 JSON 保存值(第 7 节)。
|
|
168
|
+
- 除了内存存储,其他存储都接受 `namespace`,用来区分共用同一台服务器或同一张表的应用:此时 `clear()` 只删除这个命名空间中的条目。没有命名空间时,`clear()` 删除存储中的所有条目(Redis 和 Memcache 见下文)。
|
|
169
|
+
- 在 SQLite、PostgreSQL 和 MySQL 存储中,没有任何机制在后台删除过期的行:过期的行一直留在表中,直到被读取(此时会被删除)、被覆盖或被清空。
|
|
170
|
+
|
|
171
|
+
### 内存
|
|
172
|
+
|
|
173
|
+
| 选项 | 默认值 | 含义 |
|
|
174
|
+
|---|---|---|
|
|
175
|
+
| `sweepInterval` | `60000` | 多久清除一次过期的条目,单位毫秒(过期的条目本来就算作未命中) |
|
|
176
|
+
|
|
177
|
+
值按原样保存,不复制(第 7 节)。每个进程有自己的条目,进程结束时条目丢失。
|
|
178
|
+
|
|
179
|
+
### 文件
|
|
180
|
+
|
|
181
|
+
| 选项 | 默认值 | 含义 |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| `filename` | 必填 | JSON 文件,相对于工作目录;文件连同目录一起自动创建 |
|
|
184
|
+
| `writeDelay` | `100` | 修改之后多久保存文件,单位毫秒;这段时间内的修改一起保存 |
|
|
185
|
+
| `expiredCheckDelay` | `3600000` | 两次从文件中清除过期条目之间的最短间隔,单位毫秒 |
|
|
186
|
+
| `namespace` | 无 | |
|
|
187
|
+
|
|
188
|
+
- 条目保存在内存中。文件在缓存创建时读取,每次保存都重写整个文件(通过一个临时文件),所以这个存储适合小型缓存。应用停止时会保存最后的修改。
|
|
189
|
+
- 一个文件只属于一个进程:另一个进程只在它自己的缓存创建时读取文件,而每次保存都会替换掉其他进程保存的条目。
|
|
190
|
+
- 文件不是 JSON 时,缓存创建失败。保存失败会作为 `'error'` 事件发出。
|
|
191
|
+
|
|
192
|
+
### Redis
|
|
193
|
+
|
|
194
|
+
| 选项 | 默认值 | 含义 |
|
|
195
|
+
|---|---|---|
|
|
196
|
+
| `host` | 必填 | |
|
|
197
|
+
| `port` | `6379` | |
|
|
198
|
+
| `database` | `0` | 数据库的编号 |
|
|
199
|
+
| `username`、`password` | 无 | |
|
|
200
|
+
| `tls` | `false` | 是否使用 TLS 连接 |
|
|
201
|
+
| `namespace` | 无 | |
|
|
202
|
+
| `keyPrefixSeparator` | `'::'` | 键中跟在命名空间后面的分隔符 |
|
|
203
|
+
| `connectTimeout` | `5000` | 第一次连接最多可以用多长时间,单位毫秒 |
|
|
204
|
+
| `throwOnConnectError` | `true` | 第一次连接失败时,是否让缓存创建失败 |
|
|
205
|
+
| `reconnect` | `true` | 连接断开后是否重连(每次尝试多等 100 毫秒,最多 3 秒) |
|
|
206
|
+
| `keepAlive` | 驱动的默认值 | 连接空闲多久之后发送第一个 TCP keep-alive 探测,单位毫秒 |
|
|
207
|
+
| `useUnlink` | `true` | 是否用 `UNLINK` 而不是 `DEL` 删除 |
|
|
208
|
+
| `clearBatchSize` | `1000` | `clear()` 每次扫描和删除多少个键 |
|
|
209
|
+
| `noNamespaceAffectsAll` | `false` | 没有命名空间时,`clear()` 是否清空整个数据库(`FLUSHDB`) |
|
|
210
|
+
|
|
211
|
+
- 键的格式是 `{namespace}::{namespace}:{key}`(lakutata 2.x 的格式);没有命名空间时就是键本身。过期的条目由 Redis 自己删除。
|
|
212
|
+
- 没有命名空间时,`clear()` 删除数据库中命名空间之外的字符串键(不含 `::` 的键):命名空间中的键,以及其他类型的键(哈希、列表……),都会保留。设置 `noNamespaceAffectsAll: true` 时,它清空整个数据库。
|
|
213
|
+
- `clear()` 按类型扫描键,这需要 Redis 6 或更高版本。
|
|
214
|
+
- 连接断开期间,命令立即失败,而不是等待:读取算作未命中,写入失败。设置 `throwOnConnectError: false` 时,即使 Redis 没有运行,缓存也能创建,并会继续尝试连接。
|
|
215
|
+
- 存储使用 `redis` 包;没有安装 `redis` 时,使用 `@redis/client`。
|
|
216
|
+
|
|
217
|
+
### Memcache
|
|
218
|
+
|
|
219
|
+
| 选项 | 默认值 | 含义 |
|
|
220
|
+
|---|---|---|
|
|
221
|
+
| `host` | 必填 | |
|
|
222
|
+
| `port` | `11211` | |
|
|
223
|
+
| `username`、`password` | 无 | 用于需要 SASL 认证的服务器 |
|
|
224
|
+
| `namespace` | 无 | |
|
|
225
|
+
|
|
226
|
+
- 客户端在第一条命令时才连接:连不上服务器不会让缓存创建失败,但读取会未命中,写入会失败。
|
|
227
|
+
- memcached 以秒计算 TTL:条目在服务器上最多可能多留两秒,但 TTL 一过就算作未命中。
|
|
228
|
+
- 无论有没有命名空间,`clear()` 都会清空整个服务器:其他应用的条目也会被删除。
|
|
229
|
+
|
|
230
|
+
### MongoDB
|
|
231
|
+
|
|
232
|
+
| 选项 | 默认值 | 含义 |
|
|
233
|
+
|---|---|---|
|
|
234
|
+
| `host`、`database`、`collection` | 必填 | 集合不存在时会自动创建 |
|
|
235
|
+
| `port` | `27017` | |
|
|
236
|
+
| `username`、`password` | 无 | 用户在 `admin` 数据库上认证 |
|
|
237
|
+
| `namespace` | 无 | |
|
|
238
|
+
|
|
239
|
+
- 每个条目是一个文档。缓存创建时,会在键上创建唯一索引,并创建一个 TTL 索引,MongoDB 通过它删除过期的文档,大约每分钟一次;在此之前,过期的文档算作未命中。
|
|
240
|
+
- 连接失败会让缓存创建失败。
|
|
241
|
+
|
|
242
|
+
### SQLite
|
|
243
|
+
|
|
244
|
+
| 选项 | 默认值 | 含义 |
|
|
245
|
+
|---|---|---|
|
|
246
|
+
| `database` | 必填 | 数据库文件,相对于工作目录,所在目录必须已经存在;或者 `':memory:'` |
|
|
247
|
+
| `table` | 必填 | 表,不存在时会自动创建 |
|
|
248
|
+
| `busyTimeout` | `10000` | 数据库被其他连接锁住时,语句最多等待多久,单位毫秒 |
|
|
249
|
+
| `namespace` | 无 | |
|
|
250
|
+
|
|
251
|
+
- `table` 中字母、数字和 `_` 以外的字符会被去掉。
|
|
252
|
+
- 语句同步执行:等待被锁住的数据库时,语句会阻塞事件循环,最长 `busyTimeout`。
|
|
253
|
+
- `better-sqlite3` 是原生插件,针对一种运行时构建:Electron 应用需要使用针对其 Electron 版本构建的 `better-sqlite3`。在 Bun 上,存储改用 `bun:sqlite`。
|
|
254
|
+
|
|
255
|
+
### PostgreSQL
|
|
256
|
+
|
|
257
|
+
| 选项 | 默认值 | 含义 |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| `host`、`database`、`table` | 必填 | |
|
|
260
|
+
| `port` | `5432` | |
|
|
261
|
+
| `schema` | `'public'` | 表所在的模式 |
|
|
262
|
+
| `username`、`password` | 驱动的默认值 | 没有 `username` 时,取环境变量 `PGUSER`,否则取进程的用户 |
|
|
263
|
+
| `maxPoolSize` | `10` | 最大连接数 |
|
|
264
|
+
| `namespace` | 无 | |
|
|
265
|
+
|
|
266
|
+
- 缓存创建时,如果模式和表不存在,会创建它们:`(key VARCHAR(255) PRIMARY KEY, value TEXT)`。PostgreSQL 会把普通的名字转换成小写:`table: 'Entries'` 创建的是 `entries`。连接或创建失败会让缓存创建失败。
|
|
267
|
+
- 键长度超过 255 个字符(包括命名空间)时,写入失败。
|
|
268
|
+
|
|
269
|
+
### MySQL
|
|
270
|
+
|
|
271
|
+
| 选项 | 默认值 | 含义 |
|
|
272
|
+
|---|---|---|
|
|
273
|
+
| `host`、`database`、`table` | 必填 | |
|
|
274
|
+
| `port` | `3306` | |
|
|
275
|
+
| `username`、`password` | 驱动的默认值 | |
|
|
276
|
+
| `namespace` | 无 | |
|
|
277
|
+
|
|
278
|
+
- 缓存创建时,如果表不存在,会创建它:`(id VARCHAR(255) PRIMARY KEY, value MEDIUMTEXT)`,键区分大小写。连接或创建失败会让缓存创建失败。
|
|
279
|
+
- 键长度超过 255 个字符(包括命名空间)时,写入失败;一个值的 JSON 最大 16 MB。
|
|
280
|
+
|
|
281
|
+
## 7. 存储保存的值
|
|
282
|
+
|
|
283
|
+
内存存储保存值本身:对象缓存之后再被修改,缓存中的对象也会变;读取返回的就是缓存的那个对象。需要时,请复制对象。
|
|
284
|
+
|
|
285
|
+
其他存储以 JSON 保存值,所以有些值读回来时会不一样:
|
|
286
|
+
|
|
287
|
+
| 缓存的值 | 读回的值 |
|
|
288
|
+
|---|---|
|
|
289
|
+
| `Date` | 它的 ISO 字符串(`'2026-01-02T03:04:05.000Z'`) |
|
|
290
|
+
| 类的实例 | 带有其字段的普通对象,没有方法 |
|
|
291
|
+
| `Map` 或 `Set` | `{}` |
|
|
292
|
+
| `NaN`、`Infinity` | `null` |
|
|
293
|
+
| `Buffer` | `Buffer` |
|
|
294
|
+
| `bigint` | 写入失败,抛出 `TypeError` |
|
|
295
|
+
|
|
296
|
+
内存存储放在其他存储前面时,同一个键读回的可能是值本身,也可能是它的 JSON 副本,取决于条目在哪个存储中。所以请缓存 JSON 能原样保存的值:普通对象、数组、字符串、数字和布尔值。
|
|
297
|
+
|
|
298
|
+
## 8. 事件与错误
|
|
299
|
+
|
|
300
|
+
| 事件 | 什么时候 | 监听器收到 |
|
|
301
|
+
|---|---|---|
|
|
302
|
+
| `'set'` | `set()` 或 `wrap()` 写入了一个值,或者写入失败 | `{key, value, error?}` |
|
|
303
|
+
| `'del'` | `del()` 删除了一个键,或者删除失败 | `{key, error?}` |
|
|
304
|
+
| `'clear'` | `clear()` 完成 | 无参数,或者错误 |
|
|
305
|
+
| `'refresh'` | `wrap()` 的一次后台刷新完成 | `{key, value, error?}`:新值,或者旧值和错误 |
|
|
306
|
+
| `'error'` | 存储失败,且没有其他事件报告(见下文) | 错误 |
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
import type {Cacher, OnRefreshEventData} from 'lakutata/com/cacher'
|
|
310
|
+
|
|
311
|
+
declare const cache: Cacher
|
|
312
|
+
|
|
313
|
+
cache.on('refresh', (data: OnRefreshEventData): void => {
|
|
314
|
+
if (data.error) console.warn(`Refreshing ${data.key} failed, the cached value is kept`)
|
|
315
|
+
})
|
|
316
|
+
//读取失败、连接断开、文件保存失败……
|
|
317
|
+
cache.on('error', (error: Error): void => console.error('Cache store error', error))
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
- `'error'` 事件报告:读取失败(算作未命中)、`multipleSet()` 和 `multipleDel()` 失败、`wrap()` 复制到上层存储失败、连接断开,以及文件存储保存失败。没有监听器时,这些错误会被丢弃:要知道存储什么时候出了问题,就要监听它。
|
|
321
|
+
- 监听器在事件发出之后异步调用。
|
|
322
|
+
- 没有设置 `nonBlocking` 时,存储写入或删除失败会让调用失败,抛出驱动的错误。设置了 `nonBlocking` 时,`'set'` 和 `'del'` 在写入存储之前就发出,失败时会再发出一次带有错误的事件。
|
|
323
|
+
|
|
324
|
+
| 错误 | 什么时候 |
|
|
325
|
+
|---|---|
|
|
326
|
+
| `InvalidObjectOptionsException` | 选项无效,在应用启动时 |
|
|
327
|
+
| `CacheDriverNotFoundException`(`E_CACHE_DRIVER_NOT_FOUND`) | 存储所需的包没有安装,在缓存创建时 |
|
|
328
|
+
| 驱动的错误 | 缓存创建时存储连不上,或者存储写入、删除失败 |
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Lakutata 命令行工具
|
|
2
|
+
|
|
3
|
+
**`lakutata` 命令**(`@lakutata/cli` 包)用模板创建项目,在工作区的各个包中运行脚本并统一版本号,添加包和原生插件,执行应用的数据库迁移,检查应用的翻译,并把应用打包成单个文件、可执行文件和安装包。全部选项见 `@lakutata/cli` 的 README。
|
|
4
|
+
|
|
5
|
+
## 1. 安装与运行
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @lakutata/cli create --name my-app # 不安装,直接运行
|
|
9
|
+
npm install --global @lakutata/cli # 或者在本机安装一次,之后:lakutata create --name my-app
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
模板会把 `@lakutata/cli` 加入项目的 `devDependencies`,所以项目的脚本可以直接调用 `lakutata`,在项目里也可以用 `npx lakutata`。工具需要 Node.js `^22.13.0 || >=24.11.0`:22.13 及以后的 22.x,或 24.11 及以后的版本(不支持 23)。
|
|
13
|
+
|
|
14
|
+
- `lakutata --help` 列出所有命令,`lakutata help <command>`(或 `lakutata <command> --help`)说明某个命令,`lakutata --version` 输出工具的版本。不带命令运行时,工具输出帮助,并以退出码 1 退出。
|
|
15
|
+
- 选项名用短横线连接(`--package-manager`)。默认开启的布尔选项用 `--no-` 关闭(`--no-git`、`--no-bail`)。
|
|
16
|
+
- 结果输出到标准输出,警告(`! …`)输出到标准错误。失败时在标准错误输出 `error: <消息>`,并以退出码 1 退出。只有在终端里才使用颜色,设置 `NO_COLOR` 可以关闭颜色。
|
|
17
|
+
- **语言**:帮助、交互提问和校验错误支持英文、简体中文和繁体中文,依次根据 `LAKUTATA_LANG`、`LANGUAGE`、`LC_ALL`、`LC_MESSAGES`、`LANG` 和系统语言选择(`LAKUTATA_LANG=zh-CN lakutata --help`)。命令名、选项名以及工具的其他消息仍是英文。
|
|
18
|
+
|
|
19
|
+
## 2. 项目
|
|
20
|
+
|
|
21
|
+
### `lakutata create`
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
lakutata create --name my-app # 默认模板,创建在 ./my-app
|
|
25
|
+
lakutata create --name shop --template service --with i18n,installer --yes
|
|
26
|
+
lakutata create --name tool --template cli --package-manager none --no-git
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| 选项 | 默认值 | 含义 |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `--name` | 在终端里会询问 | 应用的名称,也是项目目录的名称 |
|
|
32
|
+
| `--id` | 名称的小写形式 | 应用的 ID,也是包名 |
|
|
33
|
+
| `--template` | `default` | 官方模板(见 `lakutata templates`)、`npm:<package>[@<version>]`、`github:<owner>/<repo>[#<ref>]`,或一个目录 |
|
|
34
|
+
| `--path` | `.` | 在哪里创建项目目录(`<path>/<name>`) |
|
|
35
|
+
| `--init` | | 直接在 `--path` 这个已有目录中创建项目 |
|
|
36
|
+
| `--description`、`--author`、`--license` | 许可证为 `UNLICENSED` | 包的信息 |
|
|
37
|
+
| `--with` | | 模板的可选功能,用逗号分隔(`i18n`、`installer`) |
|
|
38
|
+
| `--package-manager` | `npm`;由 pnpm 或 yarn 启动工具时为对应的包管理器 | `npm`、`pnpm`、`yarn`,或 `none` 表示不安装依赖 |
|
|
39
|
+
| `--no-git` | | 不创建 git 仓库 |
|
|
40
|
+
| `--yes` | | 不询问可选功能,也不要求确认 |
|
|
41
|
+
|
|
42
|
+
在终端里运行时,工具会询问缺少的名称,列出模板的可选功能供选择,展示将要创建的内容并请求确认。之后它复制模板,填入名称、ID 和包的其他信息,安装依赖(`<包管理器> install`),再执行 `git init`(不提交)。官方模板取与工具主版本号相同的最新正式版,从 npm 仓库下载,并保存在工具的缓存中;没有网络时,如果以前下载过,就使用缓存中的模板。
|
|
43
|
+
|
|
44
|
+
使用 `--init` 时,目录中已有的文件会保留:模板中同名的文件不会复制过去,工具会发出警告列出这些文件,需要的内容请手动合并(`package.json` 主要是其中的脚本和依赖)。
|
|
45
|
+
|
|
46
|
+
### `lakutata templates`、`info` 和 `clean`
|
|
47
|
+
|
|
48
|
+
- `lakutata templates` 列出官方模板及其可选功能。列表来自 npm 仓库,缓存一天(`--refresh` 会重新获取)。模板里有什么见 [ProjectStructure](ProjectStructure.md)。
|
|
49
|
+
- `lakutata info` 显示工具、框架(能访问仓库时还显示最新版本)和 Node.js 的版本,以及当前目录的项目所用的框架版本。
|
|
50
|
+
- `lakutata clean [paths...]` 在任何平台上删除当前目录所在包中的路径;不给路径时,删除其 `tsconfig.json` 的 `outDir` 和 `declarationDir`。它拒绝删除包以外的路径,以及 `src`、`node_modules`、`package.json` 和 `.git`。模板在 `build` 或 `rebuild` 脚本中调用它。
|
|
51
|
+
|
|
52
|
+
## 3. 工作区与原生插件
|
|
53
|
+
|
|
54
|
+
`lakutata list`、`run`、`version` 和 `add` 作用于工作区,即包含多个包的仓库:它们列出工作区的包,按依赖顺序在每个包中运行脚本,给所有包统一版本号,并添加新的包。见 [Workspaces](Workspaces.md)。
|
|
55
|
+
|
|
56
|
+
`lakutata add <name> --native gyp|cmake|rust` 添加一个用 C++ 或 Rust 编写的原生插件,`lakutata native <link|include|define> <value>` 给它链接库。见 [NativeAddons](NativeAddons.md)。
|
|
57
|
+
|
|
58
|
+
## 4. 迁移与翻译
|
|
59
|
+
|
|
60
|
+
`lakutata migration <action>` 生成、创建、执行、回滚和查看应用的数据库迁移。它用应用的 `dev` 脚本构建并启动应用,此时应用只连接数据库。见 [Migrations](Migrations.md) 第 2 节。
|
|
61
|
+
|
|
62
|
+
`lakutata i18n <action>` 检查应用各语言的消息,生成键名的类型声明,以及添加新语言。见 [I18n](I18n.md) 的“命令行工具”一节。
|
|
63
|
+
|
|
64
|
+
## 5. 打包
|
|
65
|
+
|
|
66
|
+
`lakutata package bundle` 和 `package exe` 把编译好的应用打包成单个 JavaScript 文件,或者不需要安装 Node.js 就能运行的可执行文件,见 [Packaging](Packaging.md)。`lakutata package installer` 制作应用的安装包,安装包还可以把应用注册为服务,见 [Installers](Installers.md)。
|
|
67
|
+
|
|
68
|
+
## 6. 环境变量
|
|
69
|
+
|
|
70
|
+
| 变量 | 含义 |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `LAKUTATA_LANG` | 工具的语言(`en`、`zh-CN`、`zh-TW`) |
|
|
73
|
+
| `LAKUTATA_NPM_REGISTRY` | 下载模板的仓库(否则取 `npm_config_registry`,再否则为 `https://registry.npmjs.org`) |
|
|
74
|
+
| `LAKUTATA_GITHUB_DOWNLOAD` | 下载 `github:` 模板的地址(默认 `https://github.com`) |
|
|
75
|
+
| `LAKUTATA_CACHE_DIR` | 工具的缓存目录:模板,以及制作安装包所用的工具(默认 `$XDG_CACHE_HOME/lakutata`,否则 `~/.cache/lakutata`) |
|
|
76
|
+
| `NO_COLOR` | 关闭颜色 |
|
|
77
|
+
|
|
78
|
+
工具运行的脚本和应用,拿到的是工具启动时的环境变量。打包后的应用所用的环境变量见 [Packaging](Packaging.md) 第 6 节。
|