lakutata 3.1.4 → 3.1.6

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.
Files changed (67) hide show
  1. package/README.md +11 -11
  2. package/README.zh-CN.md +5 -5
  3. package/dist/cjs/exports/NwjsAdapter.d.ts +1 -0
  4. package/dist/cjs/exports/NwjsAdapter.js +20 -0
  5. package/dist/cjs/exports/ReactNativeAdapter.d.ts +1 -0
  6. package/dist/cjs/exports/ReactNativeAdapter.js +20 -0
  7. package/dist/esm/exports/NwjsAdapter.js +1 -0
  8. package/dist/esm/exports/ReactNativeAdapter.js +1 -0
  9. package/dist/types/exports/NwjsAdapter.d.ts +1 -0
  10. package/dist/types/exports/ReactNativeAdapter.d.ts +1 -0
  11. package/doc/en/Application.md +6 -1
  12. package/doc/en/Barcode.md +386 -0
  13. package/doc/en/Cache.md +2 -1
  14. package/doc/en/Command.md +20 -4
  15. package/doc/en/ContainerdIntegration.md +1 -1
  16. package/doc/en/Database.md +4 -0
  17. package/doc/en/DatabaseDrivers.md +75 -4
  18. package/doc/en/Entrypoint.md +4 -2
  19. package/doc/en/Exceptions.md +1 -1
  20. package/doc/en/FFI.md +1 -1
  21. package/doc/en/GRPC.md +1 -1
  22. package/doc/en/GettingStarted.md +16 -2
  23. package/doc/en/HTTP.md +1 -0
  24. package/doc/en/I18n.md +1 -1
  25. package/doc/en/Installers.md +34 -6
  26. package/doc/en/Math.md +1 -1
  27. package/doc/en/Migrations.md +2 -2
  28. package/doc/en/NativeAddons.md +12 -2
  29. package/doc/en/Overview.md +14 -11
  30. package/doc/en/Packaging.md +8 -1
  31. package/doc/en/ProjectStructure.md +18 -3
  32. package/doc/en/ServiceCalls.md +85 -5
  33. package/doc/en/UpgradingFrom1.md +2 -2
  34. package/doc/en/UpgradingFrom2.md +4 -4
  35. package/doc/en/WebRTC.md +4 -4
  36. package/doc/en/examples/MobileApp.md +730 -0
  37. package/doc/en/examples/README.md +2 -0
  38. package/doc/en/examples/TrayApp.md +1061 -0
  39. package/doc/zh/Application.md +3 -1
  40. package/doc/zh/Barcode.md +313 -0
  41. package/doc/zh/Cache.md +1 -1
  42. package/doc/zh/Command.md +12 -3
  43. package/doc/zh/ContainerdIntegration.md +1 -1
  44. package/doc/zh/Database.md +2 -0
  45. package/doc/zh/DatabaseDrivers.md +52 -2
  46. package/doc/zh/Entrypoint.md +3 -1
  47. package/doc/zh/Exceptions.md +1 -1
  48. package/doc/zh/FFI.md +1 -1
  49. package/doc/zh/GRPC.md +1 -1
  50. package/doc/zh/GettingStarted.md +12 -2
  51. package/doc/zh/HTTP.md +1 -0
  52. package/doc/zh/I18n.md +1 -1
  53. package/doc/zh/Installers.md +16 -5
  54. package/doc/zh/Math.md +1 -1
  55. package/doc/zh/Migrations.md +1 -1
  56. package/doc/zh/NativeAddons.md +4 -2
  57. package/doc/zh/Overview.md +5 -4
  58. package/doc/zh/Packaging.md +6 -1
  59. package/doc/zh/ProjectStructure.md +5 -2
  60. package/doc/zh/ServiceCalls.md +65 -2
  61. package/doc/zh/UpgradingFrom1.md +2 -2
  62. package/doc/zh/UpgradingFrom2.md +3 -3
  63. package/doc/zh/WebRTC.md +1 -1
  64. package/doc/zh/examples/MobileApp.md +671 -0
  65. package/doc/zh/examples/README.md +2 -0
  66. package/doc/zh/examples/TrayApp.md +980 -0
  67. package/package.json +36 -16
@@ -8,7 +8,7 @@
8
8
 
9
9
  | 选项 | 含义 |
10
10
  |---|---|
11
- | `id`、`name` | 必填。应用的 ID(同时作为 `process.title`)和名称(`app.appId`、`app.appName`) |
11
+ | `id`、`name` | 必填。应用的 ID(同时作为 `process.title`,NW.js 中除外)和名称(`app.appId`、`app.appName`) |
12
12
  | `timezone` | IANA 时区名,或 `'auto'`(默认)表示使用系统时区;写入 `TZ` |
13
13
  | `mode` | `'development'` 或 `'production'`,在环境变量未指定时生效(见第 4 节) |
14
14
  | `shutdownSignals` | 使应用优雅退出的信号(默认 `['SIGTERM', 'SIGINT']`),或 `false` |
@@ -49,6 +49,8 @@ Application
49
49
  })
50
50
  ```
51
51
 
52
+ 环境变量和别名在应用启动时、读取选项之前设置。所以用到别名的选项,比如 `path.resolve('@data/app.sqlite')`,要像上面那样写在传给 `run()` 的选项函数中:直接传给 `run()` 的对象在启动之前就已生成,那时 `@data` 还没有声明,`path.resolve()` 得到的是工作目录下名为 `@data` 的目录中的 `app.sqlite`。
53
+
52
54
  `app.exit(code)` 以指定退出码优雅地结束应用;`app.exit(true)` 立即以退出码 2 结束进程,不执行 `onDone` 处理函数,也不销毁对象。
53
55
 
54
56
  ## 3. 生命周期
@@ -0,0 +1,313 @@
1
+ # Lakutata Barcode
2
+
3
+ `@lakutata/barcode` 包生成条形码和二维码,也能从图片里识别它们:商品标签、物流面单、票券、印成二维码的链接,可以从上传的照片或摄像头的画面里识别出来。它不属于 `lakutata`,需要单独安装。它的特点是:
4
+
5
+ - **能生成的都能识别回来**:32 种一维码和二维码,从 EAN-13、UPC、Code 128、GS1 DataBar 到 QR 码、Data Matrix、PDF417、Aztec 和 MaxiCode。只有一边支持的码制(邮政码、汉信码、DotCode 等)没有提供。
6
+ - **生成**为 PNG、SVG 或像素,尺寸、颜色、旋转方向和纠错等级都可以指定。
7
+ - **识别** PNG、JPEG、GIF、BMP、WebP、TGA 图片或像素;图片里的每个条码都会识别出来,并给出位置。
8
+ - **一个组件** `Barcode`,不用注册就能注入,使用默认选项;在配置里用名字注册的组件,可以有自己的默认选项、时限和并发数。每次调用都可以设置时限,也可以取消。
9
+ - **原生插件**:基于 [zint](https://zint.org.uk) 和 [zxing-cpp](https://github.com/zxing-cpp/zxing-cpp),在 Linux、macOS 和 Windows 上安装这个包时编译,在 Android、iOS 和鸿蒙的手机应用里由 `lakutata mobile stage` 编译;也能在 Bun 和 Electron 的主进程里运行。生成和识别都在原生插件自己的线程里执行,不占用 JavaScript 线程。
10
+
11
+ `@lakutata/barcode` 的 README 介绍了每个选项;这篇指南按用途逐一说明这个包能做什么,每种用途都给出一个例子。
12
+
13
+ ```bash
14
+ npm install @lakutata/barcode
15
+ ```
16
+
17
+ 原生插件在安装这个包时编译,所以机器上要有 Python 3 和支持 C++20 的编译器(macOS 上是 Xcode 的命令行工具,Linux 上是 `build-essential` 或 `build-base`,Windows 上是 Visual Studio 生成工具)。包管理器必须运行这个包的安装脚本:npm 会运行它,可能会提示这个脚本“not yet covered by allowScripts”(运行 `npm install-scripts approve @lakutata/barcode` 记录你允许它运行);Bun 和 pnpm 只为被告知可以信任的包运行安装脚本(Bun 在 package.json 的 `trustedDependencies` 里设置,pnpm 用 `pnpm approve-builds`)。组件需要 `@lakutata/core`,这是一个 peer 依赖,安装 `lakutata` 时会一起装上。
18
+
19
+ ## 1. 生成条码
20
+
21
+ `generate(format, content, options)` 的内容可以是文本(按 UTF-8 编码),也可以是字节(`Uint8Array`);默认返回 PNG:
22
+
23
+ ```typescript
24
+ import {Container} from 'lakutata'
25
+ import {Barcode, type RawImage} from '@lakutata/barcode'
26
+
27
+ const container: Container = new Container()
28
+ const barcode: Barcode = await container.get(Barcode)
29
+
30
+ const png: Buffer = await barcode.generate('QRCode', 'https://example.com/orders/123')
31
+ const svg: string = await barcode.generate('DataMatrix', 'LKT-1001', {output: 'svg'})
32
+ const pixels: RawImage = await barcode.generate('Code128', 'LKT-1001', {output: 'raw', scale: 2})
33
+ console.log(png.length, svg.length) //704 1008
34
+ console.log(pixels.width, pixels.height, pixels.channels) //264 116 1
35
+
36
+ await container.destroy()
37
+ ```
38
+
39
+ - 二维码的尺寸(版本)和内容的编码方式按内容自动选择:选能装下内容的最小尺寸,除非用 `version` 指定。
40
+ - 像素在黑白时是灰度(每像素 1 字节),有颜色时是 RGB(3 字节),有透明时是 RGBA(4 字节),可以直接交给 canvas、图像库或打印机驱动。
41
+ - 内容无法用这种码制编码时(比如 EAN-13 里有字母,或者超出指定版本的容量),会抛出 `BarcodeContentException`,附带 zint 给出的原因。
42
+
43
+ 在应用里,组件是注入的,不用从容器里取;要用自己的选项,就在配置里用一个名字注册组件,再按这个名字注入(第 5 节)。
44
+
45
+ ## 2. 尺寸、颜色和打印
46
+
47
+ `scale` 是模块的大小,模块就是最窄的条,或二维码的一个小方块:在 PNG 和像素中是像素数,在 SVG 中是单位数。它是整数,所以 PNG 里的条边缘始终清晰。打印时,按模块的毫米数算出 scale:`scale = 毫米数 × dpi / 25.4`,再取整。EAN-13 的标准模块宽度是 0.33 毫米,允许按 80% 到 200% 缩放:在 203 dpi 的热敏标签打印机上,scale 为 3 时模块是 0.375 毫米(114%);在 300 dpi 上,scale 为 4 时是 0.34 毫米。SVG 可以按任意尺寸打印,它的 scale 只决定单位。
48
+
49
+ - `height` 是一维码条的高度,单位是模块,所以会随 scale 一起缩放。
50
+ - `showText` 在一维码的条下方显示内容;PNG 和像素输出要求 scale 至少为 2(字体每个模块需要 2 个像素)。
51
+ - `quietZone` 默认留出码制标准规定的空白;扫描器靠这些空白找到条码,所以不留空白的标签,要在条码周围留出空位。
52
+ - `foreground` 和 `background` 写作 `#RRGGBB` 或 `#RRGGBBAA`。请保持浅底深条:这个包也能识别深底浅条(`tryInvert`),但很多扫描器不能。透明背景在识别时按白色处理。
53
+ - `rotate` 把条码顺时针旋转 90、180 或 270 度,用于横着打印的标签。
54
+ - `errorCorrection` 让二维码部分损坏或被遮住时仍能识别:QR 码从 `'L'`(约 7%)到 `'H'`(约 30%)。等级越高,同样的内容生成的码越大。
55
+
56
+ ## 3. EAN、UPC 和 GS1
57
+
58
+ 零售码制会计算校验位,给出的校验位也会被验证:
59
+
60
+ ```typescript
61
+ import {Container} from 'lakutata'
62
+ import {Barcode, type BarcodeResult, BarcodeContentException} from '@lakutata/barcode'
63
+
64
+ const container: Container = new Container()
65
+ const barcode: Barcode = await container.get(Barcode)
66
+
67
+ //12 位数字:自动计算校验位
68
+ const ean: Buffer = await barcode.generate('EAN13', '590123412345', {addOn: '12'})
69
+ const [product]: BarcodeResult[] = await barcode.read(ean)
70
+ console.log(product.format, product.text, product.addOn) //EAN13 5901234123457 12
71
+
72
+ //13 位数字:验证校验位
73
+ await barcode.generate('EAN13', '5901234123458').catch((error: BarcodeContentException) => console.log(error.message))
74
+ //Error 275: Invalid check digit '8', expecting '7'
75
+
76
+ //物流标签:GTIN、有效期和批号
77
+ const gs1: Buffer = await barcode.generate('Code128', '[01]09506000134352[17]261231[10]ABC123', {gs1: true})
78
+ const [label]: BarcodeResult[] = await barcode.read(gs1)
79
+ console.log(label.text, label.gs1, label.symbologyIdentifier) //(01)09506000134352(17)261231(10)ABC123 true ]C1
80
+
81
+ await container.destroy()
82
+ ```
83
+
84
+ - **UPC。** UPC-A 就是以 0 开头的 EAN-13,会被识别为 13 位的 `EAN13`:`01234567890` 识别出 `0012345678905`。`formats` 里有 `UPCA` 而没有 `EAN13` 时,格式是 `UPCA`,数字还是这 13 位。ISBN 也会被识别为 `EAN13`。
85
+ - **附加码。** 2 位或 5 位的附加码(期号、价格)放在 `addOn` 里,和文本分开。用 `addOn: 'require'` 时,只保留带附加码的条码。
86
+ - **GS1。** 应用标识符可以写在方括号或圆括号里;加上 `gs1: true` 时,Code 128 就成了 GS1-128,QR 码、Data Matrix 和 Aztec 也可以装 GS1 数据。DataBar Expanded 两种码制的内容总是 GS1 数据;其他 DataBar 码制装的是 GTIN,带不带校验位都可以。识别时,数据以圆括号形式给出,`gs1` 为 `true`。
87
+
88
+ 每种码制会被识别成什么、识别出的文本是什么样子,见这个包的 README。
89
+
90
+ ## 4. 识别图片
91
+
92
+ `read(image, options)` 接受图片文件(它的字节,`Buffer` 或 `Uint8Array`)或像素,返回找到的所有条码;没有条码时返回空数组:
93
+
94
+ ```typescript
95
+ import {readFileSync} from 'node:fs'
96
+ import {Container} from 'lakutata'
97
+ import {Barcode, type BarcodeResult, type RawImage} from '@lakutata/barcode'
98
+
99
+ const container: Container = new Container()
100
+ const barcode: Barcode = await container.get(Barcode)
101
+
102
+ const photo: Buffer = readFileSync('qrcode.jpeg')
103
+ for (const result of await barcode.read(photo, {formats: ['QRCode', 'DataMatrix']})) {
104
+ console.log(result.format, result.text, result.position.topLeft, result.orientation)
105
+ //QRCode https://lakutata.com/fixtures { x: 16, y: 16 } 0
106
+ }
107
+
108
+ //像素:灰度每像素 1 字节,RGB 3 字节,RGBA 4 字节(canvas 的 ImageData、解码后的视频帧)
109
+ const frame: RawImage = await barcode.generate('DataMatrix', 'LKT-1001', {output: 'raw'})
110
+ const rgba: Uint8Array = new Uint8Array(frame.width * frame.height * 4)
111
+ frame.data.forEach((gray: number, index: number): void => rgba.set([gray, gray, gray, 255], index * 4))
112
+ const results: BarcodeResult[] = await barcode.read({data: rgba, width: frame.width, height: frame.height, channels: 4})
113
+ console.log(results.map((result: BarcodeResult): string => result.text)) //[ 'LKT-1001' ]
114
+
115
+ await container.destroy()
116
+ ```
117
+
118
+ - **识别得更快。** `formats` 把查找范围限制在预期的码制上:在 1920×1080 的画面里识别一个 QR 码,查找所有码制约 8 毫秒,只查找 QR 码约 1.4 毫秒(第 7 节)。`pure: true` 用于图片里只有一个摆正的条码、周围没有别的东西的情况,例如 `generate()` 生成的图片。
119
+ - **难识别的图片。** `tryHarder`、`tryRotate`、`tryInvert` 和 `tryDownscale` 默认都打开:它们会再去找很小的、旋转了的、深底浅色的和大图里的条码。已知图片容易识别时,关掉它们会更快。
120
+ - **识别不出的情况。** 模块宽度小于约 2 像素的条码、模糊或反光的照片、被图片边缘截断的条码。这时识别出的条码会变少或者没有;只有图片无法解码时才会抛出异常(`BarcodeImageException`)。
121
+ - **大图片**:超过 5000 万像素的图片在解码像素之前就会被拒绝,所以上传的文件不会让进程分配几个 GB 的内存。
122
+ - **字节。** `bytes` 是编码在码里的原始内容;`text` 是它的可读形式(带校验位,GS1 用圆括号),`symbologyIdentifier` 说明怎样解读这些字节(`]Q1` 是 QR 码,`]C1` 是 GS1-128 等)。
123
+
124
+ ## 5. 在应用中使用
125
+
126
+ 组件不用注册就能注入(`@Inject(Barcode)`),使用默认选项。要用自己的选项时,在配置的 `components` 里给组件起一个名字,用 `buildBarcodeOptions()` 注册它,设置 `generate()` 和 `read()` 的默认选项、时限和并发数;一个应用可以用不同的名字注册多个,各用各的选项。下面的仓库应用注册了两个:`labels` 打印标签,`scanner` 识别照片:
127
+
128
+ ```typescript
129
+ import {Application, Component} from 'lakutata'
130
+ import {Inject} from 'lakutata/decorator/di'
131
+ import {type Barcode, type BarcodeResult, buildBarcodeOptions} from '@lakutata/barcode'
132
+
133
+ class Warehouse extends Component {
134
+ @Inject('labels')
135
+ protected readonly labels: Barcode
136
+
137
+ @Inject('scanner')
138
+ protected readonly scanner: Barcode
139
+
140
+ //货品标签:Code 128,编号印在条的下方
141
+ public async label(sku: string): Promise<Buffer> {
142
+ return await this.labels.generate('Code128', sku)
143
+ }
144
+
145
+ //照片里的条码:只查找 Code 128 和 QR 码
146
+ public async scan(photo: Buffer): Promise<BarcodeResult[]> {
147
+ return await this.scanner.read(photo)
148
+ }
149
+ }
150
+
151
+ Application
152
+ .run(() => ({
153
+ id: 'warehouse.app',
154
+ name: 'Warehouse',
155
+ components: {
156
+ //203 dpi 的标签打印机:模块 3 个像素,条下方显示内容
157
+ labels: buildBarcodeOptions({defaults: {generate: {scale: 3, showText: true}}}),
158
+ //每张照片最多识别 2 秒
159
+ scanner: buildBarcodeOptions({defaults: {read: {formats: ['Code128', 'QRCode']}}, timeout: 2000}),
160
+ warehouse: {class: Warehouse}
161
+ }
162
+ }))
163
+ .onLaunched(async (app: Application): Promise<void> => {
164
+ const warehouse: Warehouse = await app.getObject<Warehouse>('warehouse')
165
+ const [code]: BarcodeResult[] = await warehouse.scan(await warehouse.label('LKT-1001'))
166
+ console.log(code.format, code.text) //Code128 LKT-1001
167
+ })
168
+ ```
169
+
170
+ - `buildBarcodeOptions()` 用你选定的名字声明组件;其他对象按这个名字注入它(`@Inject('labels')`),或者用 `getObject('labels')` 获取。
171
+ - 注册的组件只能按名字注入。`@Inject(Barcode)` 得到的是另一个组件:它是自动加载的,使用默认选项,配置里的选项对它不起作用。
172
+
173
+ 下面的控制器把商品标签作为 PNG 返回,并识别上传图片里的条码:
174
+
175
+ ```typescript
176
+ import express, {type Express, type Request} from 'express'
177
+ import {Application, DTO} from 'lakutata'
178
+ import {buildEntrypoints, Controller, type FileResult, Result} from 'lakutata/com/entrypoint'
179
+ import {GET, POST} from 'lakutata/decorator/ctrl'
180
+ import {Inject} from 'lakutata/decorator/di'
181
+ import {Expect} from 'lakutata/decorator/dto'
182
+ import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
183
+ import {Barcode, type BarcodeResult, buildBarcodeOptions} from '@lakutata/barcode'
184
+
185
+ class LabelDTO extends DTO {
186
+ @Expect(DTO.String().pattern(/^\d{12,13}$/).required())
187
+ public ean: string
188
+ }
189
+
190
+ class ScanDTO extends DTO {
191
+ @Expect(DTO.Binary().required())
192
+ public image: Buffer
193
+ }
194
+
195
+ class LabelController extends Controller {
196
+ @Inject('labels')
197
+ protected readonly labels: Barcode
198
+
199
+ //GET /labels/590123412345:商品的 EAN-13,PNG 格式
200
+ @GET('/labels/:ean', LabelDTO)
201
+ public async label(input: LabelDTO): Promise<FileResult> {
202
+ return Result.file(await this.labels.generate('EAN13', input.ean), {contentType: 'image/png'})
203
+ }
204
+
205
+ //POST /scans,请求体是图片:返回图片里的条码
206
+ @POST('/scans', ScanDTO)
207
+ public async scan(input: ScanDTO): Promise<{ format: string, text: string }[]> {
208
+ const results: BarcodeResult[] = await this.labels.read(input.image)
209
+ return results.map((result: BarcodeResult) => ({format: result.format, text: result.text}))
210
+ }
211
+ }
212
+
213
+ Application.run(() => ({
214
+ id: 'shop.app',
215
+ name: 'Shop',
216
+ components: {
217
+ labels: buildBarcodeOptions({defaults: {generate: {scale: 3}, read: {formats: ['EAN13', 'QRCode']}}, timeout: 10000}),
218
+ entrypoint: buildEntrypoints({
219
+ controllers: [LabelController],
220
+ http: expressHTTPEntrypoint({
221
+ port: 8080,
222
+ before: (app: Express): void => {
223
+ //POST /scans 的请求体就是图片
224
+ app.post('/scans', express.raw({type: 'image/*', limit: '10mb'}))
225
+ },
226
+ input: (request: Request): Record<string, unknown> => Buffer.isBuffer(request.body) ? {image: request.body} : {...request.query, ...request.body}
227
+ })
228
+ })
229
+ },
230
+ bootstrap: ['entrypoint']
231
+ }))
232
+ ```
233
+
234
+ ```text
235
+ $ curl -s -o label.png localhost:8080/labels/590123412345
236
+ $ curl -s --data-binary @label.png -H 'Content-Type: image/png' localhost:8080/scans
237
+ [{"format":"EAN13","text":"5901234123457"}]
238
+ ```
239
+
240
+ - 调用时传入的选项覆盖组件的默认选项:`this.labels.generate('QRCode', url, {scale: 6})` 的其他选项仍用默认值。
241
+ - 参数会先经过校验:未知的码制、不是 `#RRGGBB` 格式的颜色,在调用原生插件之前就会被拒绝,抛出 `InvalidMethodAcceptException`。
242
+ - 一个组件可以同时为所有控制器服务:各次调用互不影响,只受 `concurrency` 的限制。
243
+ - 在 React Native 的手机应用(手机模板)里,组件同样在应用的 Node.js 中运行:`lakutata mobile stage` 会从源码为 Android(用 NDK)、iOS(用 Xcode)和鸿蒙(用 DevEco Studio)编译原生插件。
244
+
245
+ ## 6. 时限、取消和并发
246
+
247
+ 每次调用都可以传入 `signal`(`AbortSignal`)和以毫秒为单位的 `timeout`,时限从调用时开始计算;没有传 `timeout` 的调用使用组件的 `timeout`。
248
+
249
+ ```typescript
250
+ import {readFileSync} from 'node:fs'
251
+ import {Container} from 'lakutata'
252
+ import {Barcode, type BarcodeResult} from '@lakutata/barcode'
253
+
254
+ const container: Container = new Container()
255
+ const barcode: Barcode = await container.get(Barcode)
256
+ const photo: Buffer = readFileSync('qrcode.jpeg')
257
+
258
+ //最多 2 秒,排队的时间也算在内
259
+ const results: BarcodeResult[] = await barcode.read(photo, {timeout: 2000})
260
+
261
+ //直到识别被取消
262
+ const scan: AbortController = new AbortController()
263
+ const reading: Promise<BarcodeResult[]> = barcode.read(photo, {signal: scan.signal})
264
+ scan.abort()
265
+ await reading.catch((error: Error) => console.log(error.constructor.name)) //BarcodeCancelledException
266
+
267
+ await container.destroy()
268
+ ```
269
+
270
+ - 还没开始执行的操作会直接放弃。已经开始执行的操作无法中途停止,因为 zint 和 zxing-cpp 没有可以中断的地方:它的 Promise 会立即被拒绝(`BarcodeTimeoutException`、`BarcodeCancelledException`),结果在执行结束后丢弃。这些操作都很快(第 7 节);时限是为大照片和恶意上传准备的。
271
+ - `concurrency` 限制一个组件同时执行的操作数,其余的排队等待,排队期间时限照样计算。不设置时,操作在原生插件的线程里排队。
272
+ - 原生插件有 `LAKUTATA_BARCODE_THREADS` 个线程,默认是处理器数减一,所有组件共用;它不使用 libuv 的线程池,所以不会拖慢文件和 DNS 操作。
273
+ - 应用停止时,组件会等待正在执行的操作,最多 `drainTimeout` 毫秒(默认 10000);还在排队的操作以 `BarcodeCancelledException` 拒绝,之后的调用也一样。
274
+
275
+ ## 7. 速度
276
+
277
+ 在 Apple M4 上测得(Node.js 24,除特别说明外每次只执行一个调用):
278
+
279
+ | 操作 | 耗时 |
280
+ |---|---|
281
+ | 一个网址的 QR 码,PNG(148×148) | 0.19 毫秒 |
282
+ | 同一个 QR 码,像素 / SVG | 0.08 毫秒 / 0.17 毫秒 |
283
+ | EAN-13 / Code 128,PNG,带文字 | 0.62 毫秒 / 1.6 毫秒 |
284
+ | 识别这个 QR 码的 PNG,所有码制 / `formats: ['QRCode']` | 0.33 毫秒 / 0.11 毫秒 |
285
+ | 识别它的像素,`formats: ['QRCode']`,`pure: true` | 0.06 毫秒 |
286
+ | 在 1920×1080 的画面里识别一个 QR 码,所有码制 / `formats: ['QRCode']` | 7.8 毫秒 / 1.4 毫秒 |
287
+ | 同时生成 / 同时识别 1000 个 QR 码 PNG(9 个线程) | 63 毫秒 / 33 毫秒 |
288
+
289
+ 手机拍的照片比这些 `generate()` 生成的图片识别得慢:照片里的条码更小、有旋转、有模糊,`try…` 选项要做更多的工作。
290
+
291
+ ## 8. 验证方式,以及不支持的情况
292
+
293
+ 测试为每种码制生成条码,再从它的 PNG 和像素识别回来,内容有随机的(多种文字的文本,以及字节);测试检查每个选项的效果,识别 7 种文件格式的图片,并识别 240 张损坏的图片,结果只能是识别出条码或者抛出 `BarcodeImageException`。测试在 macOS(arm64:Node.js 24 和 18.10、Bun 1.4)、Linux(arm64:Debian 12 上的 Node.js 22,Alpine 上的 Node.js 24)和 Windows 11(arm64:Node.js 24)上运行;这个包也能在 Electron 44 的主进程里运行,也能在 Android、iOS 和鸿蒙的手机应用里运行:在三个模拟器上,每种码制和 7 种文件格式都能识别回来。
294
+
295
+ 它不支持的情况,有的是有意为之,有的是还没做:
296
+
297
+ - zxing-cpp 识别不了的码制不提供生成:四态邮政码、汉信码、DotCode、Code One、Grid Matrix、Codablock-F、Code 16K、Code 49 以及 zint 的其他堆叠码制。Telepen Numeric 也没有提供,因为识别时除非只查找它,否则会把它当成 Telepen。
298
+ - 分成多个码的内容(结构化追加)逐个识别:`structuredAppend` 给出每个码的位置,内容的拼接由应用完成。
299
+ - 像素只支持灰度、RGB 和 RGBA;BGR 和 BGRA(某些摄像头接口)需要应用自己转换。GIF 只识别第一帧。
300
+ - 输出只有 PNG、SVG 和像素,不提供 EPS、EMF 和 PDF。
301
+
302
+ ## 9. 异常
303
+
304
+ 所有异常都继承 `BarcodeException`,它继承 lakutata 的 `Exception`。
305
+
306
+ | 异常 | `errno` | 什么时候 |
307
+ |---|---|---|
308
+ | `BarcodeContentException` | `E_BARCODE_CONTENT` | 内容无法用这种码制编码,附带 zint 给出的原因 |
309
+ | `BarcodeOptionException` | `E_BARCODE_OPTION` | 码制不支持这个选项(如给 Code 128 指定 `errorCorrection`),或者值超出范围(`version: 41`) |
310
+ | `BarcodeImageException` | `E_BARCODE_IMAGE` | 图片无法解码,或者超过 5000 万像素;像素的字节数不等于 `width × height × channels` |
311
+ | `BarcodeTimeoutException` | `E_BARCODE_TIMEOUT` | 操作超过时限 |
312
+ | `BarcodeCancelledException` | `E_BARCODE_CANCELLED` | 操作的 signal 被中止,或者调用了已经销毁的组件 |
313
+ | `BarcodeException` | `E_BARCODE` | 原生插件的内部错误 |
package/doc/zh/Cache.md CHANGED
@@ -250,7 +250,7 @@ Application.run(() => ({
250
250
 
251
251
  - `table` 中字母、数字和 `_` 以外的字符会被去掉。
252
252
  - 语句同步执行:等待被锁住的数据库时,语句会阻塞事件循环,最长 `busyTimeout`。
253
- - `better-sqlite3` 是原生插件,针对一种运行时构建:Electron 应用需要使用针对其 Electron 版本构建的 `better-sqlite3`。在 Bun 上,存储改用 `bun:sqlite`。
253
+ - `better-sqlite3` 是原生插件,针对一种运行时构建:Electron 应用需要使用针对其 Electron 版本构建的 `better-sqlite3`;NW.js 应用会用 NW.js 的头文件重新构建它(需要 0.111.3 及以后的版本:更早的版本无法加载它)。在 Bun 上,存储改用 `bun:sqlite`。
254
254
 
255
255
  ### PostgreSQL
256
256
 
package/doc/zh/Command.md CHANGED
@@ -9,7 +9,7 @@ npx @lakutata/cli create --name my-app # 不安装,直接运行
9
9
  npm install --global @lakutata/cli # 或者在本机安装一次,之后:lakutata create --name my-app
10
10
  ```
11
11
 
12
- 模板会把 `@lakutata/cli` 加入项目的 `devDependencies`,所以项目的脚本可以直接调用 `lakutata`,在项目里也可以用 `npx lakutata`。工具需要 Node.js `^22.13.0 || >=24.11.0`:22.13 及以后的 22.x,或 24.11 及以后的版本(不支持 23)。
12
+ 模板会把 `@lakutata/cli` 加入项目的 `devDependencies`,所以项目的脚本可以直接调用 `lakutata`,在项目里也可以用 `npx lakutata`。工具需要 Node.js 18.10 或更高版本(`>=18.10.0`)。
13
13
 
14
14
  - `lakutata --help` 列出所有命令,`lakutata help <command>`(或 `lakutata <command> --help`)说明某个命令,`lakutata --version` 输出工具的版本。不带命令运行时,工具输出帮助,并以退出码 1 退出。
15
15
  - 选项名用短横线连接(`--package-manager`)。默认开启的布尔选项用 `--no-` 关闭(`--no-git`、`--no-bail`)。
@@ -30,11 +30,12 @@ lakutata create --name tool --template cli --package-manager none --no-git
30
30
  |---|---|---|
31
31
  | `--name` | 在终端里会询问 | 应用的名称,也是项目目录的名称 |
32
32
  | `--id` | 名称的小写形式 | 应用的 ID,也是包名 |
33
+ | `--bundle-id` | `com.example.<名称>` | 应用在手机系统上的标识(反向域名),用于手机模板;应用商店不接受 `com.example` |
33
34
  | `--template` | `default` | 官方模板(见 `lakutata templates`)、`npm:<package>[@<version>]`、`github:<owner>/<repo>[#<ref>]`,或一个目录 |
34
35
  | `--path` | `.` | 在哪里创建项目目录(`<path>/<name>`) |
35
36
  | `--init` | | 直接在 `--path` 这个已有目录中创建项目 |
36
37
  | `--description`、`--author`、`--license` | 许可证为 `UNLICENSED` | 包的信息 |
37
- | `--with` | | 模板的可选功能,用逗号分隔(`i18n`、`installer`) |
38
+ | `--with` | | 模板的可选功能,用逗号分隔(`i18n`、`installer`、`win7`) |
38
39
  | `--package-manager` | `npm`;由 pnpm 或 yarn 启动工具时为对应的包管理器 | `npm`、`pnpm`、`yarn`,或 `none` 表示不安装依赖 |
39
40
  | `--no-git` | | 不创建 git 仓库 |
40
41
  | `--yes` | | 不询问可选功能,也不要求确认 |
@@ -67,6 +68,10 @@ lakutata create --name tool --template cli --package-manager none --no-git
67
68
 
68
69
  `lakutata package bundle` 和 `package exe` 把编译好的应用打包成单个 JavaScript 文件,或者不需要安装 Node.js 就能运行的可执行文件,见 [Packaging](Packaging.md)。`lakutata package installer` 制作应用的安装包,安装包还可以把应用注册为服务,见 [Installers](Installers.md)。
69
70
 
71
+ NW.js 应用所用的 NW.js 版本由 `lakutata nwjs use <版本>` 设置(`latest`、`win7` 或某个版本,`lakutata nwjs versions` 会列出这些版本),版本会写进应用的 `package.json`。`lakutata nwjs run [args...]` 在这个 NW.js 的 sdk 版本(带 DevTools)中运行编译好的应用,原生插件会被处理成能在其中加载,并把参数传给应用。应用的安装包里是 normal 版本(见 [Installers](Installers.md) 第 9 节)。
72
+
73
+ 按 `mobile-react` 模板布局的 React Native 手机应用运行在它的原生模块内嵌的 libnode 中。`lakutata mobile node <android|ios|harmony>` 从 lakutata/libnode 的 Release 下载一个系统的 libnode;`lakutata mobile stage <platform>` 把应用的 Node.js 项目生成到系统的原生工程读取它的位置;`lakutata mobile run <platform>` 构建应用并在模拟器或设备上运行;`lakutata mobile deploy <platform>` 把 Node.js 项目放进那里已安装的应用,再重新启动应用。
74
+
70
75
  ## 6. 环境变量
71
76
 
72
77
  | 变量 | 含义 |
@@ -74,7 +79,11 @@ lakutata create --name tool --template cli --package-manager none --no-git
74
79
  | `LAKUTATA_LANG` | 工具的语言(`en`、`zh-CN`、`zh-TW`) |
75
80
  | `LAKUTATA_NPM_REGISTRY` | 下载模板的仓库(否则取 `npm_config_registry`,再否则为 `https://registry.npmjs.org`) |
76
81
  | `LAKUTATA_GITHUB_DOWNLOAD` | 下载 `github:` 模板的地址(默认 `https://github.com`) |
77
- | `LAKUTATA_CACHE_DIR` | 工具的缓存目录:模板,以及制作安装包所用的工具(默认 `$XDG_CACHE_HOME/lakutata`,否则 `~/.cache/lakutata`) |
82
+ | `LAKUTATA_CACHE_DIR` | 工具的缓存目录:模板、制作安装包所用的工具、NW.js 的运行时,以及手机应用的 libnode 和它们的原生插件(默认 `$XDG_CACHE_HOME/lakutata`,否则 `~/.cache/lakutata`) |
83
+ | `LAKUTATA_NWJS_MIRROR` | NW.js 运行时的下载地址(默认 `https://dl.nwjs.io`) |
84
+ | `LAKUTATA_NWJS_VERSIONS` | NW.js 的发布版本列表(默认 `https://nwjs.io/versions.json`) |
85
+ | `LAKUTATA_LIBNODE_MIRROR` | lakutata/libnode 的 Release 的镜像,手机应用的 libnode 从这里下载(`<镜像>/v<版本>/libnode_<platform>_<arch>.tar.gz`) |
86
+ | `LAKUTATA_MOBILE_NODE_DIR` | 手机应用使用自己的 libnode,用 lakutata/libnode 仓库编译(每个系统一个目录:`android`、`ios`、`harmony`),代替 Release 中的 libnode |
78
87
  | `NO_COLOR` | 关闭颜色 |
79
88
 
80
89
  工具运行的脚本和应用,拿到的是工具启动时的环境变量。打包后的应用所用的环境变量见 [Packaging](Packaging.md) 第 6 节。
@@ -8,7 +8,7 @@
8
8
 
9
9
  | 功能 | 需要 |
10
10
  |---|---|
11
- | 全部功能 | Linux,带 runc 的 containerd 1.6 或更高版本,Node.js `^22.13.0 || >=24.11.0`;应用以 root 运行,或属于拥有 containerd 套接字的用户组 |
11
+ | 全部功能 | Linux,带 runc 的 containerd 1.6 或更高版本,Node.js `>=18.10.0`;应用以 root 运行,或属于拥有 containerd 套接字的用户组 |
12
12
  | CNI 网络(包括默认的 `bridge`)、端口发布 | CNI 插件(`bridge`、`portmap`、`host-local` 等,位于 `/opt/cni/bin`、`/usr/lib/cni` 或 `/usr/libexec/cni`,即 `cniPath` 选项)以及 iptables |
13
13
  | 带 `stdin` 的 `exec()`、分开的 `stdout`/`stderr`、终端 | 主机上的 `mkfifo`(没有时,`exec()` 把两种输出都放在 `stdout` 中) |
14
14
  | 构建 | BuildKit 守护进程(`buildkitd`),本地或远程 |
@@ -95,6 +95,8 @@ Application
95
95
 
96
96
  **条件中的 `null` 和 `undefined`。** 默认情况下,where 条件中值为 `null` 或 `undefined` 的属性会被忽略,这与 TypeORM 0.3 相同;TypeORM 1 的默认行为则是抛出错误。所以 `findOneBy({id: undefined})` 会查到表里的第一行:用请求中的值查询之前要先检查它们,或者设置 `invalidWhereValuesBehavior: {undefined: 'throw'}`,让这类值抛出错误(给出的每个键会替换它的默认值)。
97
97
 
98
+ **带别名的路径。** 用应用的别名写的路径(`@data/app.sqlite`、`@app/migrations/*.js`)会在数据库连接时解析:数据库文件(`database`,sql.js 的 `location`),以及实体、迁移和订阅者的 glob。别名由 `.alias()` 声明(见 [Application](Application.md) 第 2 节)。
99
+
98
100
  **用 TypeORM 0.3 创建的数据库。** 如果表名或列名中有非 ASCII 字符,TypeORM 1 给它们的索引和约束起的名字会不同,生成的迁移就会重命名它们。`namingStrategy: new NamingStrategyV03()`(来自 `lakutata/orm`)可以保持 TypeORM 0.3 的命名。
99
101
 
100
102
  **存在 Redis 中的查询缓存**(`cache: {type: 'redis'}` 或 `'ioredis'`)需要安装 `redis` 或 `ioredis` 包。
@@ -21,7 +21,7 @@
21
21
  npm install lakutata pg
22
22
  ```
23
23
 
24
- 没有 `sqlite` 这个类型:SQLite 文件使用 `better-sqlite3`;在无法运行原生模块的环境中使用 `sqljs`。TypeORM 还有面向移动平台的驱动(`capacitor`、`cordova`、`expo`、`nativescript`、`react-native`),lakutata 不以这些平台为目标。
24
+ 没有 `sqlite` 这个类型:SQLite 文件使用 `better-sqlite3`;在无法运行原生模块的环境中使用 `sqljs`。TypeORM 还有面向移动平台 JavaScript 环境的驱动(`capacitor`、`cordova`、`expo`、`nativescript`、`react-native`),lakutata 不使用它们。`mobile-react` 模板的手机应用把 lakutata 应用运行在 Node.js 中,使用本页的驱动(见[手机应用](#手机应用))。
25
25
 
26
26
  ## 所有驱动通用的规则
27
27
 
@@ -124,7 +124,8 @@ export const db = buildDatabaseOptions({
124
124
 
125
125
  - 外键约束是生效的(`PRAGMA foreign_keys = ON`)。只有一个连接:不支持 `poolSize`。
126
126
  - **`bigint` 列以数字返回**:超过 2^53 的值会丢失精度。这类值请存在文本列中。
127
- - `better-sqlite3` 是原生模块,针对某个 Node.js 版本编译。Electron 应用需要针对它的 Electron 版本的构建(`electron-rebuild`,或 better-sqlite3 为 Electron 提供的预编译文件);Bun 无法加载它(在 Bun 上使用 `sqljs`)。
127
+ - `better-sqlite3` 是原生模块,针对某个 Node.js 版本编译。Electron 应用需要针对它的 Electron 版本的构建(`electron-rebuild`,或 better-sqlite3 为 Electron 提供的预编译文件);NW.js 应用会用 NW.js 的头文件重新构建它(需要 0.111.3 及以后的版本:更早的版本请使用 `sqljs`);Bun 无法加载它(在 Bun 上使用 `sqljs`)。
128
+ - 用 Node.js 24.19 及以后版本的头文件从源码编译时,垃圾回收器释放它的对象会让进程中止(`RemoveEnvironmentCleanupHook` 中的 `Assertion failed: (env) != nullptr`,见 [nodejs/node#65446](https://github.com/nodejs/node/issues/65446)),所有基于 `node::ObjectWrap` 的插件都是这样。它的预编译文件是用更早的头文件编译的,不受影响;`lakutata mobile stage` 为手机系统编译插件时,去掉了 `node::ObjectWrap` 中引起这个问题的清理钩子。
128
129
 
129
130
  ## SQLite:sql.js
130
131
 
@@ -150,6 +151,7 @@ export const db = buildDatabaseOptions({
150
151
  | `database` | | 要加载的数据库内容(`Uint8Array`),代替 `location` |
151
152
  | `useLocalForage` | `false` | 在浏览器中保存到 IndexedDB(通过 `localforage`),而不是 `localStorage` |
152
153
  | `sqlJsConfig` | | sql.js 初始化的选项(用 `locateFile` 指定它的 `.wasm` 文件) |
154
+ | `driver` | | 代替 `sql.js` 包自带版本使用的 sql.js 版本:它的 asm.js 版本(`sql.js/dist/sql-asm.js`)不需要 WebAssembly |
153
155
 
154
156
  - 没有 `autoSave` 时,在调用实体管理器(`SqljsEntityManager`)的 `saveDatabase()` 之前,什么都不会写到磁盘。有 `autoSave` 时,事务中的变化在事务提交后保存。
155
157
  - 每次保存都会写入整个文件:sql.js 适合小型数据库。
@@ -259,3 +261,51 @@ export const db = buildDatabaseOptions({
259
261
  - **SAP HANA**(`sap`):`host`、`port`、`username`、`password`、`database`、`schema`,加密连接的 `encrypt` 以及 `key`、`cert`、`ca`,还有 `pool`(`maxConnectedOrPooled`,默认 10)。
260
262
  - **Google Cloud Spanner**(`spanner`):`projectId`、`instanceId`、`databaseId`,以及 `credentials`(`client_email`、`private_key`),不给出时使用运行环境中的凭据。它的类型中从 MySQL 复制来的其他选项不会被读取。
261
263
  - **通过 Data API 访问的 Amazon Aurora**(`aurora-mysql`、`aurora-postgres`):`region`、`secretArn`、`resourceArn` 和 `database` 为必填;查询通过 AWS 的 HTTP API 发送,而不是通过数据库连接。
264
+
265
+ ## 手机应用
266
+
267
+ `mobile-react` 模板的手机应用把 lakutata 应用运行在它内嵌的 Node.js(Node.js 24.20)中,本页的驱动在那里和在电脑上一样可用。下面的结果是在 Android(API 36)、iOS(27.2)和鸿蒙的模拟器上实际验证的:写入数据库,关闭,再重新打开。
268
+
269
+ | 驱动 | Android | iOS | 鸿蒙 |
270
+ |---|---|---|---|
271
+ | `better-sqlite3` | 可以 | 可以 | 可以 |
272
+ | `sqljs` 的 asm.js 版本 | 可以 | 可以 | 可以 |
273
+ | `sqljs` 的 WebAssembly 版本 | 可以 | 可以,但慢得多(polyfill:asm.js 版本用 0.09 秒的操作,它用了 1.5 秒) | 不行:`ReferenceError: WebAssembly is not defined` |
274
+ | `mongo-embedded` | 可以 | 可以 | 可以 |
275
+ | `postgres`、`mysql`、`mongodb`(网络上的服务器) | 可以 | 可以 | 可以 |
276
+
277
+ SQL Server 和 Oracle 还没有在手机上验证过。
278
+
279
+ - `lakutata mobile stage` 用 NDK、Xcode 或 DevEco Studio 为每个系统从源码编译 `better-sqlite3`(见 `@lakutata/cli` README 中的 `lakutata mobile` 命令)。
280
+ - 数据库文件需要长期保存,而 `os.tmpdir()` 是应用的缓存目录,系统可能清空它。模板的 `App.ts` 把应用的数据目录声明为别名 `@data`:Android 上是应用的 files 目录,iOS 上是 `Library/Application Support`,鸿蒙上是 `/data/storage/el2/base/files`,在开发用的电脑上是 `packages/app/data`。数据库的选项可以直接写那里的文件 `@data/app.sqlite`,数据库连接时会解析这个别名。
281
+ - sql.js 的 asm.js 版本是编译成 JavaScript 的 SQLite:不需要 WebAssembly,也没有其他文件,所以在三个系统上都一样能运行。鸿蒙的 Node.js 没有 WebAssembly,iOS 的 Node.js 只有 nodejs-mobile 的 polyfill,慢得多:
282
+
283
+ ```typescript
284
+ import type {ApplicationOptions} from 'lakutata'
285
+ import {buildDatabaseOptions} from 'lakutata/com/database'
286
+ import {createRequire} from 'node:module'
287
+
288
+ /**
289
+ * 应用的选项
290
+ */
291
+ export function config(): ApplicationOptions {
292
+ return {
293
+ id: 'app',
294
+ name: 'app',
295
+ components: {
296
+ db: buildDatabaseOptions({
297
+ type: 'sqljs',
298
+ location: '@data/app.sqlite',
299
+ autoSave: true,
300
+ //用 sql.js 的 asm.js 版本代替它的 WebAssembly 版本
301
+ driver: createRequire(import.meta.url)('sql.js/dist/sql-asm.js'),
302
+ entities: []
303
+ })
304
+ }
305
+ }
306
+ }
307
+ ```
308
+
309
+ - sql.js 1.15 把它的 asm.js 版本标为已弃用:每次加载都会打印一条警告,作者请无法放弃它的用户到 [sql-js/sql.js#635](https://github.com/sql-js/sql.js/issues/635) 说明情况。他们推荐的替代版本 `sql-wasm-inline.js` 需要 WebAssembly,而鸿蒙的 Node.js 没有。应用要长期保存的数据库,最好用 `better-sqlite3`,它在三个系统上都可用。
310
+ - sql.js 的 WebAssembly 版本从它的模块旁边的 `sql-wasm.wasm` 读取,而应用的单文件包里没有这个文件:在 `lakutata.resources` 中把它声明为资源(`from` 是 npm 把 `sql.js` 装在工作区里的位置),再把它在资源中的路径交给 `sqlJsConfig.locateFile`(见 [Packaging](Packaging.md) 第 6 节)。
311
+ - 数据库服务器通过设备的网络连接;在模拟器上,用 `adb reverse`(Android)或 `hdc rport`(鸿蒙)连到电脑。
@@ -56,6 +56,8 @@ Application.run(() => ({
56
56
  | `service` | `socketIOServiceEntrypoint()` | `lakutata/adapter/socket.io` | `socket.io` | [ServiceCalls](ServiceCalls.md) |
57
57
  | `service` | `jsonRPCServiceEntrypoint()` | `lakutata/adapter/jsonrpc` | `ws`(WebSocket 时) | [ServiceCalls](ServiceCalls.md) |
58
58
  | `service` | `electronIPCServiceEntrypoint()` | `lakutata/adapter/electron` | (Electron) | [ServiceCalls](ServiceCalls.md) |
59
+ | `service` | `nwjsServiceEntrypoint()` | `lakutata/adapter/nwjs` | (NW.js) | [ServiceCalls](ServiceCalls.md) |
60
+ | `service` | `reactNativeServiceEntrypoint()` | `lakutata/adapter/react-native` | | [ServiceCalls](ServiceCalls.md) |
59
61
  | `service` | `grpcServiceEntrypoint()` | `@lakutata/grpc/service` | `@lakutata/grpc` | [GRPC](GRPC.md) |
60
62
  | `service` | `natsServiceEntrypoint()` | `@lakutata/nats` | `@lakutata/nats` | [NATS](NATS.md) |
61
63
  | `schedule` | `scheduleEntrypoint()` | `lakutata/com/entrypoint` | | [Schedules](Schedules.md) |
@@ -165,7 +167,7 @@ Application.run(() => ({
165
167
 
166
168
  ## 6. 向客户端推送事件
167
169
 
168
- 保持客户端连接的服务传输方式(Socket.IO、Electron、HTTP 服务的事件流、WebSocket 或 stdio 上的 JSON-RPC、gRPC)可以接收应用推送的事件。`entrypoint.notify(clientId, event, ...args)` 把事件发给一个客户端,即某个动作从 `this.context.clientId` 读到的那个客户端,返回客户端是否收到;`entrypoint.broadcast(event, ...args)` 发给所有已连接的客户端,返回客户端的数量。见 [GRPC](GRPC.md) 和 [ServiceCalls](ServiceCalls.md)。
170
+ 保持客户端连接的服务传输方式(Socket.IO、Electron、NW.js、HTTP 服务的事件流、WebSocket 或 stdio 上的 JSON-RPC、gRPC)可以接收应用推送的事件。`entrypoint.notify(clientId, event, ...args)` 把事件发给一个客户端,即某个动作从 `this.context.clientId` 读到的那个客户端,返回客户端是否收到;`entrypoint.broadcast(event, ...args)` 发给所有已连接的客户端,返回客户端的数量。见 [GRPC](GRPC.md) 和 [ServiceCalls](ServiceCalls.md)。
169
171
 
170
172
  ## 7. 定制入口
171
173
 
@@ -113,7 +113,7 @@ export async function pay(orderId: number, charge: () => Promise<void>): Promise
113
113
  | 传输方式 | 客户端收到 |
114
114
  |---|---|
115
115
  | HTTP | 状态码 `statusCode` 和 `{errno, errMsg}`(见 [HTTP](HTTP.md) 第 6 节) |
116
- | Socket.IO、Electron、HTTP 服务、NATS、gRPC | 完整的错误:它所有的自身属性(见 [ServiceCalls](ServiceCalls.md) 第 3 节) |
116
+ | Socket.IO、Electron、NW.js、HTTP 服务、NATS、gRPC | 完整的错误:它所有的自身属性(见 [ServiceCalls](ServiceCalls.md) 第 3 节) |
117
117
  | JSON-RPC | 一个错误:输入不合法时错误码为 `-32602`,没有匹配的动作时为 `-32601`,应用正在停止时为 `-32001`,其他情况为 `-32000`;完整的错误放在 `data` 中 |
118
118
  | 命令行 | 标准错误中的 `error: <消息>`,退出码为 1(见 [CLI](CLI.md)) |
119
119
  | 定时任务 | 没有客户端:异常交给 `onError`,或者写入日志(见 [Schedules](Schedules.md)) |
package/doc/zh/FFI.md CHANGED
@@ -319,7 +319,7 @@ export class Vendor extends NativeLibrary {
319
319
 
320
320
  ## 12. 运行环境
321
321
 
322
- Node.js `^22.13.0 || >=24.11.0`、Electron 的主进程和 Bun;不支持 Electron 的渲染进程和浏览器。
322
+ Node.js `>=18.10.0`、Electron 的主进程和 Bun;不支持 Electron 的渲染进程和浏览器。
323
323
 
324
324
  这个包的测试(包括库的线程)在 macOS(arm64:Node.js 24、Bun 1.4、Electron 44)、Linux(arm64,glibc 和 musl:Node.js 24、Bun 1.4)和 Windows 11(arm64,以及模拟运行的 x64:Node.js 24、Bun 1.4)上都能通过。在 macOS 上,还用 `lakutata package` 把一个使用它的应用(它的库在自己的线程里执行)分别打成了 bundle、Node.js 和 Bun 的可执行文件以及 Electron 应用:每一个都能运行,库的线程也都从应用自己的资源目录里加载了自带的库。
325
325
 
package/doc/zh/GRPC.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Lakutata gRPC
2
2
 
3
- `@lakutata/grpc` 包让应用通过 gRPC 提供服务动作,也能从另一个应用调用这些动作;任何有 gRPC 库的语言都可以调用它们。它用 TypeScript 基于 Node.js(`^22.13.0 || >=24.11.0`)的 HTTP/2 模块实现,不需要原生插件,也不需要生成代码。`lakutata` 不包含这个包,需要另外安装:
3
+ `@lakutata/grpc` 包让应用通过 gRPC 提供服务动作,也能从另一个应用调用这些动作;任何有 gRPC 库的语言都可以调用它们。它用 TypeScript 基于 Node.js(`>=18.10.0`)的 HTTP/2 模块实现,不需要原生插件,也不需要生成代码。`lakutata` 不包含这个包,需要另外安装:
4
4
 
5
5
  ```bash
6
6
  npm install lakutata @lakutata/grpc
@@ -4,7 +4,15 @@
4
4
 
5
5
  ## 1. 环境要求
6
6
 
7
- - Node.js `^22.13.0 || >=24.11.0`:22.13 及以后的 22.x,或 24.11 及以后的版本(不支持 23)。
7
+ - Node.js 18.10 或更高版本(`>=18.10.0`)。少数功能需要较新的 Node.js:
8
+ - Fastify 适配器:Fastify 5 需要 Node.js 18.19、19.9、20 或更高版本(Express 适配器没有这个要求);
9
+ - `@lakutata/queue` 的 Kafka 传输:需要 Node.js 22.22、24.6 或更高版本,这是 `@platformatic/kafka` 的要求;
10
+ - Node.js 的可执行文件(`lakutata package exe` 和安装包):要用 Node.js 20.12、21.7 或更高版本来生成,或者用 `--node-program` 指定这样一个 Node.js;
11
+ - 对框架的对象使用 `await using`:需要 Node.js 18.18、20.4 或更高版本。
12
+
13
+ 在 Node.js 18 和 19 上,应用要用 commander 13、@inquirer/prompts 7 和 better-sqlite3 11:它们再往后的大版本需要 Node.js 20 或更高版本。
14
+
15
+ `lakutata create` 生成的项目声明的是 Node.js `>=22.13.0`,Web 模板和桌面应用模板用到的工具需要它。
8
16
  - TypeScript,开启它的装饰器(`experimentalDecorators`)及其元数据(`emitDecoratorMetadata`)。框架所需的编译选项以 `lakutata/tsconfig.json` 发布;模板使用 TypeScript 6。
9
17
  - ES 模块(`"type": "module"`),和模板一样;包里同时也提供了 CommonJS 构建。
10
18
 
@@ -33,12 +41,14 @@ curl 'http://localhost:8080/example?name=you'
33
41
  | `web-react`、`web-vue` | 提供 React 或 Vue 单页应用(Vite)的 HTTP API |
34
42
  | `web-next`、`web-nuxt` | 在同一个进程中提供 Next.js 或 Nuxt 应用的 HTTP API |
35
43
  | `electron-react`、`electron-vue` | 桌面应用:Electron,主进程通过 IPC 为 React 或 Vue 渲染进程提供服务 |
44
+ | `nwjs-react`、`nwjs-vue` | 桌面应用:NW.js,后台脚本为隔离在 webview 中的 React 或 Vue 用户界面提供服务 |
45
+ | `mobile-react` | Android、iOS 和鸿蒙手机应用:React Native,应用内的 Node.js 为界面提供服务 |
36
46
 
37
47
  ```bash
38
48
  npx @lakutata/cli create --name shop --template service --with i18n,installer
39
49
  ```
40
50
 
41
- `--with` 加入可选功能:所有模板都有 `i18n`(多语言消息),`cli`、`service` 和 `web-*` 模板还有 `installer`(安装包和系统服务)。在终端里没有给出 `--with` 时,工具会列出这些功能供选择。模板中的文件见 [ProjectStructure](ProjectStructure.md),`lakutata create` 的选项见 [Command](Command.md)。
51
+ `--with` 加入可选功能:所有模板都有 `i18n`(多语言消息),`cli`、`service` 和 `web-*` 模板还有 `installer`(安装包和系统服务),`nwjs-*` 模板还有 `win7`(能在 Windows 7 上运行的 NW.js 0.72)。在终端里没有给出 `--with` 时,工具会列出这些功能供选择。模板中的文件见 [ProjectStructure](ProjectStructure.md),`lakutata create` 的选项见 [Command](Command.md)。
42
52
 
43
53
  `default` 模板的脚本:
44
54
 
package/doc/zh/HTTP.md CHANGED
@@ -101,6 +101,7 @@ HTTP/1.1 400 Bad Request
101
101
  |---|---|---|
102
102
  | 适配器 | `lakutata/adapter/express` 中的 `expressHTTPEntrypoint()` | `lakutata/adapter/fastify` 中的 `fastifyHTTPEntrypoint()` |
103
103
  | 需要安装 | `express`(4 或 5) | `fastify`(5) |
104
+ | Node.js | 18.10 起 | 18.19、19.9、20 起(Fastify 5 需要 `diagnostics_channel.tracingChannel`) |
104
105
 
105
106
  `expressHTTPEntrypoint()` 的选项:
106
107
 
package/doc/zh/I18n.md CHANGED
@@ -164,7 +164,7 @@ Application.run(() => ({
164
164
  | HTTP(Express、Fastify) | Query 参数 `?lang=`、Cookie `lang`,然后是请求头 `Accept-Language`(按权重) |
165
165
  | Socket.IO | 连接握手时的 Query 参数 `lang`、Cookie `lang`、`Accept-Language` |
166
166
  | HTTP 服务适配器、JSON-RPC(HTTP、WebSocket) | HTTP 请求(WebSocket 为建立连接的请求)的 Query 参数 `lang`、Cookie `lang`、`Accept-Language` |
167
- | 命令行、Electron IPC、stdio 上的 JSON-RPC | `environmentVariable` 指定的环境变量(默认没有)、`LANGUAGE`、`LC_ALL`、`LC_MESSAGES`、`LANG`,最后是进程的语言 |
167
+ | 命令行、Electron IPC、NW.js、React Native、stdio 上的 JSON-RPC | `environmentVariable` 指定的环境变量(默认没有)、`LANGUAGE`、`LC_ALL`、`LC_MESSAGES`、`LANG`,最后是进程的语言 |
168
168
  | 定时任务 | 无(默认语言) |
169
169
 
170
170
  不是语言标签的 Query 参数或 Cookie 值会被跳过。