bun-app-window 0.6.2 → 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 +302 -63
- package/README.md +265 -146
- package/README.zh-CN.md +249 -147
- package/README.zh-TW.md +252 -146
- package/dist/libshell.dll +0 -0
- 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 +280 -279
- package/src/shell/ipc.ts +91 -33
- package/src/shell/native.ts +64 -62
- 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 原始碼,共
|
|
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
|
|
|
@@ -79,44 +80,49 @@ shell.setTitle("Doing work…");
|
|
|
79
80
|
|
|
80
81
|
## 核心 API
|
|
81
82
|
|
|
83
|
+
`ackOf(shell, rc)` 把裸狀態碼轉成 `{ rc, err }`,並且**只在失敗時才讀 `lastError()`**——原生錯誤槽不會被成功呼叫清空,成功之後再讀會拿到上一條殘留,看起來像新的失敗。
|
|
84
|
+
|
|
82
85
|
### 實例方法與屬性
|
|
83
86
|
|
|
84
|
-
| 成員
|
|
85
|
-
|
|
|
86
|
-
| `start()`
|
|
87
|
-
| `stop()`
|
|
88
|
-
| `show()`
|
|
89
|
-
| `isAlive()`
|
|
90
|
-
| `pollEvents(max?)`
|
|
91
|
-
| `state()`
|
|
92
|
-
| `lastError()`
|
|
93
|
-
| `setTitle` / `minimize` / `toggleMaximize` / `close` | 基礎視窗控制操作,回傳狀態碼 `rc`
|
|
94
|
-
| `setMinSize(w, h)`
|
|
95
|
-
| `drag()`
|
|
96
|
-
| `beginResize(edge)`
|
|
97
|
-
| `post(json)`
|
|
98
|
-
| `setZoom(f)` / `getZoom()`
|
|
99
|
-
| `reload()` / `openDevTools()` / `toggleFullscreen()` | 頁面重新載入、開啟開發人員工具、切換全螢幕(分別對應 `Ctrl+R` / `F12` / `F11`)
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
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 符號 |
|
|
105
109
|
|
|
106
110
|
### 視窗旗標(Flags)
|
|
107
111
|
|
|
108
112
|
透過按位元 OR(`|`)組合傳入 `opts.flags`:
|
|
109
113
|
|
|
110
|
-
| 旗標
|
|
111
|
-
|
|
|
112
|
-
| `FLAG_FRAMELESS`
|
|
113
|
-
| `FLAG_RESIZABLE`
|
|
114
|
-
| `FLAG_CENTER`
|
|
115
|
-
| `FLAG_TOPMOST`
|
|
116
|
-
| `FLAG_DEFER_SHOW`
|
|
117
|
-
| `FLAG_ACCEPT_DROP`
|
|
118
|
-
| `FLAG_START_MAXIMIZED`
|
|
119
|
-
| `FLAG_ALLOW_EXTERNAL_NAV` | 放棄外部導覽攔截——用於本身就是瀏覽器的應用程式
|
|
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` 而不是自己畫一套 |
|
|
120
126
|
|
|
121
127
|
### 外部導覽預設被攔截
|
|
122
128
|
|
|
@@ -160,18 +166,18 @@ import htmlText from "./index.html" with { type: "text" };
|
|
|
160
166
|
const html = htmlText as unknown as string;
|
|
161
167
|
|
|
162
168
|
const app = await createApp({
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
},
|
|
171
177
|
});
|
|
172
178
|
|
|
173
|
-
await app.wait();
|
|
174
|
-
await app.stop();
|
|
179
|
+
await app.wait(); // 視窗關閉時 resolve
|
|
180
|
+
await app.stop(); // 釋放事件泵與服務;冪等,已關閉時立即回傳
|
|
175
181
|
```
|
|
176
182
|
|
|
177
183
|
傳 `page` 就能把打包好的頁面指令碼交給骨架,不必自己寫那條路由。`createApp` 會應答
|
|
@@ -187,7 +193,7 @@ const { code } = await bundlePage({ entry: "src/ui/page.ts" });
|
|
|
187
193
|
const app = await createApp({ html, page: code, service });
|
|
188
194
|
```
|
|
189
195
|
|
|
190
|
-
`App`
|
|
196
|
+
`App` 上另有兩個成員,各能省掉一次呼叫和一條子路徑匯入:
|
|
191
197
|
|
|
192
198
|
- `app.debug` —— 與 `debugOf(app.shell)` 回傳同一個物件。同樣不在穩定性承諾範圍內。
|
|
193
199
|
- `app.attachPlacement(file)` —— 把位置持久化接到骨架的**唯一**事件泵上。自己呼叫
|
|
@@ -198,7 +204,7 @@ const app = await createApp({ html, page: code, service });
|
|
|
198
204
|
```ts
|
|
199
205
|
// page.ts —— index.html 的 bundle 內
|
|
200
206
|
import { connect } from "bun-app-window/ipc";
|
|
201
|
-
const api = await connect();
|
|
207
|
+
const api = await connect(); // token 從頁面 URL 的 ?t= 取得
|
|
202
208
|
await api.greet("Ada");
|
|
203
209
|
```
|
|
204
210
|
|
|
@@ -211,6 +217,17 @@ await api.greet("Ada");
|
|
|
211
217
|
- `stop()` 有序且冪等:`shell.stop()` → 等 `EVENT_CLOSED`(帶 5 秒上限)→ `server.stop()`。
|
|
212
218
|
- 起一個視窗事件泵並且只起一個:`app.events`。它同時承擔「等視窗關閉」,
|
|
213
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。
|
|
214
231
|
- 啟動失敗不留半啟動狀態:`openShell` 拋錯時,已綁定的埠會被釋放。
|
|
215
232
|
|
|
216
233
|
注意事項:
|
|
@@ -248,28 +265,45 @@ export type Events = { windowState: { maximized: boolean } };
|
|
|
248
265
|
import { createIpcServer } from "bun-app-window/ipc";
|
|
249
266
|
|
|
250
267
|
const ipc = createIpcServer({
|
|
251
|
-
|
|
268
|
+
service: { greet: (name: string) => `Hello ${name}` },
|
|
252
269
|
});
|
|
253
270
|
|
|
254
271
|
serveOnSafePort({
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
+
},
|
|
261
280
|
});
|
|
262
281
|
|
|
263
|
-
ipc.emit("windowState", { maximized: false });
|
|
282
|
+
ipc.emit("windowState", { maximized: false }); // 向頁面主動推播事件
|
|
264
283
|
```
|
|
265
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
|
+
|
|
266
300
|
```ts
|
|
267
301
|
// 頁面端
|
|
268
302
|
import { connect } from "bun-app-window/ipc";
|
|
269
303
|
import type { Api, Events } from "./api";
|
|
270
304
|
|
|
271
305
|
const api = await connect<Api, Events>();
|
|
272
|
-
await api.greet("Ada");
|
|
306
|
+
await api.greet("Ada"); // 回傳型別自動推導為 string
|
|
273
307
|
api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
274
308
|
```
|
|
275
309
|
|
|
@@ -302,17 +336,21 @@ const api = await connect<Api, Events>({ timeoutMs: 0 });
|
|
|
302
336
|
用戶端是一個 `Proxy`:有幾個名稱會解析到用戶端自己身上,永遠到不了遠端方法。服務表裡若有
|
|
303
337
|
同名方法,**那個服務端方法就永遠呼叫不到**——頁面呼叫的是用戶端自己的成員,兩端都不會報錯。
|
|
304
338
|
|
|
305
|
-
| 名稱
|
|
306
|
-
|
|
|
307
|
-
| `on` / `off` | 事件訂閱輔助函式
|
|
308
|
-
| `close()`
|
|
309
|
-
| `connected`
|
|
310
|
-
| `pending`
|
|
311
|
-
| `then`
|
|
339
|
+
| 名稱 | 頁面實際拿到的東西 |
|
|
340
|
+
| ------------ | ------------------------------------------------------------------- |
|
|
341
|
+
| `on` / `off` | 事件訂閱輔助函式 |
|
|
342
|
+
| `close()` | 關閉 WebSocket 通道——之後所有呼叫都會被 reject |
|
|
343
|
+
| `connected` | `boolean`,通道目前是否可用 |
|
|
344
|
+
| `pending` | `number`,尚未收到回覆的呼叫數 |
|
|
345
|
+
| `then` | `undefined`,否則 `await client` 會把這個永不兌現的 thenable 等下去 |
|
|
312
346
|
|
|
313
347
|
Symbol 鍵同樣保留。給服務方法換個名稱(`winClose` 而不是 `close`),在頁面端映射回去 ——
|
|
314
348
|
見下方的標題列一節。
|
|
315
349
|
|
|
350
|
+
上述失效在執行期完全不可觀測,因此 `createIpcServer` 在**建立時就會拋錯**:服務表裡出現這些
|
|
351
|
+
名稱即終止,錯誤訊息指出衝突的鍵並給出替代命名。只有當服務表由外部輸入產生、且已用其他手段
|
|
352
|
+
驗證過時,才應傳 `{ rejectReservedNames: false }` 關閉本檢查。
|
|
353
|
+
|
|
316
354
|
### `bun-app-window/titlebar` — Windows 11 風格標題列
|
|
317
355
|
|
|
318
356
|
用於在無邊框模式下快速掛載符合 Windows 11 視覺規範的自訂標題列。IPC 服務是**扁平的**——
|
|
@@ -322,12 +360,12 @@ Symbol 鍵同樣保留。給服務方法換個名稱(`winClose` 而不是 `clo
|
|
|
322
360
|
```ts
|
|
323
361
|
// 宿主端 —— 扁平服務;`close` 是 IPC 用戶端的保留名稱
|
|
324
362
|
createIpcServer({
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
363
|
+
service: {
|
|
364
|
+
minimize: () => shell.minimize(),
|
|
365
|
+
toggleMaximize: () => shell.toggleMaximize(),
|
|
366
|
+
winClose: () => shell.close(),
|
|
367
|
+
drag: () => shell.drag(),
|
|
368
|
+
},
|
|
331
369
|
});
|
|
332
370
|
```
|
|
333
371
|
|
|
@@ -336,14 +374,14 @@ createIpcServer({
|
|
|
336
374
|
import { mountTitleBar, bridgeFromWindowService } from "bun-app-window/titlebar";
|
|
337
375
|
|
|
338
376
|
const bar = mountTitleBar({
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
+
}),
|
|
347
385
|
});
|
|
348
386
|
|
|
349
387
|
api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
@@ -352,7 +390,9 @@ api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
|
352
390
|
實作細節:
|
|
353
391
|
|
|
354
392
|
- 原生拖曳體驗:拖曳操作透過呼叫 `shell.drag()` 交由底層發送 `WM_NCLBUTTONDOWN`(`HTCAPTION`)訊息處理,完整保留 Windows 11 貼齊版面配置(Snap Layouts)與連按兩下最大化等系統級行為。
|
|
355
|
-
-
|
|
393
|
+
- 樣式自訂:色彩與尺寸均透過 CSS 變數(`--ws-tb-*`)控制,覆寫變數即可實作佈景主題切換。若需自行注入樣式表而不是讓 `mountTitleBar` 注入一次,可單獨呼叫匯出的 `titleBarCss()`。
|
|
394
|
+
- 字形選擇:預設走 `detectIconSet()`,它向 `document.fonts` 詢問 `Segoe Fluent Icons`(Win 11)或 `Segoe MDL2 Assets`(Win 10),有一個在就用 `fluent`——兩套字型的碼位相同,一張表涵蓋兩代系統。只有兩個字型都探測不到時才退回 `unicode` 幾何符號(`─ □ ❐ ✕`)。要覆寫探測結果就明確傳 `icons: "unicode" | "fluent"`。
|
|
395
|
+
- 交給殼層繪製按鈕:傳 `captions: "native"`,元件**一個按鈕都不渲染**,只在右側留出 `46 × 3` 給殼層的 layered 子視窗。必須與 `FLAG_NATIVE_CAPTIONS` 搭配——只開一半的結果是兩套按鈕疊在一起,或者一條點不到的空白區。注意這三個按鈕上的滑鼠點擊走**非客戶區**訊息,所以 `bridge.minimize` / `toggleMaximize` / `close` 在滑鼠這條路上不會被呼叫,動作由殼層自己執行;鍵盤 Tab + Enter 仍然經過 bridge。
|
|
356
396
|
|
|
357
397
|
### `bun-app-window/state` — 視窗位置與尺寸持久化
|
|
358
398
|
|
|
@@ -369,7 +409,7 @@ if (saved) restorePlacement(shell, saved);
|
|
|
369
409
|
|
|
370
410
|
shell.show();
|
|
371
411
|
|
|
372
|
-
const store = attachPlacementStore(shell, FILE);
|
|
412
|
+
const store = attachPlacementStore(shell, FILE); // 訂閱視窗事件,拖曳停下後寫入一次
|
|
373
413
|
process.on("beforeExit", () => store.flush());
|
|
374
414
|
```
|
|
375
415
|
|
|
@@ -403,8 +443,10 @@ const store = attachPlacementStore(app.shell, FILE, { events: app.events });
|
|
|
403
443
|
import { EVENT_CLOSED, EVENT_STATE } from "bun-app-window/events";
|
|
404
444
|
|
|
405
445
|
for (const e of shell.pollEvents()) {
|
|
406
|
-
|
|
407
|
-
|
|
446
|
+
if (e.type === EVENT_CLOSED) break; // 視窗已銷毀,原生單例已釋放
|
|
447
|
+
if (e.type === EVENT_STATE && e.maximized) {
|
|
448
|
+
/* ... */
|
|
449
|
+
}
|
|
408
450
|
}
|
|
409
451
|
```
|
|
410
452
|
|
|
@@ -415,7 +457,9 @@ for (const e of shell.pollEvents()) {
|
|
|
415
457
|
import { EVENT_CLOSED, attachEventPump } from "bun-app-window/events";
|
|
416
458
|
|
|
417
459
|
const pump = attachEventPump(shell, { intervalMs: 16 });
|
|
418
|
-
pump.on((e) => {
|
|
460
|
+
pump.on((e) => {
|
|
461
|
+
if (e.type === EVENT_CLOSED) pump.stop();
|
|
462
|
+
});
|
|
419
463
|
pump.onGap((missing) => console.warn(`漏了 ${missing} 條事件`));
|
|
420
464
|
```
|
|
421
465
|
|
|
@@ -435,8 +479,8 @@ pump.onGap((missing) => console.warn(`漏了 ${missing} 條事件`));
|
|
|
435
479
|
import { debugOf } from "bun-app-window/debug";
|
|
436
480
|
|
|
437
481
|
const d = debugOf(shell);
|
|
438
|
-
d.metrics();
|
|
439
|
-
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%"
|
|
440
484
|
d.hittest(x, y);
|
|
441
485
|
```
|
|
442
486
|
|
|
@@ -445,8 +489,7 @@ d.hittest(x, y);
|
|
|
445
489
|
|
|
446
490
|
### `bun-app-window/types` — 常數面,瀏覽器安全
|
|
447
491
|
|
|
448
|
-
一個零依賴模組,承載全部常數:`FLAG_
|
|
449
|
-
`ZOOM_LADDER`、`ZOOM_MIN`、`ZOOM_MAX`、`STATE_*` 及相關型別。
|
|
492
|
+
一個零依賴模組,承載全部常數:`FLAG_*`(十個全在,含 `FLAG_TITLEBAR_RESERVE` 與 `FLAG_NATIVE_CAPTIONS`)、`BROWSER_UI_*`、`RESIZE_EDGE`、`RESIZE_INDEX`、`resizeIndex`、`ZOOM_LADDER`、`ZOOM_MIN`、`ZOOM_MAX`、`STATE_MAXIMIZED` / `STATE_MINIMIZED` / `STATE_FOCUSED`,以及 caption 幾何三件套 `CAPTION_BAR_HEIGHT`(32)/ `CAPTION_BUTTON_WIDTH`(46)/ `CAPTION_BUTTONS`——最後這個的順序是「從右往左 close、maximize、minimize」,而這個順序是有承重的:頁面畫的按鈕順序若與 `caption_button_at()` 的回報順序不一致,點「關閉」會變成最大化。
|
|
450
493
|
|
|
451
494
|
**頁面側請用這個子路徑。** 主入口經 `./native` 引入 `bun:ffi`,瀏覽器打包器解析不了它——
|
|
452
495
|
所以在頁面程式碼裡寫 `import { BROWSER_UI_ALL } from "bun-app-window"` 會在打包期失敗。
|
|
@@ -478,6 +521,56 @@ await writePageBundle({ entry: "src/ui/page.ts", out: "src/ui/page.bundle.txt" }
|
|
|
478
521
|
`bytes` 是 UTF-8 位元組數,不是 `string.length`;原始碼裡只要有非 ASCII 文字兩者就不同,
|
|
479
522
|
而一個「看起來對」的體積讀數比沒有讀數更糟。
|
|
480
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
|
+
|
|
481
574
|
失敗時拋錯並帶上 Bun 的完整診斷。兩種形態都覆蓋了:編譯期問題以
|
|
482
575
|
`{ success: false, logs }` 回傳,而入口本身不可達時**拋 `AggregateError`**,
|
|
483
576
|
它的 `message` 只有一句 `Bundle failed`,細節在 `.errors` 上。
|
|
@@ -508,13 +601,13 @@ const shell = await openShell({ url, browserUi: BROWSER_UI_ALL });
|
|
|
508
601
|
|
|
509
602
|
亦可依需求組合以下旗標:
|
|
510
603
|
|
|
511
|
-
| 旗標
|
|
512
|
-
|
|
|
513
|
-
| `BROWSER_UI_MENU`
|
|
514
|
-
| `BROWSER_UI_KEYS`
|
|
515
|
-
| `BROWSER_UI_STATUSBAR` | 左下角連結懸停狀態列
|
|
516
|
-
| `BROWSER_UI_SWIPE`
|
|
517
|
-
| `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` | 表單自動填入提示 |
|
|
518
611
|
|
|
519
612
|
註:`F11`、`F12` 及 `Ctrl±` 縮放由視窗外殼層直接接管,不受上述旗標影響。
|
|
520
613
|
|
|
@@ -525,26 +618,26 @@ const shell = await openShell({ url, browserUi: BROWSER_UI_ALL });
|
|
|
525
618
|
```ts
|
|
526
619
|
// 宿主端:將選單能力掛載到扁平服務上
|
|
527
620
|
createIpcServer({
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
621
|
+
service: {
|
|
622
|
+
showContextMenu: (x: number, y: number, items: string) => shell.showContextMenu(x, y, items),
|
|
623
|
+
},
|
|
531
624
|
});
|
|
532
625
|
```
|
|
533
626
|
|
|
534
627
|
```ts
|
|
535
628
|
// 頁面端觸發選單
|
|
536
629
|
document.addEventListener("contextmenu", async (e) => {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
630
|
+
e.preventDefault();
|
|
631
|
+
await api.showContextMenu(
|
|
632
|
+
Math.round(e.screenX),
|
|
633
|
+
Math.round(e.screenY),
|
|
634
|
+
"open|開啟\nrefresh|重新整理\n-\nquit|結束",
|
|
635
|
+
);
|
|
543
636
|
});
|
|
544
637
|
|
|
545
638
|
// 頁面端監聽選單點擊結果
|
|
546
639
|
window.addEventListener("shell-msg", (e) => {
|
|
547
|
-
|
|
640
|
+
if (e.detail.t === "menu") console.log("選取選單項目 ID:", e.detail.id);
|
|
548
641
|
});
|
|
549
642
|
```
|
|
550
643
|
|
|
@@ -562,7 +655,7 @@ const shell = await openShell({ url, flags: FLAG_ACCEPT_DROP });
|
|
|
562
655
|
```js
|
|
563
656
|
// 頁面端接收路徑清單
|
|
564
657
|
window.addEventListener("shell-msg", (e) => {
|
|
565
|
-
|
|
658
|
+
if (e.detail.t === "drop") console.log("拖入檔案路徑:", e.detail.paths);
|
|
566
659
|
});
|
|
567
660
|
```
|
|
568
661
|
|
|
@@ -570,10 +663,10 @@ window.addEventListener("shell-msg", (e) => {
|
|
|
570
663
|
|
|
571
664
|
```js
|
|
572
665
|
window.addEventListener("shell-msg", (e) => {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
+
}
|
|
577
670
|
});
|
|
578
671
|
```
|
|
579
672
|
|
|
@@ -584,14 +677,27 @@ window.addEventListener("shell-msg", (e) => {
|
|
|
584
677
|
視窗外殼層不會向頁面隱式注入任何 DOM 或全域樣式。為消除「網頁感」,建議應用程式於前端自行補充以下基礎規則:
|
|
585
678
|
|
|
586
679
|
```css
|
|
587
|
-
html,
|
|
588
|
-
body {
|
|
589
|
-
|
|
590
|
-
|
|
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
|
+
}
|
|
591
697
|
```
|
|
592
698
|
|
|
593
699
|
```html
|
|
594
|
-
<input type="text" spellcheck="false" autocomplete="off" autocapitalize="off"
|
|
700
|
+
<input type="text" spellcheck="false" autocomplete="off" autocapitalize="off" />
|
|
595
701
|
```
|
|
596
702
|
|
|
597
703
|
尤其建議為輸入框設定 `spellcheck="false"`,否則瀏覽器拼字檢查極易在檔案路徑或專業數值下方繪製紅色波浪底線,破壞桌面應用程式的原生觀感。
|
|
@@ -602,7 +708,7 @@ button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
|
602
708
|
|
|
603
709
|
```ts
|
|
604
710
|
import { createShell } from "bun-app-window";
|
|
605
|
-
import { embeddedDll } from "bun-app-window/embedded";
|
|
711
|
+
import { embeddedDll } from "bun-app-window/embedded"; // 將 DLL 位元組內嵌至封裝產物
|
|
606
712
|
|
|
607
713
|
const shell = await createShell({ url, embedded: embeddedDll });
|
|
608
714
|
```
|
|
@@ -641,18 +747,18 @@ bun build --compile --windows-hide-console src/index.ts --outfile dist/app.exe
|
|
|
641
747
|
|
|
642
748
|
### 內建快速鍵與滑鼠行為
|
|
643
749
|
|
|
644
|
-
| 觸發方式
|
|
645
|
-
|
|
|
646
|
-
| `Ctrl +` / `Ctrl -` / `Ctrl 0` | 頁面縮放:放大一階 / 縮小一階 / 重設為 100%
|
|
647
|
-
| `Ctrl + 滑鼠滾輪`
|
|
648
|
-
| `F11`
|
|
649
|
-
| `F5` / `Ctrl + R`
|
|
650
|
-
| `F12`
|
|
651
|
-
| `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 系統視窗選單 |
|
|
652
758
|
|
|
653
759
|
## 開發與驗證
|
|
654
760
|
|
|
655
|
-
本專案工具鏈基於 Bun 1.4.
|
|
761
|
+
本專案工具鏈基於 Bun 1.4.4 與 TypeScript 7.0.2 建構,`tsconfig.json` 開啟全量嚴格模式檢查。
|
|
656
762
|
|
|
657
763
|
**本專案不使用遠端 CI。** 該帳戶的 GitHub Actions 因 billing lock 無法分配 runner;而專案本身是單平台、單人開發、零外部服務相依的相依套件,遠端 CI 的邊際價值低於其維護成本,因此品質把關改為本機執行。建議入口:
|
|
658
764
|
|
|
@@ -661,9 +767,9 @@ bun run scripts/gate.ts # 對目前工作區執行三段把關
|
|
|
661
767
|
bun run scripts/gate.ts --clean # 同上,但在 HEAD 的乾淨副本內執行
|
|
662
768
|
```
|
|
663
769
|
|
|
664
|
-
`-Clean` 重現了託管 runner 的條件:將 `HEAD` clone 到 `%TEMP%`,依 lockfile 安裝相依套件,再於其中執行把關。如此一來,本機產物(`.build/`、`node_modules/`、`vendor/webview2
|
|
770
|
+
`-Clean` 重現了託管 runner 的條件:將 `HEAD` clone 到 `%TEMP%`,依 lockfile 安裝相依套件,再於其中執行把關。如此一來,本機產物(`.build/`、`node_modules/`、`vendor/webview2/`)便無法掩蓋缺少的檔案。未提交的變更會被提示並排除於乾淨副本之外——如需驗證工作區現況,請移除 `-Clean`。
|
|
665
771
|
|
|
666
|
-
品質把關分三段:`bun check`、`bun test`,以及消費方驗收(`scripts/verify-pack.ts`)。第三段會把 tarball 打包到 `%TEMP%`,安裝進一個臨時專案,對該專案執行型別檢查,再執行 `scripts/consumer-probe.ts`——斷言涵蓋全部子路徑、DLL 是否真的從 `node_modules` 被 `dlopen`、以及 `libInfo.source` 是否為 `package
|
|
772
|
+
品質把關分三段:`bun check`、`bun test`,以及消費方驗收(`scripts/verify-pack.ts`)。第三段會把 tarball 打包到 `%TEMP%`,安裝進一個臨時專案,對該專案執行型別檢查,再執行 `scripts/consumer-probe.ts`——斷言涵蓋全部子路徑、DLL 是否真的從 `node_modules` 被 `dlopen`、以及 `libInfo.source` 是否為 `package`(條數由探針自報)。可離線執行,且預設拒絕:任何未在允許清單登記的檔案被打進套件,把關就會失敗。只有在改動把關腳本本身時才用 `-SkipPack`。
|
|
667
773
|
|
|
668
774
|
底層指令仍可直接呼叫:
|
|
669
775
|
|
|
@@ -673,16 +779,16 @@ bun test # 執行自動化測試套件(位於 tests/)
|
|
|
673
779
|
bun run demo # 啟動互動式自我檢測範例程式(需於桌面工作階段下執行)
|
|
674
780
|
```
|
|
675
781
|
|
|
676
|
-
需注意:`bun run demo`
|
|
782
|
+
需注意:`bun run demo` 與單一 `.exe` 的驗收都需要互動式桌面,因此兩者都不具備 CI 可行性,屬於構造上的本機專屬項目。`.exe` 那條的完整判據是:編譯 → 把產物複製進一個空目錄 → 啟動 → 確認視窗起得來,且 `libInfo.source` 回報 `embedded`。
|
|
677
783
|
|
|
678
|
-
|
|
784
|
+
每當修改 `src/shell/` 或 `package.json` 後,「作為外部專案相依套件安裝」這條鏈路已由消費方驗收階段自動涵蓋——它隨品質把關一起執行,不需要額外記得。只剩 `.exe` 建置仍需於桌面工作階段手動執行一次。
|
|
679
785
|
|
|
680
786
|
自動化測試或截圖腳本若需透過 Win32 `EnumWindows` 過濾視窗控制代碼(HWND),請依據視窗模式比對對應的視窗類別名稱(Window Class):
|
|
681
787
|
|
|
682
|
-
| 視窗模式
|
|
683
|
-
|
|
|
684
|
-
| 標準有邊框模式(未傳 `FLAG_FRAMELESS`) | `webview`
|
|
685
|
-
| 無邊框模式(傳入 `FLAG_FRAMELESS`)
|
|
788
|
+
| 視窗模式 | 對應視窗類別名稱 |
|
|
789
|
+
| --------------------------------------- | ---------------- |
|
|
790
|
+
| 標準有邊框模式(未傳 `FLAG_FRAMELESS`) | `webview` |
|
|
791
|
+
| 無邊框模式(傳入 `FLAG_FRAMELESS`) | `webview_shell` |
|
|
686
792
|
|
|
687
793
|
## 致謝
|
|
688
794
|
|