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-TW.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_*` 旗標的按位元 OR 組合 |
|
|
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
|
透過按位元 OR(`|`)組合傳入 `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,18 +166,18 @@ 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
|
-
|
|
169
|
+
html,
|
|
170
|
+
title: "我的應用程式",
|
|
171
|
+
flags: FLAG_FRAMELESS, // 想要自繪標題列就明確加上
|
|
172
|
+
browserUi: BROWSER_UI_ALL,
|
|
173
|
+
service: {
|
|
174
|
+
greet: (name: string) => `hi ${name}`,
|
|
175
|
+
pickFolder: () => app.shell.pickFolder(),
|
|
176
|
+
},
|
|
176
177
|
});
|
|
177
178
|
|
|
178
|
-
await app.wait();
|
|
179
|
-
await app.stop();
|
|
179
|
+
await app.wait(); // 視窗關閉時 resolve
|
|
180
|
+
await app.stop(); // 釋放事件泵與服務;冪等,已關閉時立即回傳
|
|
180
181
|
```
|
|
181
182
|
|
|
182
183
|
傳 `page` 就能把打包好的頁面指令碼交給骨架,不必自己寫那條路由。`createApp` 會應答
|
|
@@ -203,7 +204,7 @@ const app = await createApp({ html, page: code, service });
|
|
|
203
204
|
```ts
|
|
204
205
|
// page.ts —— index.html 的 bundle 內
|
|
205
206
|
import { connect } from "bun-app-window/ipc";
|
|
206
|
-
const api = await connect();
|
|
207
|
+
const api = await connect(); // token 從頁面 URL 的 ?t= 取得
|
|
207
208
|
await api.greet("Ada");
|
|
208
209
|
```
|
|
209
210
|
|
|
@@ -216,6 +217,17 @@ await api.greet("Ada");
|
|
|
216
217
|
- `stop()` 有序且冪等:`shell.stop()` → 等 `EVENT_CLOSED`(帶 5 秒上限)→ `server.stop()`。
|
|
217
218
|
- 起一個視窗事件泵並且只起一個:`app.events`。它同時承擔「等視窗關閉」,
|
|
218
219
|
所以是唯一的事件消費者;`attachPlacementStore` 要用它時傳 `{ events: app.events }`。
|
|
220
|
+
- 把頁面端的例外回傳給宿主。未捕捉例外與未處理的 Promise 拒絕由注入的監聽腳本捕捉,
|
|
221
|
+
經 HTTP 上報——不走 WebSocket:頁面腳本在求值階段崩潰時通道尚未建立,而那正是最需要
|
|
222
|
+
觀測的情形。傳 `onPageError` 接收;不傳時輸出到宿主的 stderr,而不是丟棄。
|
|
223
|
+
- 啟動前校驗 `html` 與 `page` 的一致性。只給其中一個屬於設定錯誤,唯一症狀是視窗正常繪製
|
|
224
|
+
但任何互動都無回應,因此直接拋錯。引用存在但缺少 `type="module"` 時只警告:`<script>`
|
|
225
|
+
的合法寫法不只一種,字串比對的置信度不足以阻斷啟動。
|
|
226
|
+
- 用 `assets` 服務額外的靜態檔案。編譯型框架通常產出不止一個產物(典型是腳本 + 樣式表),
|
|
227
|
+
`assets` 是一張「路徑 → 文字」的映射,`content-type` 由副檔名推斷。此前每個消費者都要
|
|
228
|
+
手寫一段 `routes` 回呼來服務樣式表,而最常見的錯法(MIME 型別寫錯)會讓瀏覽器靜默丟棄
|
|
229
|
+
樣式。未辨識的副檔名回退到 `application/octet-stream` 而不是猜測。呼叫方的 `routes`
|
|
230
|
+
仍然優先,未登記的路徑回傳 404。
|
|
219
231
|
- 啟動失敗不留半啟動狀態:`openShell` 拋錯時,已綁定的埠會被釋放。
|
|
220
232
|
|
|
221
233
|
注意事項:
|
|
@@ -253,28 +265,45 @@ export type Events = { windowState: { maximized: boolean } };
|
|
|
253
265
|
import { createIpcServer } from "bun-app-window/ipc";
|
|
254
266
|
|
|
255
267
|
const ipc = createIpcServer({
|
|
256
|
-
|
|
268
|
+
service: { greet: (name: string) => `Hello ${name}` },
|
|
257
269
|
});
|
|
258
270
|
|
|
259
271
|
serveOnSafePort({
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
272
|
+
fetch(req) {
|
|
273
|
+
/* 處理靜態頁面與驗證 */
|
|
274
|
+
},
|
|
275
|
+
websocket: {
|
|
276
|
+
open: (ws) => ipc.attach(ws),
|
|
277
|
+
message: (ws, raw) => ipc.handle(ws, raw),
|
|
278
|
+
close: (ws) => ipc.detach(ws),
|
|
279
|
+
},
|
|
266
280
|
});
|
|
267
281
|
|
|
268
|
-
ipc.emit("windowState", { maximized: false });
|
|
282
|
+
ipc.emit("windowState", { maximized: false }); // 向頁面主動推播事件
|
|
269
283
|
```
|
|
270
284
|
|
|
285
|
+
#### 遲連線的狀態投遞
|
|
286
|
+
|
|
287
|
+
頁面在視窗建立**之後**才載入。宿主從 `createApp` 回傳、到頁面的 WebSocket 建立之間發出的
|
|
288
|
+
任何事件,都投遞給零個連線,也不會被重播——只從事件讀取初始狀態的頁面會一直是空值,
|
|
289
|
+
而兩端都不會報錯。
|
|
290
|
+
|
|
291
|
+
把「狀態類」事件標為黏性,服務端就會在每次連線建立時重播最後一份酬載:
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
ipc.emit("windowState", { maximized: false }, { sticky: true }); // attach() 時重播
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
黏性是**明確開啟**的,且每個事件名稱只保留一份酬載,不是歷史佇列。語意上是「通知」而非
|
|
298
|
+
「狀態」的事件不應開啟:遲到的連線會額外收到一條讀起來像新訊息的舊訊息。
|
|
299
|
+
|
|
271
300
|
```ts
|
|
272
301
|
// 頁面端
|
|
273
302
|
import { connect } from "bun-app-window/ipc";
|
|
274
303
|
import type { Api, Events } from "./api";
|
|
275
304
|
|
|
276
305
|
const api = await connect<Api, Events>();
|
|
277
|
-
await api.greet("Ada");
|
|
306
|
+
await api.greet("Ada"); // 回傳型別自動推導為 string
|
|
278
307
|
api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
279
308
|
```
|
|
280
309
|
|
|
@@ -307,17 +336,21 @@ const api = await connect<Api, Events>({ timeoutMs: 0 });
|
|
|
307
336
|
用戶端是一個 `Proxy`:有幾個名稱會解析到用戶端自己身上,永遠到不了遠端方法。服務表裡若有
|
|
308
337
|
同名方法,**那個服務端方法就永遠呼叫不到**——頁面呼叫的是用戶端自己的成員,兩端都不會報錯。
|
|
309
338
|
|
|
310
|
-
| 名稱
|
|
311
|
-
|
|
|
312
|
-
| `on` / `off` | 事件訂閱輔助函式
|
|
313
|
-
| `close()`
|
|
314
|
-
| `connected`
|
|
315
|
-
| `pending`
|
|
316
|
-
| `then`
|
|
339
|
+
| 名稱 | 頁面實際拿到的東西 |
|
|
340
|
+
| ------------ | ------------------------------------------------------------------- |
|
|
341
|
+
| `on` / `off` | 事件訂閱輔助函式 |
|
|
342
|
+
| `close()` | 關閉 WebSocket 通道——之後所有呼叫都會被 reject |
|
|
343
|
+
| `connected` | `boolean`,通道目前是否可用 |
|
|
344
|
+
| `pending` | `number`,尚未收到回覆的呼叫數 |
|
|
345
|
+
| `then` | `undefined`,否則 `await client` 會把這個永不兌現的 thenable 等下去 |
|
|
317
346
|
|
|
318
347
|
Symbol 鍵同樣保留。給服務方法換個名稱(`winClose` 而不是 `close`),在頁面端映射回去 ——
|
|
319
348
|
見下方的標題列一節。
|
|
320
349
|
|
|
350
|
+
上述失效在執行期完全不可觀測,因此 `createIpcServer` 在**建立時就會拋錯**:服務表裡出現這些
|
|
351
|
+
名稱即終止,錯誤訊息指出衝突的鍵並給出替代命名。只有當服務表由外部輸入產生、且已用其他手段
|
|
352
|
+
驗證過時,才應傳 `{ rejectReservedNames: false }` 關閉本檢查。
|
|
353
|
+
|
|
321
354
|
### `bun-app-window/titlebar` — Windows 11 風格標題列
|
|
322
355
|
|
|
323
356
|
用於在無邊框模式下快速掛載符合 Windows 11 視覺規範的自訂標題列。IPC 服務是**扁平的**——
|
|
@@ -327,12 +360,12 @@ Symbol 鍵同樣保留。給服務方法換個名稱(`winClose` 而不是 `clo
|
|
|
327
360
|
```ts
|
|
328
361
|
// 宿主端 —— 扁平服務;`close` 是 IPC 用戶端的保留名稱
|
|
329
362
|
createIpcServer({
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
363
|
+
service: {
|
|
364
|
+
minimize: () => shell.minimize(),
|
|
365
|
+
toggleMaximize: () => shell.toggleMaximize(),
|
|
366
|
+
winClose: () => shell.close(),
|
|
367
|
+
drag: () => shell.drag(),
|
|
368
|
+
},
|
|
336
369
|
});
|
|
337
370
|
```
|
|
338
371
|
|
|
@@ -341,14 +374,14 @@ createIpcServer({
|
|
|
341
374
|
import { mountTitleBar, bridgeFromWindowService } from "bun-app-window/titlebar";
|
|
342
375
|
|
|
343
376
|
const bar = mountTitleBar({
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
377
|
+
title: "我的應用程式",
|
|
378
|
+
icon: "/icon.svg",
|
|
379
|
+
bridge: bridgeFromWindowService({
|
|
380
|
+
minimize: () => api.minimize(),
|
|
381
|
+
toggleMaximize: () => api.toggleMaximize(),
|
|
382
|
+
close: () => api.winClose(), // 橋接時再映射回 close
|
|
383
|
+
drag: () => api.drag(),
|
|
384
|
+
}),
|
|
352
385
|
});
|
|
353
386
|
|
|
354
387
|
api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
@@ -376,7 +409,7 @@ if (saved) restorePlacement(shell, saved);
|
|
|
376
409
|
|
|
377
410
|
shell.show();
|
|
378
411
|
|
|
379
|
-
const store = attachPlacementStore(shell, FILE);
|
|
412
|
+
const store = attachPlacementStore(shell, FILE); // 訂閱視窗事件,拖曳停下後寫入一次
|
|
380
413
|
process.on("beforeExit", () => store.flush());
|
|
381
414
|
```
|
|
382
415
|
|
|
@@ -410,8 +443,10 @@ const store = attachPlacementStore(app.shell, FILE, { events: app.events });
|
|
|
410
443
|
import { EVENT_CLOSED, EVENT_STATE } from "bun-app-window/events";
|
|
411
444
|
|
|
412
445
|
for (const e of shell.pollEvents()) {
|
|
413
|
-
|
|
414
|
-
|
|
446
|
+
if (e.type === EVENT_CLOSED) break; // 視窗已銷毀,原生單例已釋放
|
|
447
|
+
if (e.type === EVENT_STATE && e.maximized) {
|
|
448
|
+
/* ... */
|
|
449
|
+
}
|
|
415
450
|
}
|
|
416
451
|
```
|
|
417
452
|
|
|
@@ -422,7 +457,9 @@ for (const e of shell.pollEvents()) {
|
|
|
422
457
|
import { EVENT_CLOSED, attachEventPump } from "bun-app-window/events";
|
|
423
458
|
|
|
424
459
|
const pump = attachEventPump(shell, { intervalMs: 16 });
|
|
425
|
-
pump.on((e) => {
|
|
460
|
+
pump.on((e) => {
|
|
461
|
+
if (e.type === EVENT_CLOSED) pump.stop();
|
|
462
|
+
});
|
|
426
463
|
pump.onGap((missing) => console.warn(`漏了 ${missing} 條事件`));
|
|
427
464
|
```
|
|
428
465
|
|
|
@@ -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,26 @@ 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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
630
|
+
e.preventDefault();
|
|
631
|
+
await api.showContextMenu(
|
|
632
|
+
Math.round(e.screenX),
|
|
633
|
+
Math.round(e.screenY),
|
|
634
|
+
"open|開啟\nrefresh|重新整理\n-\nquit|結束",
|
|
635
|
+
);
|
|
549
636
|
});
|
|
550
637
|
|
|
551
638
|
// 頁面端監聽選單點擊結果
|
|
552
639
|
window.addEventListener("shell-msg", (e) => {
|
|
553
|
-
|
|
640
|
+
if (e.detail.t === "menu") console.log("選取選單項目 ID:", e.detail.id);
|
|
554
641
|
});
|
|
555
642
|
```
|
|
556
643
|
|
|
@@ -568,7 +655,7 @@ const shell = await openShell({ url, flags: FLAG_ACCEPT_DROP });
|
|
|
568
655
|
```js
|
|
569
656
|
// 頁面端接收路徑清單
|
|
570
657
|
window.addEventListener("shell-msg", (e) => {
|
|
571
|
-
|
|
658
|
+
if (e.detail.t === "drop") console.log("拖入檔案路徑:", e.detail.paths);
|
|
572
659
|
});
|
|
573
660
|
```
|
|
574
661
|
|
|
@@ -576,10 +663,10 @@ window.addEventListener("shell-msg", (e) => {
|
|
|
576
663
|
|
|
577
664
|
```js
|
|
578
665
|
window.addEventListener("shell-msg", (e) => {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
666
|
+
if (e.detail.t !== "drop") return;
|
|
667
|
+
for (const p of e.detail.paths) {
|
|
668
|
+
console.log(e.detail.dirs.includes(p) ? "資料夾" : "檔案", p);
|
|
669
|
+
}
|
|
583
670
|
});
|
|
584
671
|
```
|
|
585
672
|
|
|
@@ -590,14 +677,27 @@ window.addEventListener("shell-msg", (e) => {
|
|
|
590
677
|
視窗外殼層不會向頁面隱式注入任何 DOM 或全域樣式。為消除「網頁感」,建議應用程式於前端自行補充以下基礎規則:
|
|
591
678
|
|
|
592
679
|
```css
|
|
593
|
-
html,
|
|
594
|
-
body {
|
|
595
|
-
|
|
596
|
-
|
|
680
|
+
html,
|
|
681
|
+
body {
|
|
682
|
+
cursor: default;
|
|
683
|
+
}
|
|
684
|
+
body {
|
|
685
|
+
user-select: none;
|
|
686
|
+
}
|
|
687
|
+
input,
|
|
688
|
+
textarea,
|
|
689
|
+
.log {
|
|
690
|
+
user-select: text;
|
|
691
|
+
cursor: auto;
|
|
692
|
+
}
|
|
693
|
+
button:focus-visible {
|
|
694
|
+
outline: 2px solid var(--accent);
|
|
695
|
+
outline-offset: 2px;
|
|
696
|
+
}
|
|
597
697
|
```
|
|
598
698
|
|
|
599
699
|
```html
|
|
600
|
-
<input type="text" spellcheck="false" autocomplete="off" autocapitalize="off"
|
|
700
|
+
<input type="text" spellcheck="false" autocomplete="off" autocapitalize="off" />
|
|
601
701
|
```
|
|
602
702
|
|
|
603
703
|
尤其建議為輸入框設定 `spellcheck="false"`,否則瀏覽器拼字檢查極易在檔案路徑或專業數值下方繪製紅色波浪底線,破壞桌面應用程式的原生觀感。
|
|
@@ -608,7 +708,7 @@ button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
|
608
708
|
|
|
609
709
|
```ts
|
|
610
710
|
import { createShell } from "bun-app-window";
|
|
611
|
-
import { embeddedDll } from "bun-app-window/embedded";
|
|
711
|
+
import { embeddedDll } from "bun-app-window/embedded"; // 將 DLL 位元組內嵌至封裝產物
|
|
612
712
|
|
|
613
713
|
const shell = await createShell({ url, embedded: embeddedDll });
|
|
614
714
|
```
|
|
@@ -647,18 +747,18 @@ bun build --compile --windows-hide-console src/index.ts --outfile dist/app.exe
|
|
|
647
747
|
|
|
648
748
|
### 內建快速鍵與滑鼠行為
|
|
649
749
|
|
|
650
|
-
| 觸發方式
|
|
651
|
-
|
|
|
652
|
-
| `Ctrl +` / `Ctrl -` / `Ctrl 0` | 頁面縮放:放大一階 / 縮小一階 / 重設為 100%
|
|
653
|
-
| `Ctrl + 滑鼠滾輪`
|
|
654
|
-
| `F11`
|
|
655
|
-
| `F5` / `Ctrl + R`
|
|
656
|
-
| `F12`
|
|
657
|
-
| `Alt + Space` / 標題列右鍵
|
|
750
|
+
| 觸發方式 | 預設行為 |
|
|
751
|
+
| ------------------------------ | ---------------------------------------------- |
|
|
752
|
+
| `Ctrl +` / `Ctrl -` / `Ctrl 0` | 頁面縮放:放大一階 / 縮小一階 / 重設為 100% |
|
|
753
|
+
| `Ctrl + 滑鼠滾輪` | 頁面縮放:放大 / 縮小 |
|
|
754
|
+
| `F11` | 切換全螢幕模式(隱藏視窗邊框並覆蓋系統工作列) |
|
|
755
|
+
| `F5` / `Ctrl + R` | 重新載入目前頁面 |
|
|
756
|
+
| `F12` | 開啟 WebView2 開發人員工具 |
|
|
757
|
+
| `Alt + Space` / 標題列右鍵 | 彈出 Windows 系統視窗選單 |
|
|
658
758
|
|
|
659
759
|
## 開發與驗證
|
|
660
760
|
|
|
661
|
-
本專案工具鏈基於 Bun 1.4.
|
|
761
|
+
本專案工具鏈基於 Bun 1.4.4 與 TypeScript 7.0.2 建構,`tsconfig.json` 開啟全量嚴格模式檢查。
|
|
662
762
|
|
|
663
763
|
**本專案不使用遠端 CI。** 該帳戶的 GitHub Actions 因 billing lock 無法分配 runner;而專案本身是單平台、單人開發、零外部服務相依的相依套件,遠端 CI 的邊際價值低於其維護成本,因此品質把關改為本機執行。建議入口:
|
|
664
764
|
|
|
@@ -685,10 +785,10 @@ bun run demo # 啟動互動式自我檢測範例程式(需於桌面工
|
|
|
685
785
|
|
|
686
786
|
自動化測試或截圖腳本若需透過 Win32 `EnumWindows` 過濾視窗控制代碼(HWND),請依據視窗模式比對對應的視窗類別名稱(Window Class):
|
|
687
787
|
|
|
688
|
-
| 視窗模式
|
|
689
|
-
|
|
|
690
|
-
| 標準有邊框模式(未傳 `FLAG_FRAMELESS`) | `webview`
|
|
691
|
-
| 無邊框模式(傳入 `FLAG_FRAMELESS`)
|
|
788
|
+
| 視窗模式 | 對應視窗類別名稱 |
|
|
789
|
+
| --------------------------------------- | ---------------- |
|
|
790
|
+
| 標準有邊框模式(未傳 `FLAG_FRAMELESS`) | `webview` |
|
|
791
|
+
| 無邊框模式(傳入 `FLAG_FRAMELESS`) | `webview_shell` |
|
|
692
792
|
|
|
693
793
|
## 致謝
|
|
694
794
|
|