lakutata 3.0.0 → 3.1.1
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 +511 -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 +63 -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 +508 -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/WebRTC.md +1364 -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/Meeting.md +573 -0
- package/doc/en/examples/README.md +17 -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 +446 -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 +47 -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 +462 -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/WebRTC.md +1049 -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/Meeting.md +524 -0
- package/doc/zh/examples/README.md +15 -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/ActionPattern.md
DELETED
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
# Lakutata 控制器与动作
|
|
2
|
-
|
|
3
|
-
`Entrypoint` 组件(`lakutata/com/entrypoint`,`@lakutata/entrypoint` 包)通过多种传输方式对外提供控制器的方法,即**动作**。动作的地址可以是 HTTP 路由、CLI 命令、服务模式或定时计划,同一个方法可以同时拥有多个地址:无论通过哪种传输方式调用,它的输入都以相同的方式经过 DTO 校验、访问控制和中间件。更多细节见 `@lakutata/entrypoint` 的 README,以及 `lakutata/com/entrypoint` 和 `lakutata/decorator/ctrl` 的类型声明。
|
|
4
|
-
|
|
5
|
-
## 1. 控制器
|
|
6
|
-
|
|
7
|
-
控制器继承 `Controller`(`lakutata/com/entrypoint`),它是一个生命周期为 `@Scoped`(锁定)的提供者:每次请求都会在该请求的作用域中创建自己的控制器,请求结束时销毁;控制器注入的提供者也属于该请求的作用域。
|
|
8
|
-
|
|
9
|
-
- `this.context` 是请求的上下文:`HTTPContext`(含 `request`、`response`、`route`、`method`)、`CLIContext`(含 `command`)、`ServiceContext`(含 `clientId`)或 `ScheduleContext`;可通过类或 `type` 区分。
|
|
10
|
-
- `this.signal` 是一个 `AbortSignal`,在请求被中止时触发:客户端断开、超过截止时间或入口正在停止。耗时较长的动作应随之停止,因为它的结果会被丢弃。
|
|
11
|
-
- 方法接收的输入(请求的数据,经动作的 DTO 校验,填充默认值并移除未知字段)是一个普通对象,而不是 DTO 实例。没有 DTO 时,输入原样传入。
|
|
12
|
-
- 返回值就是响应;抛出的异常以其 `statusCode`(HTTP)以及 `errno` 和消息作为响应,输入不合法时返回状态码 400。
|
|
13
|
-
|
|
14
|
-
```typescript
|
|
15
|
-
import {Application, DTO} from 'lakutata'
|
|
16
|
-
import {buildEntrypoints, Controller, scheduleEntrypoint} from 'lakutata/com/entrypoint'
|
|
17
|
-
import {CLIAction, GET, POST, Schedule, ServiceAction} from 'lakutata/decorator/ctrl'
|
|
18
|
-
import {Expect} from 'lakutata/decorator/dto'
|
|
19
|
-
import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
|
|
20
|
-
import {commanderCLIEntrypoint} from 'lakutata/adapter/commander'
|
|
21
|
-
import {socketIOServiceEntrypoint} from 'lakutata/adapter/socket.io'
|
|
22
|
-
|
|
23
|
-
class GetOrderDTO extends DTO {
|
|
24
|
-
//路由参数和查询参数都是字符串:strict(false) 会转换它们
|
|
25
|
-
@Expect(DTO.Number().integer().strict(false).required())
|
|
26
|
-
public id: number
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
class CreateOrderDTO extends DTO {
|
|
30
|
-
@Expect(DTO.String().required())
|
|
31
|
-
public product: string
|
|
32
|
-
|
|
33
|
-
@Expect(DTO.Number().integer().min(1).default(1))
|
|
34
|
-
public quantity: number
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
class OrderController extends Controller {
|
|
38
|
-
//GET /orders/42、命令 "order --id 42"、服务消息 {cmd: 'order.get', id: 42}
|
|
39
|
-
@GET('/orders/:id', GetOrderDTO)
|
|
40
|
-
@CLIAction('order', GetOrderDTO)
|
|
41
|
-
@ServiceAction({cmd: 'order.get'}, GetOrderDTO)
|
|
42
|
-
public async get(input: GetOrderDTO): Promise<{ id: number }> {
|
|
43
|
-
return {id: input.id}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
//POST /orders,请求体为 JSON
|
|
47
|
-
@POST('/orders', CreateOrderDTO)
|
|
48
|
-
public async create(input: CreateOrderDTO): Promise<{ product: string, quantity: number }> {
|
|
49
|
-
return input
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
//每 5 分钟执行一次
|
|
53
|
-
@Schedule('5m')
|
|
54
|
-
public async report(): Promise<void> {
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
Application.run(() => ({
|
|
59
|
-
id: 'orders.app',
|
|
60
|
-
name: 'Orders',
|
|
61
|
-
components: {
|
|
62
|
-
entrypoint: buildEntrypoints({
|
|
63
|
-
controllers: [OrderController],
|
|
64
|
-
http: expressHTTPEntrypoint({port: 8080}),
|
|
65
|
-
cli: commanderCLIEntrypoint(),
|
|
66
|
-
service: socketIOServiceEntrypoint({port: 8081}),
|
|
67
|
-
schedule: scheduleEntrypoint()
|
|
68
|
-
})
|
|
69
|
-
},
|
|
70
|
-
bootstrap: ['entrypoint']
|
|
71
|
-
}))
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## 2. 动作装饰器(`lakutata/decorator/ctrl`)
|
|
75
|
-
|
|
76
|
-
每个装饰器的参数依次为:地址,可选的输入 DTO 类,可选的动作选项(`acl`、`name`、`description`、`groups`、`rule`、`middlewares`)。
|
|
77
|
-
|
|
78
|
-
### HTTP:`@HTTPAction`、`@GET`、`@POST`、`@PUT`、`@PATCH`、`@DELETE`、`@HEAD`、`@OPTIONS`
|
|
79
|
-
|
|
80
|
-
```text
|
|
81
|
-
@HTTPAction(route, method | methods, DTOClass?, options?)
|
|
82
|
-
@GET(route, DTOClass?, options?)
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
- 路由使用 HTTP 适配器的语法(`/users/:id`);方法不区分大小写(`'get'`、`['get', 'post']`)。
|
|
86
|
-
- 输入由查询参数和解析后的请求体(JSON 或 URL 编码)合并而成,路由参数覆盖在其上。查询参数和路由参数都是字符串,而校验规则是严格的:用 `.strict(false)` 进行转换。
|
|
87
|
-
- 返回值以 JSON 响应(`undefined` 时为 204)。`lakutata/com/entrypoint` 中的 `Result.status(201, data, headers)`、`Result.file(path)` 和 `Result.events(producer)`(Server-Sent Events)用于控制响应。
|
|
88
|
-
|
|
89
|
-
### CLI:`@CLIAction`
|
|
90
|
-
|
|
91
|
-
```text
|
|
92
|
-
@CLIAction(command, DTOClass?, options?)
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
- 命令由名称和可选的位置参数组成(`'copy <source> [target]'`、`'<files...>'`);名称前面的词是命令分组(`'package exe <file>'`)。DTO 的其他属性是选项(`--times 2`),布尔属性是开关(`--shout`);校验规则的 `.description()` 是选项的帮助文本,动作选项 `description` 是命令的帮助文本。
|
|
96
|
-
- 返回值写到 stdout(字符串原样输出,其他值输出为 JSON);错误写到 stderr,命令以退出码 1 结束。
|
|
97
|
-
|
|
98
|
-
### 服务:`@ServiceAction`
|
|
99
|
-
|
|
100
|
-
```text
|
|
101
|
-
@ServiceAction(pattern, DTOClass?, options?)
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
- 模式是一个对象(最多嵌套 2 层),消息必须包含这些键且值相同;字符串值可以是通配符(`'user.*'`)。匹配的模式中最具体的一个胜出。输入是去掉模式中各键后的消息。
|
|
105
|
-
- 调用方收到 `{success: true, error: null, payload}`,或 `{success: false, error, payload: null}`,其中 `error` 包含错误的各属性(`errno`、`errMsg`、`statusCode`、`message` 等);JSON-RPC 则以它自己的 `result` 和 `error` 响应。没有模式匹配的消息会以 `ControllerActionNotFoundException` 失败。
|
|
106
|
-
|
|
107
|
-
### 定时任务:`@Schedule`
|
|
108
|
-
|
|
109
|
-
```text
|
|
110
|
-
@Schedule(schedule, options?)
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
当入口配置了定时适配器(`schedule: scheduleEntrypoint()`)时,方法按计划执行。计划可以是时长(`'30s'`、`'1h30m'`,表示间隔)、cron 表达式(`'0 */5 * * * *'`,5 或 6 个字段,或 `'@daily'` 这样的宏),或者是 `{every}`、`{cron, timezone}` 形式的选项,并可附带:
|
|
114
|
-
|
|
115
|
-
- `overlap`(默认 `false`):上一次执行尚未结束时也开始新的执行;否则跳过本次;
|
|
116
|
-
- `immediate`(默认 `false`):应用启动后立即额外执行一次;
|
|
117
|
-
- `exclusive`(默认 `false`):借助互斥锁组件,同一时间只在应用的一个实例中执行(`true` 使用名为 `mutex` 的组件,`{mutex, key}` 指定组件名和锁名);见 [Mutex](Mutex.md)。
|
|
118
|
-
|
|
119
|
-
每次执行都有自己的作用域,并经过中间件和访问控制;方法接收空输入,`this.context` 是 `ScheduleContext`(含 `schedule`、计划时间、执行序号 `run`)。抛出的错误交给 `scheduleEntrypoint` 的 `onError` 选项,或记录到日志,后续执行照常进行。
|
|
120
|
-
|
|
121
|
-
```typescript
|
|
122
|
-
import {Controller, ScheduleContext} from 'lakutata/com/entrypoint'
|
|
123
|
-
import {Schedule} from 'lakutata/decorator/ctrl'
|
|
124
|
-
|
|
125
|
-
class JobController extends Controller {
|
|
126
|
-
//每 30 秒一次,并在应用启动后立即执行一次
|
|
127
|
-
@Schedule({every: '30s', immediate: true})
|
|
128
|
-
public async heartbeat(): Promise<void> {
|
|
129
|
-
if (this.context instanceof ScheduleContext) console.log(`run ${this.context.run} of ${this.context.schedule}`)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
//上海时间每天 03:00,同一时间只在一个实例中执行(需要名为 "mutex" 的互斥锁组件)
|
|
133
|
-
@Schedule({cron: '0 3 * * *', timezone: 'Asia/Shanghai', exclusive: true}, {name: 'Purge the expired sessions'})
|
|
134
|
-
public async purge(): Promise<void> {
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### 前缀:`@Prefix`
|
|
140
|
-
|
|
141
|
-
在控制器类上使用 `@Prefix({route, command, pattern})` 为它的动作添加前缀:`route` 加在 HTTP 路由之前,`command` 加在 CLI 命令之前(以 `:` 连接,`user` 和 `get` 得到 `user:get`),`pattern` 合并到服务模式中。
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
import {DTO} from 'lakutata'
|
|
145
|
-
import {Controller} from 'lakutata/com/entrypoint'
|
|
146
|
-
import {CLIAction, GET, Prefix, ServiceAction} from 'lakutata/decorator/ctrl'
|
|
147
|
-
import {Expect} from 'lakutata/decorator/dto'
|
|
148
|
-
|
|
149
|
-
class UserIdDTO extends DTO {
|
|
150
|
-
@Expect(DTO.String().required())
|
|
151
|
-
public id: string
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
@Prefix({route: '/users', command: 'user', pattern: {resource: 'user'}})
|
|
155
|
-
class UserController extends Controller {
|
|
156
|
-
//GET /users/:id、命令 "user:get <id>"、消息 {resource: 'user', act: 'get', id}
|
|
157
|
-
@GET('/:id', UserIdDTO)
|
|
158
|
-
@CLIAction('get <id>', UserIdDTO)
|
|
159
|
-
@ServiceAction({act: 'get'}, UserIdDTO)
|
|
160
|
-
public async get(input: UserIdDTO): Promise<{ id: string }> {
|
|
161
|
-
return {id: input.id}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
## 3. 中间件与访问控制
|
|
167
|
-
|
|
168
|
-
中间件包裹在动作外围执行,早于输入校验和访问控制:可以是函数 `(invocation, next) => Promise<unknown>`,也可以是 `Middleware` 类。可以把它们交给 `buildEntrypoints({middlewares})`(作用于所有动作)、适配器的 `middlewares` 选项、控制器类或方法上的 `@UseMiddlewares(...)`,以及动作选项 `middlewares`。
|
|
169
|
-
|
|
170
|
-
```typescript
|
|
171
|
-
import {type ActionInvocation, Controller, type Next} from 'lakutata/com/entrypoint'
|
|
172
|
-
import {GET, UseMiddlewares} from 'lakutata/decorator/ctrl'
|
|
173
|
-
|
|
174
|
-
async function timing(invocation: ActionInvocation, next: Next): Promise<unknown> {
|
|
175
|
-
const started: number = Date.now()
|
|
176
|
-
try {
|
|
177
|
-
return await next()
|
|
178
|
-
} finally {
|
|
179
|
-
console.log(`${invocation.context.type} took ${Date.now() - started} ms`)
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
@UseMiddlewares(timing)
|
|
184
|
-
class HealthController extends Controller {
|
|
185
|
-
@GET('/health')
|
|
186
|
-
public async health(): Promise<string> {
|
|
187
|
-
return 'ok'
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
声明了 `{acl: true}` 的动作由 `buildEntrypoints({rules})` 中的规则检查:规则是 `AccessControlRule` 的子类,每次请求都会创建,其 `rule()` 返回 `false` 时以 `AccessDeniedException`(HTTP 403)拒绝请求。动作选项 `rule` 是该动作自己的规则。
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
import {AccessControlRule, ContextType, Controller, type HTTPContext} from 'lakutata/com/entrypoint'
|
|
196
|
-
import {DELETE} from 'lakutata/decorator/ctrl'
|
|
197
|
-
|
|
198
|
-
class AdminRule extends AccessControlRule<HTTPContext> {
|
|
199
|
-
public get supportContextTypes(): ContextType[] {
|
|
200
|
-
return [ContextType.HTTP]
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
public async rule(): Promise<boolean> {
|
|
204
|
-
if (!this.targetAction.groups.includes('admin')) return true
|
|
205
|
-
return this.context.request.headers['x-role'] === 'admin'
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
class UserController extends Controller {
|
|
210
|
-
@DELETE('/users/:id', {acl: true, groups: ['admin']})
|
|
211
|
-
public async remove(input: { id: string }): Promise<void> {
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
//buildEntrypoints({controllers: [UserController], rules: [AdminRule], httpActionGroups: {admin: 'Administration'}})
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
动作的分组必须在 `buildEntrypoints` 的 `httpActionGroups`、`cliActionGroups` 或 `serviceActionGroups` 中声明。`shutdownTimeout`(默认 30000 毫秒)是入口停止时留给正在处理的请求的时间。
|
|
219
|
-
|
|
220
|
-
## 4. 传输方式
|
|
221
|
-
|
|
222
|
-
| `buildEntrypoints` 的选项 | 适配器 | 子路径 | 需安装 |
|
|
223
|
-
|---|---|---|---|
|
|
224
|
-
| `http` | `expressHTTPEntrypoint({port})` | `lakutata/adapter/express` | `express` |
|
|
225
|
-
| `http` | `fastifyHTTPEntrypoint({port})` | `lakutata/adapter/fastify` | `fastify` |
|
|
226
|
-
| `cli` | `commanderCLIEntrypoint()` | `lakutata/adapter/commander` | `commander`(交互提示需要 `@inquirer/prompts`) |
|
|
227
|
-
| `service` | `socketIOServiceEntrypoint({port})` | `lakutata/adapter/socket.io` | `socket.io` |
|
|
228
|
-
| `service` | `jsonRPCServiceEntrypoint()`(HTTP、WebSocket、stdio) | `lakutata/adapter/jsonrpc` | WebSocket 需要 `ws` |
|
|
229
|
-
| `service` | `electronIPCServiceEntrypoint()` | `lakutata/adapter/electron` | (Electron) |
|
|
230
|
-
| `service` | `httpServiceEntrypoint({port})`(`POST /`,事件通过 SSE 推送) | `lakutata/com/entrypoint` | |
|
|
231
|
-
| `service` | `grpcServiceEntrypoint({port})` | `@lakutata/grpc/service` | `@lakutata/grpc` |
|
|
232
|
-
| `service` | `natsServiceEntrypoint('nats')` | `@lakutata/nats` | `@lakutata/nats` |
|
|
233
|
-
| `schedule` | `scheduleEntrypoint()` | `lakutata/com/entrypoint` | |
|
|
234
|
-
|
|
235
|
-
每个选项可以是一个适配器或适配器数组。`lakutata/com/entrypoint` 中的 `buildHTTPEntrypoint`、`buildCLIEntrypoint` 和 `buildServiceEntrypoint` 用于以 lakutata 2.x 的风格编写入口函数,以接入其他库;`EntrypointAdapter`(来自 `@lakutata/entrypoint/channel`)是适配器的接口。
|
|
236
|
-
|
|
237
|
-
### 从 2.x 的入口函数迁移到 HTTP 适配器
|
|
238
|
-
|
|
239
|
-
2.x 的 HTTP 入口函数(`buildHTTPEntrypoint`)仍然可以使用:它在入口的启动阶段被调用(此时它获取的对象,比如注入了入口的 Authenticator,都可以正常获取),并且会被等待,所以应用在它的服务开始监听之后才算启动完成。迁移到 `expressHTTPEntrypoint` 时:
|
|
240
|
-
|
|
241
|
-
- 应用自己的中间件和路由(CORS、压缩、健康检查、Authenticator、Web UI)放进 `before` 钩子,钩子里用 `module.getObject()` 获取对象;请求体解析用 `body` 选项(`{limit: '100mb'}`);服务器的 `keepAliveTimeout`/`headersTimeout` 用 `server` 选项;
|
|
242
|
-
- 从请求构造动作输入(查询参数、请求体、会话中的值)用 `input` 选项;
|
|
243
|
-
- `{errno, errMsg, data}` 封套用 `jsonEnvelope()`。2.x 的应用通常对动作的错误返回 200,而对 Express 中间件的错误(`next(error)`,比如 Authenticator 的 401)使用错误自带的状态码:`jsonEnvelope({actionErrorStatus: 200})` 的行为与此相同。如果用 `errorStatus: 200`,401 也会变成 200,按 401 跳转登录页的前端就会失效;
|
|
244
|
-
- 应用封套里的其他字段(`timestamp`、`nonce`、请求 ID)用 `extra` 选项给出。它在每次响应时调用,成功和错误的响应都会带上这些字段:`jsonEnvelope({actionErrorStatus: 200, extra: () => ({timestamp: Date.now()})})`。这些字段排在 `errno`、`errMsg`、`data` 之后,不会覆盖这三个字段,因此不需要为了保留它们自己实现响应器。
|
|
245
|
-
|
|
246
|
-
```typescript
|
|
247
|
-
import {buildEntrypoints} from 'lakutata/com/entrypoint'
|
|
248
|
-
import {expressHTTPEntrypoint, jsonEnvelope} from 'lakutata/adapter/express'
|
|
249
|
-
import type {Express, NextFunction, Request, Response} from 'express'
|
|
250
|
-
|
|
251
|
-
class Unauthorized extends Error {
|
|
252
|
-
public readonly errno: string = 'E_UNAUTHORIZED'
|
|
253
|
-
|
|
254
|
-
public readonly statusCode: number = 401
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export const entrypoint = buildEntrypoints({
|
|
258
|
-
controllers: [],
|
|
259
|
-
http: expressHTTPEntrypoint({
|
|
260
|
-
port: 3000,
|
|
261
|
-
body: {limit: '100mb'},
|
|
262
|
-
before: async (app: Express): Promise<void> => {
|
|
263
|
-
app.get('/health', (_request: Request, response: Response): unknown => response.json({status: 'ok'}))
|
|
264
|
-
//请求的鉴权:它的错误保留自带的状态码(401)
|
|
265
|
-
app.use((request: Request, _response: Response, next: NextFunction): void => request.headers.authorization ? next() : next(new Unauthorized('Not logged in')))
|
|
266
|
-
},
|
|
267
|
-
//动作的错误在封套中以 200 返回,与 2.x 相同,并带上时间戳
|
|
268
|
-
respond: jsonEnvelope({actionErrorStatus: 200, extra: (): Record<string, unknown> => ({timestamp: Date.now()})})
|
|
269
|
-
})
|
|
270
|
-
})
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
`fastifyHTTPEntrypoint` 的选项相同(使用 `fastifyJSONEnvelope({actionErrorStatus: 200, extra})`,Authenticator 写成 Fastify 的钩子)。自定义的响应器可以从 `error` 的第四个参数得到每个错误的来源(`'action'` 或 `'request'`)。
|
|
274
|
-
|
|
275
|
-
### 服务动作的客户端与推送事件
|
|
276
|
-
|
|
277
|
-
带客户端的服务适配器会把请求所属客户端的 id 放入上下文的 `ServiceContext.clientId`:`electronIPCServiceEntrypoint`(窗口中的每个页面)、`socketIOServiceEntrypoint`(每个 socket,连接时通过 `auth.id` 或 `id` 请求头给出 id)、`httpServiceEntrypoint`(`POST` 请求的 `id` 请求头,事件流 `GET /events` 的 `id` 请求头或查询参数)、WebSocket 和 stdio 上的 `jsonRPCServiceEntrypoint`,以及 `grpcServiceEntrypoint`(`id` 元数据)。入口组件用 `notify(clientId, event, ...args)` 向某个客户端推送事件(返回是否有客户端收到),用 `broadcast(event, ...args)` 向所有客户端推送(返回客户端数量):
|
|
278
|
-
|
|
279
|
-
```typescript
|
|
280
|
-
import {Controller, Entrypoint, ServiceContext} from 'lakutata/com/entrypoint'
|
|
281
|
-
import {ServiceAction} from 'lakutata/decorator/ctrl'
|
|
282
|
-
import {Inject} from 'lakutata/decorator/di'
|
|
283
|
-
|
|
284
|
-
class TaskController extends Controller {
|
|
285
|
-
@Inject('entrypoint')
|
|
286
|
-
protected readonly entrypoint: Entrypoint
|
|
287
|
-
|
|
288
|
-
@ServiceAction({cmd: 'start'})
|
|
289
|
-
public async start(): Promise<void> {
|
|
290
|
-
const clientId: string | undefined = (this.context as ServiceContext).clientId
|
|
291
|
-
if (clientId) setTimeout((): void => void this.entrypoint.notify(clientId, 'done', {task: 1}), 1000)
|
|
292
|
-
this.entrypoint.broadcast('started', {task: 1})
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
Electron 的渲染进程用 `window.ipc.on(event, listener)` 监听事件(Electron 模板中提供),socket 用 `socket.on(event, listener)`,HTTP 客户端通过 `GET /events` 的 Server-Sent Events 监听(`EventSource`、curl)。窗口中的页面在重新加载或窗口关闭之前都是同一个客户端。
|
|
298
|
-
|
|
299
|
-
### JSON-RPC 2.0
|
|
300
|
-
|
|
301
|
-
`jsonRPCServiceEntrypoint` 通过 JSON-RPC 2.0 提供服务动作:请求的 method 和(按名称给出的)params 就是动作的模式和输入,`{"method": "order.get", "params": {"id": 1}}` 会被分发为 `{method: 'order.get', id: 1}`(`methodField` 指定字段名,`toPattern(method, params)` 把它们映射为其他模式,`positionalParams` 接受按位置给出的 params)。批量请求和通知遵循规范;错误码为 `-32601`(没有匹配的动作)、`-32602`(DTO 拒绝了输入)和 `-32000`(动作抛出的错误,完整的错误对象放在 `data` 中)。传输方式有 HTTP(`POST`)、WebSocket(`ws` 包)和 stdio(按行或按 `Content-Length` 头分帧);后两者以通知的形式推送客户端事件。在 WebSocket 上,不响应 ping 的连接会被关闭(`heartbeat`,30 秒);在 stdio 上,输入结束后应用退出(`end: 'application'`)。
|
|
302
|
-
|
|
303
|
-
```text
|
|
304
|
-
service: [
|
|
305
|
-
jsonRPCServiceEntrypoint({port: 4000, path: '/rpc'}),
|
|
306
|
-
jsonRPCServiceEntrypoint({transport: 'websocket', port: 4001}),
|
|
307
|
-
jsonRPCServiceEntrypoint({transport: 'stdio', framing: 'content-length'})
|
|
308
|
-
]
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
### gRPC 与 NATS
|
|
312
|
-
|
|
313
|
-
`grpcServiceEntrypoint`(`@lakutata/grpc/service`)通过 gRPC 提供服务动作:服务 `lakutata.service.v1.Service`(`SERVICE_PROTO`,供其他语言的客户端使用)以模式和输入的 JSON 调用动作,并以流的形式推送客户端的事件。用 `buildGRPCServiceProxy` 声明的 `GRPCServiceProxy` 调用远程应用的动作并监听其事件。调用的截止时间到达时会中止对应的动作(`this.signal`)。详见 `@lakutata/grpc` 的 README。
|
|
314
|
-
|
|
315
|
-
```typescript
|
|
316
|
-
import {Application} from 'lakutata'
|
|
317
|
-
import {buildGRPCServiceProxy, type GRPCServiceProxy} from '@lakutata/grpc/service'
|
|
318
|
-
|
|
319
|
-
Application
|
|
320
|
-
.run(() => ({
|
|
321
|
-
id: 'shop.app',
|
|
322
|
-
name: 'Shop',
|
|
323
|
-
components: {
|
|
324
|
-
orders: buildGRPCServiceProxy({address: 'orders.internal:50051'})
|
|
325
|
-
}
|
|
326
|
-
}))
|
|
327
|
-
.onLaunched(async (app: Application): Promise<void> => {
|
|
328
|
-
const orders: GRPCServiceProxy = await app.getObject<GRPCServiceProxy>('orders')
|
|
329
|
-
orders.on('orderChanged', (order: { id: number }): void => console.log(order))
|
|
330
|
-
console.log(await orders.invoke({cmd: 'order.get', id: 1}))
|
|
331
|
-
})
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
`natsServiceEntrypoint`(`@lakutata/nats`)通过 NATS 提供服务动作,订阅以应用 id 为名的主题,各实例共享同一个队列组;用 `buildServiceProxy` 声明的 `ServiceProxy` 调用它们。详见 `@lakutata/nats` 的 README。
|
package/doc/zh/Core.md
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
# Lakutata 核心
|
|
2
|
-
|
|
3
|
-
本指南介绍框架的核心(`@lakutata/core` 包,由 `lakutata`、`lakutata/decorator/di`、`lakutata/decorator/dto` 和 `lakutata/decorator/asst` 重新导出):对象及其容器、生命周期类型与作用域、异步构造、依赖注入、模块以及应用的生命周期。更多细节见 `@lakutata/core` 的 README 和各导出的类型声明。
|
|
4
|
-
|
|
5
|
-
## 1. 对象与容器
|
|
6
|
-
|
|
7
|
-
应用中的每个对象都由 IoC 容器创建,从不使用 `new`。类的继承关系如下:
|
|
8
|
-
|
|
9
|
-
| 类 | 作用 | 默认生命周期 |
|
|
10
|
-
|---|---|---|
|
|
11
|
-
| `BaseObject` | 根类:异步构造、`init()`/`destroy()`、`getObject()` | `@Transient` |
|
|
12
|
-
| `Provider` | 存在于某个作用域(如一次请求)中的功能单元(仓储、哈希器等) | `@Scoped` |
|
|
13
|
-
| `Component` | 整个应用共享的长期服务(日志、数据库、入口等),同时是事件发射器 | `@Singleton`(锁定) |
|
|
14
|
-
| `Module` | 拥有自己的组件、提供者和对象的组件 | `@Singleton`(锁定) |
|
|
15
|
-
| `Application` | 根模块,由 `Application.run()` 创建 | |
|
|
16
|
-
| `Controller`(`lakutata/com/entrypoint`) | 处理入口请求的提供者 | `@Scoped`(锁定) |
|
|
17
|
-
|
|
18
|
-
### 生命周期类型
|
|
19
|
-
|
|
20
|
-
| 装饰器(`lakutata/decorator/di`) | 实例数量 |
|
|
21
|
-
|---|---|
|
|
22
|
-
| `@Singleton()` | 每个注册该类的容器一个,由它的使用者和它的作用域共享 |
|
|
23
|
-
| `@Scoped()` | 每个作用域一个(`container.createScope()`;入口的每次请求是一个作用域,根容器本身也是一个作用域) |
|
|
24
|
-
| `@Transient()` | 每次解析都新建,从不缓存 |
|
|
25
|
-
|
|
26
|
-
子类继承父类的生命周期类型。`Component` 和 `Module` 锁定了自己的类型:子类声明其他类型会抛出 `LifetimeLockedException`;`Provider` 默认是 `@Scoped`,也可以声明为 `@Transient` 或 `@Singleton`(与 2.x 相同:一个实例由各作用域共享,例如 `@lakutata/nats` 的 `ServiceProxy`);长期持有资源(连接、订阅、服务器)的对象更适合用 `Component`。单例注入作用域对象时,拿到的是创建它的那个作用域的实例。`@Lifetime('SINGLETON' | 'SCOPED' | 'TRANSIENT', lock)` 是这三个装饰器的完整写法。
|
|
27
|
-
|
|
28
|
-
### 作用域
|
|
29
|
-
|
|
30
|
-
作用域是一个子容器,持有自己的作用域对象实例,父容器的单例和注册项则是共享的。入口为每次请求创建一个作用域:该请求的控制器以及它注入的提供者都在其中创建,请求结束时一并销毁。组件可以通过受保护的 `createScope()` 创建自己的作用域。
|
|
31
|
-
|
|
32
|
-
## 2. 异步构造
|
|
33
|
-
|
|
34
|
-
容器以异步方式创建对象,`getObject()`(或 `@Inject`)返回的 Promise 在对象就绪后才完成:
|
|
35
|
-
|
|
36
|
-
1. 解析它的 `@Inject` 属性;
|
|
37
|
-
2. 为它的 `@Configurable` 属性赋予配置值,并由各自的校验规则校验(及转换);未配置的属性保留其初始值,没有初始值时取校验规则的默认值;
|
|
38
|
-
3. 执行它的 `init()`(在此抛出的错误会导致创建失败);
|
|
39
|
-
4. 在它可以被其他对象获取之后,执行它的 `startup()`:获取它的一方会等待 `startup()` 完成(失败时得到同样的错误);但在 `startup()` 内部、直接或经由它所创建的对象去获取的对象,会立即拿到这个对象,所以它获取的对象可以反过来注入它(入口在这里启动各个适配器,适配器的钩子会获取注入了入口的 Authenticator)。之所以叫 `startup` 而不叫 `start`,是为了避免自带 `start()` 方法的类(如 `@lakutata/docker` 的容器)被框架自动启动;
|
|
40
|
-
5. 应用启动完成(且对象已启动)后,执行它的 `launched()`(不会被等待,其错误会被记录到日志)。
|
|
41
|
-
|
|
42
|
-
销毁时(调用 `dispose()`,或创建它的对象、作用域、应用被销毁时),先等待 `startup()` 结束,再执行它的 `beforeDestroy()`,再销毁它创建的对象,最后执行它的 `destroy()`。每个对象都拥有自己的容器:通过 `this.getObject()` 创建的对象会随它一起销毁。
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
import {Application, Component, DTO, type ILogger} from 'lakutata'
|
|
46
|
-
import {Configurable, Inject} from 'lakutata/decorator/di'
|
|
47
|
-
|
|
48
|
-
class Mailer extends Component {
|
|
49
|
-
//它的选项,由应用选项中的声明给出
|
|
50
|
-
@Configurable(DTO.String().required())
|
|
51
|
-
protected readonly host: string
|
|
52
|
-
|
|
53
|
-
//未配置时为 25:初始值就是默认值,和配置值一样会被校验
|
|
54
|
-
@Configurable(DTO.Number().integer().min(1).max(65535))
|
|
55
|
-
protected readonly port: number = 25
|
|
56
|
-
|
|
57
|
-
//lakutata 包预置的日志组件 "log"
|
|
58
|
-
@Inject('log')
|
|
59
|
-
protected readonly log: ILogger
|
|
60
|
-
|
|
61
|
-
protected async init(): Promise<void> {
|
|
62
|
-
this.log.info('Mailer connected to %s:%d', this.host, this.port)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
public async send(to: string, text: string): Promise<void> {
|
|
66
|
-
this.log.info('Mail to %s: %s', to, text)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
protected async destroy(): Promise<void> {
|
|
70
|
-
this.log.info('Mailer disconnected')
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
Application
|
|
75
|
-
.run(() => ({
|
|
76
|
-
id: 'com.example.app',
|
|
77
|
-
name: 'Example',
|
|
78
|
-
components: {
|
|
79
|
-
mailer: {class: Mailer, host: 'smtp.example.com'}
|
|
80
|
-
},
|
|
81
|
-
bootstrap: ['mailer']
|
|
82
|
-
}))
|
|
83
|
-
.onLaunched(async (app: Application): Promise<void> => {
|
|
84
|
-
const mailer: Mailer = await app.getObject<Mailer>('mailer')
|
|
85
|
-
await mailer.send('someone@example.com', 'Hello')
|
|
86
|
-
})
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## 3. 依赖注入
|
|
90
|
-
|
|
91
|
-
`@Inject`(来自 `lakutata/decorator/di`)在对象创建时、`init()` 之前为属性赋值:
|
|
92
|
-
|
|
93
|
-
- `@Inject()`:注入以属性名注册的对象;
|
|
94
|
-
- `@Inject('name')` 或 `@Inject(symbol)`:注入以该名称注册的对象;
|
|
95
|
-
- `@Inject(SomeClass)`:注入为该类注册的对象(`@Inject(Application)` 注入应用本身);
|
|
96
|
-
- `@Inject('name', transform)`:注入 `transform(object)` 的返回值(可以是异步函数)。
|
|
97
|
-
|
|
98
|
-
标记了 `@Autoload()` 的类在第一次被请求时自动注册,而不是报错:单例注册到根容器,作用域类注册到最近的作用域,瞬态类注册到发起请求的容器。无法解析的注册项会使创建失败并抛出 `DependencyInjectionException`。
|
|
99
|
-
|
|
100
|
-
`@Configurable(schema, fn?)` 把属性声明为对象的选项:它的值来自对象的声明(`{class: Mailer, host: 'smtp.example.com'}`)或 `getObject()` 的第二个参数,经校验规则校验后,如果提供了 `fn` 再交给 `fn` 处理。模块在初始化时会检查其声明的对象的选项:不合法的值会使初始化失败并抛出 `InvalidObjectOptionsException`,不是可配置属性的选项会给出警告。
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
import {Application, Component, Provider} from 'lakutata'
|
|
104
|
-
import {Autoload, Inject, Scoped} from 'lakutata/decorator/di'
|
|
105
|
-
|
|
106
|
-
//未在应用中声明:第一次被注入时自动注册
|
|
107
|
-
@Autoload()
|
|
108
|
-
@Scoped()
|
|
109
|
-
class RequestClock extends Provider {
|
|
110
|
-
public readonly startedAt: number = Date.now()
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
class Reports extends Component {
|
|
114
|
-
@Inject(RequestClock)
|
|
115
|
-
protected readonly clock: RequestClock
|
|
116
|
-
|
|
117
|
-
@Inject(Application)
|
|
118
|
-
protected readonly app: Application
|
|
119
|
-
|
|
120
|
-
public elapsed(): number {
|
|
121
|
-
return Date.now() - this.clock.startedAt
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
## 4. 模块
|
|
127
|
-
|
|
128
|
-
`Module` 在其 `options` 属性的初始值中,用与应用相同的选项声明自己的组件、提供者和对象。模块内的对象先从模块解析依赖,再从应用解析;应用的对象看不到模块内的对象。在应用(或父模块)的 `objects.named` 中声明模块,它的选项写在 `options` 下(合并到其初始值之上),并把它列入 `bootstrap` 以便在启动时创建。
|
|
129
|
-
|
|
130
|
-
模块被创建时,先检查选项,再注册对象,然后依次执行它的 `bootstrap`。模块外部的对象获取它时(`getObject()`、`@Inject`),要等这些步骤全部完成才会拿到它,若某一步失败则得到该步骤的错误;模块自己的对象(由这些步骤创建)则会立即拿到它。
|
|
131
|
-
|
|
132
|
-
```typescript
|
|
133
|
-
import {Application, Component, Module, type ModuleOptions} from 'lakutata'
|
|
134
|
-
|
|
135
|
-
class Billing extends Component {
|
|
136
|
-
public charge(amount: number): void {
|
|
137
|
-
console.log(`Charged ${amount}`)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
class PaymentModule extends Module {
|
|
142
|
-
protected options: ModuleOptions = {
|
|
143
|
-
components: {
|
|
144
|
-
billing: {class: Billing}
|
|
145
|
-
},
|
|
146
|
-
bootstrap: ['billing']
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
Application.run(() => ({
|
|
151
|
-
id: 'shop.app',
|
|
152
|
-
name: 'Shop',
|
|
153
|
-
objects: {
|
|
154
|
-
named: {
|
|
155
|
-
payment: {class: PaymentModule}
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
bootstrap: ['payment']
|
|
159
|
-
}))
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
`module.reload()` 会销毁模块及其对象,然后重新初始化。
|
|
163
|
-
|
|
164
|
-
## 5. 应用
|
|
165
|
-
|
|
166
|
-
### 选项
|
|
167
|
-
|
|
168
|
-
`Application.run(options)`(或 `run(() => options)`,在启动时调用该函数,可以是异步的)接受以下选项:
|
|
169
|
-
|
|
170
|
-
| 选项 | 含义 |
|
|
171
|
-
|---|---|
|
|
172
|
-
| `id`、`name` | 必填。应用的 ID(同时作为 `process.title`)和名称(`app.appId`、`app.appName`) |
|
|
173
|
-
| `timezone` | IANA 时区名,或 `'auto'`(默认)表示使用系统时区;写入 `TZ` |
|
|
174
|
-
| `mode` | `'development'` 或 `'production'`,在环境变量未指定时生效(见下文) |
|
|
175
|
-
| `shutdownSignals` | 使应用优雅退出的信号(默认 `['SIGTERM', 'SIGINT']`),或 `false` |
|
|
176
|
-
| `components`、`providers` | 按名称声明的组件和提供者:`{class, ...options}` |
|
|
177
|
-
| `objects` | 其他对象(模块、普通对象):`{named: {...}, anonymous: [...]}` |
|
|
178
|
-
| `bootstrap` | 启动时依次执行的步骤:名称、symbol 或类(创建该对象),或以应用为参数调用的异步函数 |
|
|
179
|
-
| `checkOptions` | 启动时检查所声明对象的选项(默认 `true`) |
|
|
180
|
-
|
|
181
|
-
不带 `class` 的声明会补充框架预置的同名对象的选项:`lakutata` 包预置了 `log`(`Logger`)和 `entrypoint`(`Entrypoint`),因此 `log: {level: 'debug'}` 会设置预置日志组件的级别。`Application.preset(options)` 为进程中的所有应用预置对象。
|
|
182
|
-
|
|
183
|
-
### 链式 API
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
import {Application, type ApplicationExitReason, type ILogger} from 'lakutata'
|
|
187
|
-
|
|
188
|
-
Application
|
|
189
|
-
//环境变量,在启动时、调用选项函数之前设置
|
|
190
|
-
.env({TZ_NAME: 'Asia/Shanghai'})
|
|
191
|
-
//路径别名:传给 path.resolve() 或 app.alias.resolve() 的路径中的 '@data';目录不存在时创建
|
|
192
|
-
.alias({'@data': './data'}, true)
|
|
193
|
-
.run(() => ({
|
|
194
|
-
id: 'com.example.app',
|
|
195
|
-
name: 'Example',
|
|
196
|
-
components: {}
|
|
197
|
-
}))
|
|
198
|
-
//bootstrap 完成后调用(不会被等待)
|
|
199
|
-
.onLaunched((app: Application, log: ILogger): void => log.info('%s launched in %s mode', app.appName, app.mode()))
|
|
200
|
-
//应用退出时、对象销毁之前调用,会被等待
|
|
201
|
-
.onDone(async (app: Application, log: ILogger, reason: ApplicationExitReason): Promise<void> => {
|
|
202
|
-
log.info('%s done (exit code %d%s)', app.appName, reason.code, reason.signal ? `, ${reason.signal}` : '')
|
|
203
|
-
})
|
|
204
|
-
//进程中未捕获的异常,应用继续运行
|
|
205
|
-
.onUncaughtException((error: Error, log: ILogger): void => log.warn(error.message))
|
|
206
|
-
//启动失败:返回的数字作为退出码(未返回时为 1)
|
|
207
|
-
.onFatalException((error: Error, log: ILogger): number => {
|
|
208
|
-
log.error('Launch failed: %s', error.message)
|
|
209
|
-
return 100
|
|
210
|
-
})
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
`app.exit(code)` 以指定退出码优雅地结束应用;`app.exit(true)` 立即以退出码 2 结束进程,不执行 `onDone` 处理函数,也不销毁对象。
|
|
214
|
-
|
|
215
|
-
### 生命周期
|
|
216
|
-
|
|
217
|
-
1. 设置环境变量,注册路径别名(`@runtime` 为当前工作目录)。
|
|
218
|
-
2. 读取并校验选项;应用 ID、时区和运行模式。
|
|
219
|
-
3. 在新的根容器中创建应用:注册它的对象,并依次执行 `bootstrap`。
|
|
220
|
-
4. 执行 `onLaunched` 处理函数和各对象的 `launched()` 钩子。
|
|
221
|
-
|
|
222
|
-
- **启动失败**:应用无法启动时(选项不合法、对象初始化失败、bootstrap 步骤失败),由 `onFatalException` 的处理函数决定退出码(没有返回时为 1)。没有注册处理函数时,框架记录错误后以退出码 1 退出,和 Node.js 处理未捕获异常的方式一致,这样运行它的工具(Docker 的重启策略、Kubernetes、systemd、CI)都能知道启动失败了;已经创建的对象(正在监听的服务、数据库连接等)也不会让进程继续挂着。
|
|
223
|
-
- **退出信号**:收到 `SIGTERM`(`docker stop`、Kubernetes 滚动更新、systemd 停止服务)或 `SIGINT`(Ctrl+C)时,应用会优雅退出:先执行 `onDone` 处理函数,此时应用仍然完全可用(第三个参数说明退出原因:收到信号时是 `{code: 0, signal: 'SIGTERM'}`,调用 `app.exit(code)` 时是 `{code}`);然后各入口停止接收新请求并等待正在处理的请求完成,各对象被销毁,最后以退出码 0 退出。如果再次收到信号(比如退出过程卡住了),或者信号是在应用启动期间收到的,进程会立即以 128 + 信号编号退出(`SIGTERM` 为 143,`SIGINT` 为 130)。选项 `shutdownSignals` 可以指定其他信号(如 `['SIGTERM', 'SIGINT', 'SIGHUP']`),设为 `false` 则交由 Node.js 处理。
|
|
224
|
-
- **运行模式**:`app.mode()` 返回 `'development'` 或 `'production'`。环境变量会覆盖代码里的配置,同一份构建产物可以在不同模式下运行:依次看环境变量 `MODE`、`NODE_ENV`,然后是选项 `mode`,都没有时为 `'development'`。不是这两个值的 `MODE` 会发出警告并被忽略,不是这两个值的 `NODE_ENV`(如 `test`)会被跳过。确定的模式会写回 `MODE` 和 `NODE_ENV`,第三方库读到的也是同一个模式。环境变量可以来自系统、`Application.env({MODE: 'production'})`,或者 Bun 读取的 `.env` 文件(指 `bun App.js` 这种运行方式;Bun 构建的可执行文件不会读取它)。
|
|
225
|
-
- **打包后的应用**:`isPackaged()`(来自 `lakutata`)表示应用是否以打包形式运行(`lakutata package bundle` 的打包产物或 `lakutata package exe` 的可执行文件)。应用运行时要读取的文件(Web 应用构建好的页面、模板、数据等)在 `package.json` 的 `lakutata.resources` 中声明,写法和 electron-builder 的 `extraResources` 一样:`[{"from": "../ui/dist", "to": "ui"}]`。打包后,`resourcesPath()` 返回这些文件所在的目录;未打包时返回 `undefined`,由应用到构建目录中查找:`const ui = resourcesPath() ? path.join(resourcesPath()!, 'ui') : uiBuildDirectory`。
|
|
226
|
-
|
|
227
|
-
## 6. 入口
|
|
228
|
-
|
|
229
|
-
`entrypoint` 组件(`lakutata/com/entrypoint`)通过 HTTP、CLI、服务传输和定时任务对外提供控制器。用 `buildEntrypoints({controllers, http, cli, service, schedule})` 给出它的选项,并把它列入 `bootstrap`;详见 [ActionPattern](ActionPattern.md)。
|
|
230
|
-
|
|
231
|
-
## 7. 不使用应用的容器
|
|
232
|
-
|
|
233
|
-
测试、脚本和库可以在单独的容器中使用这些对象:
|
|
234
|
-
|
|
235
|
-
```typescript
|
|
236
|
-
import {Component, Container, DTO} from 'lakutata'
|
|
237
|
-
import {Configurable} from 'lakutata/decorator/di'
|
|
238
|
-
|
|
239
|
-
class Mailer extends Component {
|
|
240
|
-
@Configurable(DTO.String().required())
|
|
241
|
-
protected readonly host: string
|
|
242
|
-
|
|
243
|
-
public async send(to: string, text: string): Promise<string> {
|
|
244
|
-
return `${this.host} -> ${to}: ${text}`
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const container: Container = new Container()
|
|
249
|
-
//按选项创建,并等待它的 init() 完成
|
|
250
|
-
const mailer: Mailer = await container.set(Mailer, {host: 'localhost'})
|
|
251
|
-
await mailer.send('someone@example.com', 'Hello')
|
|
252
|
-
//销毁它创建的所有对象
|
|
253
|
-
await container.destroy()
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
`container.register()`/`registerNamed()` 只注册类而不创建对象,`get()` 解析对象,`build()` 无论生命周期类型都新建一个对象,`createScope()` 创建作用域。
|