bun-app-window 0.7.0 → 0.8.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/CHANGELOG.md +264 -73
- package/README.md +247 -142
- package/README.zh-CN.md +238 -142
- package/README.zh-TW.md +241 -141
- package/package.json +9 -5
- package/src/shell/app.ts +255 -2
- package/src/shell/assets.d.ts +4 -4
- package/src/shell/embedded.ts +9 -10
- package/src/shell/index.ts +301 -318
- package/src/shell/ipc.ts +91 -33
- package/src/shell/native.ts +61 -64
- package/src/shell/pack.ts +247 -28
- package/src/shell/port.ts +37 -38
- package/src/shell/resolve.ts +121 -140
- package/src/shell/state.ts +8 -19
- package/src/shell/types.ts +79 -81
- package/src/ui/titlebar.ts +1 -2
package/README.zh-CN.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# bun-app-window
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/bun-app-window)
|
|
4
|
-
[](https://bun.sh)
|
|
5
5
|
[](#系统要求与设计边界)
|
|
6
6
|
[](#系统要求与设计边界)
|
|
7
7
|
[](#系统要求与设计边界)
|
|
@@ -26,13 +26,13 @@ await openShell({ url: `http://127.0.0.1:${server.port}`, flags: FLAG_FRAMELESS
|
|
|
26
26
|
|
|
27
27
|
在引入本项目前,请确认以下技术前提与架构取舍是否符合你的应用场景:
|
|
28
28
|
|
|
29
|
-
| 维度
|
|
30
|
-
|
|
|
31
|
-
| 仅支持 Windows x64
|
|
32
|
-
| 仅支持 Bun 运行时
|
|
29
|
+
| 维度 | 限制与说明 |
|
|
30
|
+
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
31
|
+
| 仅支持 Windows x64 | `package.json` 中的 `os` 与 `cpu` 字段已做硬性限制,拒绝在其他平台安装(包名保持平台中立仅为预留命名空间)。预编译动态库 `libshell.dll` 随包分发,无需在安装期编译 |
|
|
32
|
+
| 仅支持 Bun 运行时 | 包内直接发布 TypeScript 源码,要求 Bun ≥ 1.4.4。Node.js 环境虽可完成包安装,但无法直接运行 |
|
|
33
33
|
| 类型检查要求 TypeScript ≥ 7.0.2 | API 在 TypeScript 7.0.2 下设计与检查,`devDependencies` 中精确锁版;不为更旧的编译器做兼容。高于 7.0.2 的结果未作断言——`bun-types` 只在那一侧才打开 `typesVersions: ">= 7.1"` 门控,`with { type: "text" }` 的解析结果会随之改变 |
|
|
34
|
-
| 类型检查需自备 `@types/bun`
|
|
35
|
-
| 基于 `bun:ffi` 构建
|
|
34
|
+
| 类型检查需自备 `@types/bun` | 包内是 `.ts` 源文件,消费方的 `bun check` 会一并编译它们。需执行 `bun add -d @types/bun`,否则类型检查会在 `node_modules` 内报 `TS2868: Cannot find name 'Bun'` |
|
|
35
|
+
| 基于 `bun:ffi` 构建 | `bun:ffi` 在 Bun 中仍标记为实验性特性(Experimental)。官方建议生产环境优先采用 Node-API,本项目选择 `bun:ffi` 是为了换取零第三方依赖的轻量架构 |
|
|
36
36
|
|
|
37
37
|
运行环境与能力边界:
|
|
38
38
|
|
|
@@ -48,7 +48,7 @@ bun add bun-app-window
|
|
|
48
48
|
|
|
49
49
|
### 包内包含什么
|
|
50
50
|
|
|
51
|
-
发布的 tarball 只有预编译的 `dist/libshell.dll` 与 TypeScript 源码,共 22 个文件(0.
|
|
51
|
+
发布的 tarball 只有预编译的 `dist/libshell.dll` 与 TypeScript 源码,共 22 个文件(0.50 MB,解包后 1.47 MB)。原生 C++ 层(`native/`)与上游 vendored 头文件(`vendor/`)**不随包分发**:它们只在重编 `libshell.dll` 时需要,而重编必须 clone 仓库。因此 `bun run build:native` 与 `build:exe` 是本仓库专用脚本,装进 `node_modules` 后执行没有意义。
|
|
52
52
|
|
|
53
53
|
## 快速开始
|
|
54
54
|
|
|
@@ -56,22 +56,23 @@ bun add bun-app-window
|
|
|
56
56
|
import { openShell, serveOnSafePort, FLAG_FRAMELESS, FLAG_CENTER } from "bun-app-window";
|
|
57
57
|
|
|
58
58
|
const server = serveOnSafePort({
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
fetch: () =>
|
|
60
|
+
new Response("<h1>Hello</h1>", {
|
|
61
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
62
|
+
}),
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
const shell = await openShell({
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
url: `http://127.0.0.1:${server.port}`,
|
|
67
|
+
title: "My App",
|
|
68
|
+
width: 1000,
|
|
69
|
+
height: 680,
|
|
70
|
+
flags: FLAG_FRAMELESS | FLAG_CENTER,
|
|
71
|
+
minWidth: 480,
|
|
72
|
+
minHeight: 320,
|
|
72
73
|
});
|
|
73
74
|
|
|
74
|
-
shell.post(JSON.stringify({ t: "ready" }));
|
|
75
|
+
shell.post(JSON.stringify({ t: "ready" })); // 宿主 → 页面单向消息
|
|
75
76
|
shell.setTitle("Doing work…");
|
|
76
77
|
```
|
|
77
78
|
|
|
@@ -83,45 +84,45 @@ shell.setTitle("Doing work…");
|
|
|
83
84
|
|
|
84
85
|
### 实例方法与属性
|
|
85
86
|
|
|
86
|
-
| 成员
|
|
87
|
-
|
|
|
88
|
-
| `start()`
|
|
89
|
-
| `stop()`
|
|
90
|
-
| `show()`
|
|
91
|
-
| `isAlive()`
|
|
92
|
-
| `pollEvents(max?)`
|
|
93
|
-
| `state()`
|
|
94
|
-
| `lastError()`
|
|
95
|
-
| `setTitle` / `minimize` / `toggleMaximize` / `close` | 基础窗口控制操作,返回状态码 `rc`
|
|
96
|
-
| `setMinSize(w, h)`
|
|
97
|
-
| `drag()`
|
|
98
|
-
| `beginResize(edge)`
|
|
99
|
-
| `post(json)`
|
|
100
|
-
| `setZoom(f)` / `getZoom()`
|
|
101
|
-
| `reload()` / `openDevTools()` / `toggleFullscreen()` | 页面重载、打开开发者工具、切换全屏(分别对应 `Ctrl+R` / `F12` / `F11`)
|
|
102
|
-
| `setFrameless(frameless)` / `isFrameless()`
|
|
103
|
-
| `setBrowserUi(mask)`
|
|
104
|
-
| `showContextMenu(x, y, items)`
|
|
105
|
-
| `pickFolder(title?)`
|
|
106
|
-
| `libInfo`
|
|
107
|
-
| `symbols`
|
|
87
|
+
| 成员 | 说明 |
|
|
88
|
+
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
89
|
+
| `start()` | 创建并启动窗口,返回 `{ rc, error }`。调用后立即返回,UI 消息循环运行于独立原生线程 |
|
|
90
|
+
| `stop()` | 销毁窗口并释放底层资源,支持幂等重复调用 |
|
|
91
|
+
| `show()` | 显示窗口。常配合 `FLAG_DEFER_SHOW` 使用,实现在页面渲染就绪后再展现窗口 |
|
|
92
|
+
| `isAlive()` | 检测窗口是否存活(注:`state()` 无法区分「尚未启动」与「已销毁」状态)。等窗口关闭请用 `pollEvents()` 的 `CLOSED`,别轮询它 |
|
|
93
|
+
| `pollEvents(max?)` | 批量拉取窗口生命周期事件(`STATE` / `CLOSED`),同步、消费式。详见下文「窗口事件」 |
|
|
94
|
+
| `state()` | 获取当前窗口状态:`{ maximized, minimized, focused, dark, raw }` |
|
|
95
|
+
| `lastError()` | 获取最近一次操作失败的错误信息,无错误时返回空字符串 |
|
|
96
|
+
| `setTitle` / `minimize` / `toggleMaximize` / `close` | 基础窗口控制操作,返回状态码 `rc` |
|
|
97
|
+
| `setMinSize(w, h)` | 设置窗口最小尺寸(单位:逻辑像素) |
|
|
98
|
+
| `drag()` | 触发原生窗口拖拽(在页面 `pointerdown` 事件中调用,完整保留 Windows 贴边分屏特性) |
|
|
99
|
+
| `beginResize(edge)` | 触发原生边框缩放(在页面 `pointermove` 事件中调用,`edge` 取值见 `RESIZE_EDGE.*`) |
|
|
100
|
+
| `post(json)` | 宿主向页面单向投递 JSON 消息,异步发送且不等待返回结果 |
|
|
101
|
+
| `setZoom(f)` / `getZoom()` | 设置或获取页面缩放比例(有效范围 `0.25`–`3.0`,越界值自动钳制) |
|
|
102
|
+
| `reload()` / `openDevTools()` / `toggleFullscreen()` | 页面重载、打开开发者工具、切换全屏(分别对应 `Ctrl+R` / `F12` / `F11`) |
|
|
103
|
+
| `setFrameless(frameless)` / `isFrameless()` | 运行时切换窗口形态并读取当前形态。只有**创建时**给了 `FLAG_FRAMELESS` 的窗口能切;带边框起点的窗口返回 `rc=-1`,理由在 `lastError()`。详见下文「窗口标志位」 |
|
|
104
|
+
| `setBrowserUi(mask)` | 禁用指定的浏览器默认行为,传入 `BROWSER_UI_*` 标志位的按位或组合 |
|
|
105
|
+
| `showContextMenu(x, y, items)` | 在指定屏幕坐标弹出 Win32 原生上下文菜单(详见下文用法) |
|
|
106
|
+
| `pickFolder(title?)` | 调用原生文件夹选择对话框(同步阻塞当前调用线程;**经 IPC 暴露时务必配 `timeoutMs: 0`**,见下文) |
|
|
107
|
+
| `libInfo` | 原生动态库解析元数据(包含实际路径、加载来源、SHA-256 校验值) |
|
|
108
|
+
| `symbols` | 底层直通接口:直接访问通过 `dlopen` 导出的底层 C 符号 |
|
|
108
109
|
|
|
109
110
|
### 窗口标志位(Flags)
|
|
110
111
|
|
|
111
112
|
通过按位或(`|`)组合传入 `opts.flags`:
|
|
112
113
|
|
|
113
|
-
| 标志位
|
|
114
|
-
|
|
|
115
|
-
| `FLAG_FRAMELESS`
|
|
116
|
-
| `FLAG_RESIZABLE`
|
|
117
|
-
| `FLAG_CENTER`
|
|
118
|
-
| `FLAG_TOPMOST`
|
|
119
|
-
| `FLAG_DEFER_SHOW`
|
|
120
|
-
| `FLAG_ACCEPT_DROP`
|
|
121
|
-
| `FLAG_START_MAXIMIZED`
|
|
122
|
-
| `FLAG_ALLOW_EXTERNAL_NAV` | 放弃外部导航拦截——用于本身就是浏览器的应用
|
|
123
|
-
| `FLAG_TITLEBAR_RESERVE`
|
|
124
|
-
| `FLAG_NATIVE_CAPTIONS`
|
|
114
|
+
| 标志位 | 作用说明 |
|
|
115
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
116
|
+
| `FLAG_FRAMELESS` | 无边框窗口模式,移除系统默认标题栏,交由前端页面自行绘制。以这种方式创建的窗口可在运行时用 `setFrameless(false)` 切回带边框 |
|
|
117
|
+
| `FLAG_RESIZABLE` | 允许用户调整窗口大小 |
|
|
118
|
+
| `FLAG_CENTER` | 启动时在当前显示器工作区居中显示(有边框与无边框模式均生效) |
|
|
119
|
+
| `FLAG_TOPMOST` | 窗口始终置顶 |
|
|
120
|
+
| `FLAG_DEFER_SHOW` | 创建窗口后保持隐藏状态,等待显式调用 `show()` |
|
|
121
|
+
| `FLAG_ACCEPT_DROP` | 允许窗口接收外部文件或文件夹拖放事件 |
|
|
122
|
+
| `FLAG_START_MAXIMIZED` | 窗口首次显示即处于最大化状态,避免从普通尺寸跳变到最大化的中间闪烁帧 |
|
|
123
|
+
| `FLAG_ALLOW_EXTERNAL_NAV` | 放弃外部导航拦截——用于本身就是浏览器的应用 |
|
|
124
|
+
| `FLAG_TITLEBAR_RESERVE` | 无边框模式下把网页内容整体下移一个标题栏高度,让顶部那条归原生窗口。**已被 `FLAG_NATIVE_CAPTIONS` 取代**:caption 命中区固定在客户区 `y ∈ [0, 32)`,这条路会把页面自绘的标题栏放到与点击真正落点相差 32 px 的位置 |
|
|
125
|
+
| `FLAG_NATIVE_CAPTIONS` | 无边框模式下,由壳层自己的 layered 子窗口绘制三个 caption 按钮并在那里应答 `HTMAXBUTTON`。这是 Snap Layouts 唯一能触发的途径:WebView2 的渲染子窗口在另一个进程里,恒回 `HTCLIENT`。必须与 `mountTitleBar({ captions: "native" })` 配套,页面才会留出 `46 × 3` 而不是自己画一套 |
|
|
125
126
|
|
|
126
127
|
### 外部导航默认被拦截
|
|
127
128
|
|
|
@@ -165,19 +166,19 @@ import htmlText from "./index.html" with { type: "text" };
|
|
|
165
166
|
const html = htmlText as unknown as string;
|
|
166
167
|
|
|
167
168
|
const app = await createApp({
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
169
|
+
html,
|
|
170
|
+
title: "我的应用",
|
|
171
|
+
flags: FLAG_FRAMELESS, // 想要自绘标题栏就显式加
|
|
172
|
+
browserUi: BROWSER_UI_ALL,
|
|
173
|
+
service: {
|
|
174
|
+
greet: (name: string) => `hi ${name}`,
|
|
175
|
+
// 人机交互的调用耗时不可控:页面侧必须 connect({ timeoutMs: 0 }),见 ipc 一节
|
|
176
|
+
pickFolder: () => app.shell.pickFolder(),
|
|
177
|
+
},
|
|
177
178
|
});
|
|
178
179
|
|
|
179
|
-
await app.wait();
|
|
180
|
-
await app.stop();
|
|
180
|
+
await app.wait(); // 窗口关闭时 resolve
|
|
181
|
+
await app.stop(); // 释放事件泵与服务;幂等,已关闭时立即返回
|
|
181
182
|
```
|
|
182
183
|
|
|
183
184
|
传 `page` 就能把打包好的页面脚本交给骨架,不必自己写那条路由。`createApp` 会应答
|
|
@@ -204,7 +205,7 @@ const app = await createApp({ html, page: code, service });
|
|
|
204
205
|
```ts
|
|
205
206
|
// page.ts —— index.html 的 bundle 里
|
|
206
207
|
import { connect } from "bun-app-window/ipc";
|
|
207
|
-
const api = await connect();
|
|
208
|
+
const api = await connect(); // token 从页面 URL 的 ?t= 取
|
|
208
209
|
await api.greet("Ada");
|
|
209
210
|
```
|
|
210
211
|
|
|
@@ -217,6 +218,17 @@ await api.greet("Ada");
|
|
|
217
218
|
- `stop()` 有序且幂等:`shell.stop()` → 等 `EVENT_CLOSED`(带 5 秒上限)→ `server.stop()`。
|
|
218
219
|
- 起一个窗口事件泵并且只起一个:`app.events`。它同时承担「等窗口关闭」,
|
|
219
220
|
所以是唯一的事件消费者;`attachPlacementStore` 要用它时传 `{ events: app.events }`。
|
|
221
|
+
- 把页面侧的异常回传给宿主。未捕获异常与未处理的 Promise 拒绝由注入的监听脚本捕获,
|
|
222
|
+
经 HTTP 上报——不走 WebSocket:页面脚本在求值阶段崩溃时通道尚未建立,而那正是最需要
|
|
223
|
+
观测的情形。传 `onPageError` 接收;不传时输出到宿主的 stderr,而不是丢弃。
|
|
224
|
+
- 启动前校验 `html` 与 `page` 的一致性。只给其中一个属于配置错误,唯一症状是窗口正常渲染
|
|
225
|
+
但任何交互都无响应,因此直接抛错。引用存在但缺少 `type="module"` 时只告警:`<script>`
|
|
226
|
+
的合法写法不止一种,字符串匹配的置信度不足以阻断启动。
|
|
227
|
+
- 用 `assets` 服务额外的静态文件。编译型框架通常产出不止一个产物(典型是脚本 + 样式表),
|
|
228
|
+
`assets` 是一张「路径 → 文本」的映射,`content-type` 由扩展名推断。此前每个消费者都要
|
|
229
|
+
手写一段 `routes` 回调来服务样式表,而最常见的错法(MIME 类型写错)会让浏览器静默丢弃
|
|
230
|
+
样式。未识别的扩展名回退到 `application/octet-stream` 而不是猜测。调用方的 `routes`
|
|
231
|
+
仍然优先,未登记的路径返回 404。
|
|
220
232
|
- 启动失败不留半启动状态:`openShell` 抛错时,已绑定的端口会被释放。
|
|
221
233
|
|
|
222
234
|
注意事项:
|
|
@@ -254,28 +266,45 @@ export type Events = { windowState: { maximized: boolean } };
|
|
|
254
266
|
import { createIpcServer } from "bun-app-window/ipc";
|
|
255
267
|
|
|
256
268
|
const ipc = createIpcServer({
|
|
257
|
-
|
|
269
|
+
service: { greet: (name: string) => `Hello ${name}` },
|
|
258
270
|
});
|
|
259
271
|
|
|
260
272
|
serveOnSafePort({
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
273
|
+
fetch(req) {
|
|
274
|
+
/* 处理静态页面与鉴权 */
|
|
275
|
+
},
|
|
276
|
+
websocket: {
|
|
277
|
+
open: (ws) => ipc.attach(ws),
|
|
278
|
+
message: (ws, raw) => ipc.handle(ws, raw),
|
|
279
|
+
close: (ws) => ipc.detach(ws),
|
|
280
|
+
},
|
|
267
281
|
});
|
|
268
282
|
|
|
269
|
-
ipc.emit("windowState", { maximized: false });
|
|
283
|
+
ipc.emit("windowState", { maximized: false }); // 向页面主动推送事件
|
|
270
284
|
```
|
|
271
285
|
|
|
286
|
+
#### 迟连接的状态投递
|
|
287
|
+
|
|
288
|
+
页面在窗口创建**之后**才加载。宿主从 `createApp` 返回、到页面的 WebSocket 建立之间发出的
|
|
289
|
+
任何事件,都投递给了零个连接,也不会被回放——只从事件读取初始状态的页面会一直是空值,
|
|
290
|
+
而两端都不报错。
|
|
291
|
+
|
|
292
|
+
把「状态类」事件标为粘性,服务端就会在每次连接建立时回放最后一份载荷:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
ipc.emit("windowState", { maximized: false }, { sticky: true }); // attach() 时回放
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
粘性是**显式开启**的,且每个事件名只保留一份载荷,不是历史队列。语义上是「通知」而非
|
|
299
|
+
「状态」的事件不应开启:迟到的连接会额外收到一条读起来像新消息的旧消息。
|
|
300
|
+
|
|
272
301
|
```ts
|
|
273
302
|
// 页面端
|
|
274
303
|
import { connect } from "bun-app-window/ipc";
|
|
275
304
|
import type { Api, Events } from "./api";
|
|
276
305
|
|
|
277
306
|
const api = await connect<Api, Events>();
|
|
278
|
-
await api.greet("Ada");
|
|
307
|
+
await api.greet("Ada"); // 返回类型自动推导为 string
|
|
279
308
|
api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
280
309
|
```
|
|
281
310
|
|
|
@@ -308,17 +337,21 @@ const api = await connect<Api, Events>({ timeoutMs: 0 });
|
|
|
308
337
|
客户端是一个 `Proxy`:有几个名字会解析到客户端自己身上,永远到不了远端方法。服务表里若有
|
|
309
338
|
同名方法,**那个服务端方法就永远调不到**——页面调的是客户端自己的成员,两端都不会报错。
|
|
310
339
|
|
|
311
|
-
| 名字
|
|
312
|
-
|
|
|
313
|
-
| `on` / `off` | 事件订阅辅助函数
|
|
314
|
-
| `close()`
|
|
315
|
-
| `connected`
|
|
316
|
-
| `pending`
|
|
317
|
-
| `then`
|
|
340
|
+
| 名字 | 页面实际拿到的东西 |
|
|
341
|
+
| ------------ | ------------------------------------------------------------------- |
|
|
342
|
+
| `on` / `off` | 事件订阅辅助函数 |
|
|
343
|
+
| `close()` | 关闭 WebSocket 通道——之后所有调用都会被 reject |
|
|
344
|
+
| `connected` | `boolean`,通道当前是否可用 |
|
|
345
|
+
| `pending` | `number`,尚未收到回复的调用数 |
|
|
346
|
+
| `then` | `undefined`,否则 `await client` 会把这个永不兑现的 thenable 等下去 |
|
|
318
347
|
|
|
319
348
|
Symbol 键同样保留。给服务方法换个名字(`winClose` 而不是 `close`),在页面侧映射回去 ——
|
|
320
349
|
见下面的标题栏一节。
|
|
321
350
|
|
|
351
|
+
上述失效在运行期完全不可观测,因此 `createIpcServer` 在**创建时就会抛错**:服务表里出现这些
|
|
352
|
+
名字即终止,错误信息指出冲突的键并给出替代命名。只有当服务表由外部输入生成、且已用其他手段
|
|
353
|
+
校验过时,才应传 `{ rejectReservedNames: false }` 关闭本检查。
|
|
354
|
+
|
|
322
355
|
### `bun-app-window/titlebar` — Windows 11 风格标题栏
|
|
323
356
|
|
|
324
357
|
用于在无边框模式下快速挂载符合 Windows 11 视觉规范的自定义标题栏。IPC 服务是**扁平的**——
|
|
@@ -328,12 +361,12 @@ Symbol 键同样保留。给服务方法换个名字(`winClose` 而不是 `clo
|
|
|
328
361
|
```ts
|
|
329
362
|
// 宿主端 —— 扁平服务;`close` 是 IPC 客户端的保留名
|
|
330
363
|
createIpcServer({
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
364
|
+
service: {
|
|
365
|
+
minimize: () => shell.minimize(),
|
|
366
|
+
toggleMaximize: () => shell.toggleMaximize(),
|
|
367
|
+
winClose: () => shell.close(),
|
|
368
|
+
drag: () => shell.drag(),
|
|
369
|
+
},
|
|
337
370
|
});
|
|
338
371
|
```
|
|
339
372
|
|
|
@@ -342,14 +375,14 @@ createIpcServer({
|
|
|
342
375
|
import { mountTitleBar, bridgeFromWindowService } from "bun-app-window/titlebar";
|
|
343
376
|
|
|
344
377
|
const bar = mountTitleBar({
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
378
|
+
title: "我的应用",
|
|
379
|
+
icon: "/icon.svg",
|
|
380
|
+
bridge: bridgeFromWindowService({
|
|
381
|
+
minimize: () => api.minimize(),
|
|
382
|
+
toggleMaximize: () => api.toggleMaximize(),
|
|
383
|
+
close: () => api.winClose(), // 桥接时再映射回 close
|
|
384
|
+
drag: () => api.drag(),
|
|
385
|
+
}),
|
|
353
386
|
});
|
|
354
387
|
|
|
355
388
|
api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
@@ -377,7 +410,7 @@ if (saved) restorePlacement(shell, saved);
|
|
|
377
410
|
|
|
378
411
|
shell.show();
|
|
379
412
|
|
|
380
|
-
const store = attachPlacementStore(shell, FILE);
|
|
413
|
+
const store = attachPlacementStore(shell, FILE); // 订阅窗口事件,拖动停手后写一次盘
|
|
381
414
|
process.on("beforeExit", () => store.flush());
|
|
382
415
|
```
|
|
383
416
|
|
|
@@ -411,8 +444,10 @@ const store = attachPlacementStore(app.shell, FILE, { events: app.events });
|
|
|
411
444
|
import { EVENT_CLOSED, EVENT_STATE } from "bun-app-window/events";
|
|
412
445
|
|
|
413
446
|
for (const e of shell.pollEvents()) {
|
|
414
|
-
|
|
415
|
-
|
|
447
|
+
if (e.type === EVENT_CLOSED) break; // 窗口已销毁,原生单例已释放
|
|
448
|
+
if (e.type === EVENT_STATE && e.maximized) {
|
|
449
|
+
/* ... */
|
|
450
|
+
}
|
|
416
451
|
}
|
|
417
452
|
```
|
|
418
453
|
|
|
@@ -423,7 +458,9 @@ for (const e of shell.pollEvents()) {
|
|
|
423
458
|
import { EVENT_CLOSED, attachEventPump } from "bun-app-window/events";
|
|
424
459
|
|
|
425
460
|
const pump = attachEventPump(shell, { intervalMs: 16 });
|
|
426
|
-
pump.on((e) => {
|
|
461
|
+
pump.on((e) => {
|
|
462
|
+
if (e.type === EVENT_CLOSED) pump.stop();
|
|
463
|
+
});
|
|
427
464
|
pump.onGap((missing) => console.warn(`漏了 ${missing} 条事件`));
|
|
428
465
|
```
|
|
429
466
|
|
|
@@ -442,8 +479,8 @@ pump.onGap((missing) => console.warn(`漏了 ${missing} 条事件`));
|
|
|
442
479
|
import { debugOf } from "bun-app-window/debug";
|
|
443
480
|
|
|
444
481
|
const d = debugOf(shell);
|
|
445
|
-
d.metrics();
|
|
446
|
-
d.dpi();
|
|
482
|
+
d.metrics(); // → "pos=196,196 win=1960x1400 client=1960x1400 work=3072x1824 border=13x13 round=1"
|
|
483
|
+
d.dpi(); // → "awareness=-1 dpi=192 scale=200%"
|
|
447
484
|
d.hittest(x, y);
|
|
448
485
|
```
|
|
449
486
|
|
|
@@ -484,6 +521,56 @@ await writePageBundle({ entry: "src/ui/page.ts", out: "src/ui/page.bundle.txt" }
|
|
|
484
521
|
`bytes` 是 UTF-8 字节数,不是 `string.length`;源码里只要有非 ASCII 文本两者就不同,
|
|
485
522
|
而一个「看起来对」的体积读数比没有读数更糟。
|
|
486
523
|
|
|
524
|
+
#### 编译型框架:`plugins`、`define`、`loader`
|
|
525
|
+
|
|
526
|
+
这三个字段原样交给 `Bun.build`,与任何具体框架无关,本包不自带任何插件。
|
|
527
|
+
|
|
528
|
+
```ts
|
|
529
|
+
const built = await bundlePage({
|
|
530
|
+
entry: "src/main.ts",
|
|
531
|
+
plugins: [myFrameworkPlugin()], // .vue / .svelte / .tsx —— 插件能编译什么就传什么
|
|
532
|
+
define: { __VUE_OPTIONS_API__: "false" }, // 编译期常量
|
|
533
|
+
loader: { ".thing": "ts" }, // 自定义扩展名
|
|
534
|
+
});
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
#### `alias` 由本包实现,因为 Bun 没有这个选项
|
|
538
|
+
|
|
539
|
+
Bun 1.4.4 的 `BuildConfig` 没有 `alias` 属性,而且传进去不会报错——它被静默忽略。
|
|
540
|
+
四种写法都试过,全部解析失败。因此本包用自带的 `onResolve` 插件支撑 `alias`:
|
|
541
|
+
|
|
542
|
+
```ts
|
|
543
|
+
await bundlePage({ entry, alias: { "@": "./src", lib: "./src/lib" } });
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
| 模块说明符 | 是否重写 |
|
|
547
|
+
| ------------------------------- | -------- |
|
|
548
|
+
| 与某个键相等,或以 `键/` 为前缀 | 是 |
|
|
549
|
+
| 以 `.` 或 `/` 开头 | 否 |
|
|
550
|
+
| 带 `scheme:` 前缀 | 否 |
|
|
551
|
+
|
|
552
|
+
**默认是空的。** `$lib` 是 SvelteKit 的约定,`@/` 是某些项目自己的 `tsconfig` 别名,
|
|
553
|
+
两者都不属于本包。别名与调用方的插件是合并关系而非替换,且只在传入 `alias` 时才注册该插件。
|
|
554
|
+
|
|
555
|
+
#### 多产物
|
|
556
|
+
|
|
557
|
+
编译型框架经常产出不止一个产物——脚本加样式表是常见形态。`outputs` 列出全部,每项带
|
|
558
|
+
`path` / `kind` / `code` / `bytes`,而 `code` 与 `bytes` 特指 `entry-point` 那一个
|
|
559
|
+
(不是 `outputs[0]`,输出顺序不构成契约)。
|
|
560
|
+
|
|
561
|
+
`writePageBundle` 写出每一个产物:入口写到 `out`,其余写到同目录并替换扩展名。
|
|
562
|
+
所以 `page.bundle.txt` 会得到 `page.bundle.css`,**不是** `main.css`——入口的文件名不该
|
|
563
|
+
渗进一个 HTML 与 `assets` 会引用的路径。它返回 `written: Array<{ path, bytes }>`,
|
|
564
|
+
便于清理时删掉自己产生的每一个文件。
|
|
565
|
+
|
|
566
|
+
```text
|
|
567
|
+
$ bun run bundle:page
|
|
568
|
+
109,888 B dist/page.bundle.txt ← 入口产物
|
|
569
|
+
133 B dist/page.bundle.css ← 附带产物
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
其余文件用 `createApp({ assets })` 服务——见上文的 `app` 小节。
|
|
573
|
+
|
|
487
574
|
失败时抛错并带上 Bun 的完整诊断。两种形态都覆盖了:编译期问题以
|
|
488
575
|
`{ success: false, logs }` 返回,而入口本身不可达时**抛 `AggregateError`**,
|
|
489
576
|
它的 `message` 只有一句 `Bundle failed`,细节在 `.errors` 上。
|
|
@@ -514,13 +601,13 @@ const shell = await openShell({ url, browserUi: BROWSER_UI_ALL });
|
|
|
514
601
|
|
|
515
602
|
也可按需组合以下标志位:
|
|
516
603
|
|
|
517
|
-
| 标志位
|
|
518
|
-
|
|
|
519
|
-
| `BROWSER_UI_MENU`
|
|
520
|
-
| `BROWSER_UI_KEYS`
|
|
521
|
-
| `BROWSER_UI_STATUSBAR` | 左下角链接悬停状态栏
|
|
522
|
-
| `BROWSER_UI_SWIPE`
|
|
523
|
-
| `BROWSER_UI_AUTOFILL`
|
|
604
|
+
| 标志位 | 禁用的浏览器行为 |
|
|
605
|
+
| ---------------------- | ----------------------------------------------------- |
|
|
606
|
+
| `BROWSER_UI_MENU` | 默认网页右键菜单(Edge 上下文菜单) |
|
|
607
|
+
| `BROWSER_UI_KEYS` | 浏览器内置快捷键(如 `Ctrl+P` / `Ctrl+F` / `Ctrl+S`) |
|
|
608
|
+
| `BROWSER_UI_STATUSBAR` | 左下角链接悬停状态栏 |
|
|
609
|
+
| `BROWSER_UI_SWIPE` | 触控板双指横滑前进 / 后退手势 |
|
|
610
|
+
| `BROWSER_UI_AUTOFILL` | 表单自动填充提示 |
|
|
524
611
|
|
|
525
612
|
注:`F11`、`F12` 及 `Ctrl±` 缩放由窗口壳层直接接管,不受上述标志位影响。
|
|
526
613
|
|
|
@@ -531,26 +618,22 @@ const shell = await openShell({ url, browserUi: BROWSER_UI_ALL });
|
|
|
531
618
|
```ts
|
|
532
619
|
// 宿主端:把菜单能力挂到扁平服务上
|
|
533
620
|
createIpcServer({
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
621
|
+
service: {
|
|
622
|
+
showContextMenu: (x: number, y: number, items: string) => shell.showContextMenu(x, y, items),
|
|
623
|
+
},
|
|
537
624
|
});
|
|
538
625
|
```
|
|
539
626
|
|
|
540
627
|
```ts
|
|
541
628
|
// 页面端触发菜单
|
|
542
629
|
document.addEventListener("contextmenu", async (e) => {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
Math.round(e.screenX),
|
|
546
|
-
Math.round(e.screenY),
|
|
547
|
-
"open|打开\nrefresh|刷新\n-\nquit|退出"
|
|
548
|
-
);
|
|
630
|
+
e.preventDefault();
|
|
631
|
+
await api.showContextMenu(Math.round(e.screenX), Math.round(e.screenY), "open|打开\nrefresh|刷新\n-\nquit|退出");
|
|
549
632
|
});
|
|
550
633
|
|
|
551
634
|
// 页面端监听菜单点击结果
|
|
552
635
|
window.addEventListener("shell-msg", (e) => {
|
|
553
|
-
|
|
636
|
+
if (e.detail.t === "menu") console.log("选中菜单项 ID:", e.detail.id);
|
|
554
637
|
});
|
|
555
638
|
```
|
|
556
639
|
|
|
@@ -568,7 +651,7 @@ const shell = await openShell({ url, flags: FLAG_ACCEPT_DROP });
|
|
|
568
651
|
```js
|
|
569
652
|
// 页面端接收路径列表
|
|
570
653
|
window.addEventListener("shell-msg", (e) => {
|
|
571
|
-
|
|
654
|
+
if (e.detail.t === "drop") console.log("拖入文件路径:", e.detail.paths);
|
|
572
655
|
});
|
|
573
656
|
```
|
|
574
657
|
|
|
@@ -576,10 +659,10 @@ window.addEventListener("shell-msg", (e) => {
|
|
|
576
659
|
|
|
577
660
|
```js
|
|
578
661
|
window.addEventListener("shell-msg", (e) => {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
662
|
+
if (e.detail.t !== "drop") return;
|
|
663
|
+
for (const p of e.detail.paths) {
|
|
664
|
+
console.log(e.detail.dirs.includes(p) ? "文件夹" : "文件", p);
|
|
665
|
+
}
|
|
583
666
|
});
|
|
584
667
|
```
|
|
585
668
|
|
|
@@ -590,14 +673,27 @@ window.addEventListener("shell-msg", (e) => {
|
|
|
590
673
|
窗口壳层不会向页面隐式注入任何 DOM 或全局样式。为消除「网页感」,建议应用在前端自行补充以下基础规则:
|
|
591
674
|
|
|
592
675
|
```css
|
|
593
|
-
html,
|
|
594
|
-
body {
|
|
595
|
-
|
|
596
|
-
|
|
676
|
+
html,
|
|
677
|
+
body {
|
|
678
|
+
cursor: default;
|
|
679
|
+
}
|
|
680
|
+
body {
|
|
681
|
+
user-select: none;
|
|
682
|
+
}
|
|
683
|
+
input,
|
|
684
|
+
textarea,
|
|
685
|
+
.log {
|
|
686
|
+
user-select: text;
|
|
687
|
+
cursor: auto;
|
|
688
|
+
}
|
|
689
|
+
button:focus-visible {
|
|
690
|
+
outline: 2px solid var(--accent);
|
|
691
|
+
outline-offset: 2px;
|
|
692
|
+
}
|
|
597
693
|
```
|
|
598
694
|
|
|
599
695
|
```html
|
|
600
|
-
<input type="text" spellcheck="false" autocomplete="off" autocapitalize="off"
|
|
696
|
+
<input type="text" spellcheck="false" autocomplete="off" autocapitalize="off" />
|
|
601
697
|
```
|
|
602
698
|
|
|
603
699
|
尤其建议为输入框设置 `spellcheck="false"`,否则浏览器拼写检查极易在文件路径或专业数值下绘制红色波浪线,破坏桌面应用的原生观感。
|
|
@@ -608,7 +704,7 @@ button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
|
608
704
|
|
|
609
705
|
```ts
|
|
610
706
|
import { createShell } from "bun-app-window";
|
|
611
|
-
import { embeddedDll } from "bun-app-window/embedded";
|
|
707
|
+
import { embeddedDll } from "bun-app-window/embedded"; // 将 DLL 字节内嵌至打包产物
|
|
612
708
|
|
|
613
709
|
const shell = await createShell({ url, embedded: embeddedDll });
|
|
614
710
|
```
|
|
@@ -647,18 +743,18 @@ bun build --compile --windows-hide-console src/index.ts --outfile dist/app.exe
|
|
|
647
743
|
|
|
648
744
|
### 内置快捷键与鼠标行为
|
|
649
745
|
|
|
650
|
-
| 触发方式
|
|
651
|
-
|
|
|
652
|
-
| `Ctrl +` / `Ctrl -` / `Ctrl 0` | 页面缩放:放大一档 / 缩小一档 / 重置为 100%
|
|
653
|
-
| `Ctrl + 鼠标滚轮`
|
|
654
|
-
| `F11`
|
|
655
|
-
| `F5` / `Ctrl + R`
|
|
656
|
-
| `F12`
|
|
657
|
-
| `Alt + Space` / 标题栏右键
|
|
746
|
+
| 触发方式 | 默认行为 |
|
|
747
|
+
| ------------------------------ | -------------------------------------------- |
|
|
748
|
+
| `Ctrl +` / `Ctrl -` / `Ctrl 0` | 页面缩放:放大一档 / 缩小一档 / 重置为 100% |
|
|
749
|
+
| `Ctrl + 鼠标滚轮` | 页面缩放:放大 / 缩小 |
|
|
750
|
+
| `F11` | 切换全屏模式(隐藏窗口边框并覆盖系统任务栏) |
|
|
751
|
+
| `F5` / `Ctrl + R` | 重新加载当前页面 |
|
|
752
|
+
| `F12` | 打开 WebView2 开发者工具 |
|
|
753
|
+
| `Alt + Space` / 标题栏右键 | 弹出 Windows 系统窗口菜单 |
|
|
658
754
|
|
|
659
755
|
## 开发与验证
|
|
660
756
|
|
|
661
|
-
本项目工具链基于 Bun 1.4.
|
|
757
|
+
本项目工具链基于 Bun 1.4.4 与 TypeScript 7.0.2 构建,`tsconfig.json` 开启全量严格模式检查。
|
|
662
758
|
|
|
663
759
|
**本项目不使用远端 CI。** 该账户的 GitHub Actions 因 billing lock 无法分配 runner;而项目本身是单平台、单人开发、零外部服务依赖的依赖包,远端 CI 的边际价值低于其维护成本,因此质量门禁改为本地执行。推荐入口:
|
|
664
760
|
|
|
@@ -685,10 +781,10 @@ bun run demo # 启动交互式自检示例程序(需在桌面会话下
|
|
|
685
781
|
|
|
686
782
|
自动化测试或截图脚本若需通过 Win32 `EnumWindows` 过滤窗口句柄,请根据窗口模式匹配对应的窗口类名(Window Class):
|
|
687
783
|
|
|
688
|
-
| 窗口模式
|
|
689
|
-
|
|
|
690
|
-
| 标准带边框模式(未传 `FLAG_FRAMELESS`) | `webview`
|
|
691
|
-
| 无边框模式(传入 `FLAG_FRAMELESS`)
|
|
784
|
+
| 窗口模式 | 对应窗口类名 |
|
|
785
|
+
| --------------------------------------- | --------------- |
|
|
786
|
+
| 标准带边框模式(未传 `FLAG_FRAMELESS`) | `webview` |
|
|
787
|
+
| 无边框模式(传入 `FLAG_FRAMELESS`) | `webview_shell` |
|
|
692
788
|
|
|
693
789
|
## 致谢
|
|
694
790
|
|