bun-app-window 0.0.0-stage → 0.2.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 +122 -0
- package/LICENSE +21 -0
- package/README.md +432 -2
- package/README.zh-CN.md +431 -0
- package/README.zh-TW.md +431 -0
- package/dist/libshell.dll +0 -0
- package/package.json +65 -5
- package/src/shell/assets.d.ts +16 -0
- package/src/shell/debug.ts +40 -0
- package/src/shell/embedded.ts +42 -0
- package/src/shell/index.ts +291 -0
- package/src/shell/ipc.ts +569 -0
- package/src/shell/native-asset.d.ts +18 -0
- package/src/shell/native.ts +92 -0
- package/src/shell/port.ts +77 -0
- package/src/shell/resolve.ts +211 -0
- package/src/shell/state.ts +211 -0
- package/src/shell/types.ts +144 -0
- package/src/ui/titlebar.ts +436 -0
package/README.zh-TW.md
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
# bun-app-window
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/bun-app-window)
|
|
4
|
+
[](https://bun.sh)
|
|
5
|
+
[](#系統需求與設計邊界)
|
|
6
|
+
[](#系統需求與設計邊界)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
正體中文 | [English](./README.md) | [简体中文](./README.zh-CN.md)
|
|
10
|
+
|
|
11
|
+
專為 Bun 應用程式設計的 Windows 原生視窗外殼,基於系統內建的 WebView2 執行環境(Runtime)承載前端介面。
|
|
12
|
+
|
|
13
|
+
本專案遵循極簡職責邊界:僅負責視窗生命週期、幾何尺寸、DPI 縮放適配、無邊框視窗自訂及底層訊息通道;商業邏輯與 UI 渲染完全解耦於外殼層之外。
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { openShell, serveOnSafePort, FLAG_FRAMELESS } from "bun-app-window";
|
|
17
|
+
|
|
18
|
+
const server = serveOnSafePort({ fetch: () => new Response("<h1>Hello</h1>") });
|
|
19
|
+
await openShell({ url: `http://127.0.0.1:${server.port}`, flags: FLAG_FRAMELESS });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 系統需求與設計邊界
|
|
25
|
+
|
|
26
|
+
在導入本專案前,請確認以下技術前提與架構取捨是否符合你的應用場景:
|
|
27
|
+
|
|
28
|
+
| 維度 | 限制與說明 |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| 僅支援 Windows x64 | `package.json` 中的 `os` 與 `cpu` 欄位已做強制限制,拒絕在其他平台安裝(套件名稱保持平台中立僅為預留命名空間)。預編譯動態連結程式庫 `libshell.dll` 隨套件發佈,無需在安裝期編譯 |
|
|
31
|
+
| 僅支援 Bun 執行環境 | 套件內直接發佈 TypeScript 原始碼,要求 Bun ≥ 1.4.3。Node.js 環境雖可完成套件安裝,但無法直接執行 |
|
|
32
|
+
| 型別檢查需自備 `@types/bun` | 套件內是 `.ts` 原始碼,消費方的 `bun check` 會一併編譯它們。需執行 `bun add -d @types/bun`,否則型別檢查會在 `node_modules` 內回報 `TS2868: Cannot find name 'Bun'` |
|
|
33
|
+
| 基於 `bun:ffi` 建構 | `bun:ffi` 在 Bun 中仍標記為實驗性功能(Experimental)。官方建議正式環境優先採用 Node-API,本專案選擇 `bun:ffi` 是為了換取零第三方相依性的輕量架構 |
|
|
34
|
+
|
|
35
|
+
執行環境與能力邊界:
|
|
36
|
+
|
|
37
|
+
- 依賴系統 WebView2 Runtime:目標機器需已安裝 WebView2 執行環境(Windows 11 預設內建)。
|
|
38
|
+
- 要求互動式桌面工作階段:受 Windows 工作階段隔離(Session 0 Isolation)機制限制,WebView2 無法在 Session 0(如系統背景服務)中啟動;但實測在 High 完整性層級的管理員提權行程(Process)中可正常執行。
|
|
39
|
+
- 明確不包含的功能:系統匣常駐圖示(System Tray)、原生頂部功能表列、自動更新、多視窗管理、跨平台支援。
|
|
40
|
+
|
|
41
|
+
## 安裝
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bun add bun-app-window
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 套件內容包含什麼
|
|
48
|
+
|
|
49
|
+
發佈的 tarball 只有預先編譯的 `dist/libshell.dll` 與 TypeScript 原始碼,共 19 個檔案(約 0.36 MB)。原生 C++ 層(`native/`)與上游 vendored 標頭檔(`vendor/`)**不隨套件散布**:它們只在重新編譯 `libshell.dll` 時需要,而重新編譯必須 clone 儲存庫。因此 `bun run build:native` 與 `build:exe` 是本儲存庫專用指令碼,安裝進 `node_modules` 後執行沒有意義。
|
|
50
|
+
|
|
51
|
+
## 快速開始
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { openShell, serveOnSafePort, FLAG_FRAMELESS, FLAG_CENTER } from "bun-app-window";
|
|
55
|
+
|
|
56
|
+
const server = serveOnSafePort({
|
|
57
|
+
fetch: () => new Response("<h1>Hello</h1>", {
|
|
58
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const shell = await openShell({
|
|
63
|
+
url: `http://127.0.0.1:${server.port}`,
|
|
64
|
+
title: "My App",
|
|
65
|
+
width: 1000,
|
|
66
|
+
height: 680,
|
|
67
|
+
flags: FLAG_FRAMELESS | FLAG_CENTER,
|
|
68
|
+
minWidth: 480,
|
|
69
|
+
minHeight: 320,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
shell.post(JSON.stringify({ t: "ready" })); // 宿主 → 頁面單向訊息
|
|
73
|
+
shell.setTitle("Doing work…");
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
說明:`createShell(opts)` 僅解析並載入原生動態連結程式庫,不建立視窗;`openShell(opts)` 則一步完成實例建立與視窗啟動,若啟動失敗將直接拋出例外錯誤。
|
|
77
|
+
|
|
78
|
+
## 核心 API
|
|
79
|
+
|
|
80
|
+
### 實例方法與屬性
|
|
81
|
+
|
|
82
|
+
| 成員 | 說明 |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `start()` | 建立並啟動視窗,回傳 `{ rc, error }`。呼叫後立即回傳,UI 訊息迴圈執行於獨立原生執行緒 |
|
|
85
|
+
| `stop()` | 銷毀視窗並釋放底層資源,支援冪等重複呼叫 |
|
|
86
|
+
| `show()` | 顯示視窗。常搭配 `FLAG_DEFER_SHOW` 使用,實現在頁面渲染就緒後再展現視窗 |
|
|
87
|
+
| `isAlive()` | 檢測視窗是否存活(註:`state()` 無法區分「尚未啟動」與「已銷毀」狀態) |
|
|
88
|
+
| `state()` | 取得目前視窗狀態:`{ maximized, minimized, focused, dark, raw }` |
|
|
89
|
+
| `lastError()` | 取得最近一次操作失敗的錯誤訊息,無錯誤時回傳空字串 |
|
|
90
|
+
| `setTitle` / `minimize` / `toggleMaximize` / `close` | 基礎視窗控制操作,回傳狀態碼 `rc` |
|
|
91
|
+
| `setMinSize(w, h)` | 設定視窗最小尺寸(單位:邏輯像素) |
|
|
92
|
+
| `drag()` | 觸發原生視窗拖曳(於頁面 `pointerdown` 事件中呼叫,完整保留 Windows 貼齊版面配置特性) |
|
|
93
|
+
| `beginResize(edge)` | 觸發原生邊框縮放(於頁面 `pointermove` 事件中呼叫,`edge` 取值見 `RESIZE_EDGE.*`) |
|
|
94
|
+
| `post(json)` | 宿主向頁面單向投遞 JSON 訊息,非同步發送且不等待回傳結果 |
|
|
95
|
+
| `setZoom(f)` / `getZoom()` | 設定或取得頁面縮放比例(有效範圍 `0.25`–`3.0`,越界值自動鉗制) |
|
|
96
|
+
| `reload()` / `openDevTools()` / `toggleFullscreen()` | 頁面重新載入、開啟開發人員工具、切換全螢幕(分別對應 `Ctrl+R` / `F12` / `F11`) |
|
|
97
|
+
| `setBrowserUi(mask)` | 停用指定的瀏覽器預設行為,傳入 `BROWSER_UI_*` 旗標的按位元 OR 組合 |
|
|
98
|
+
| `showContextMenu(x, y, items)` | 於指定螢幕座標彈出 Win32 原生右鍵快顯功能表(詳見下文用法) |
|
|
99
|
+
| `pickFolder(title?)` | 呼叫原生資料夾選擇對話方塊(同步阻塞目前呼叫執行緒) |
|
|
100
|
+
| `libInfo` | 原生動態連結程式庫解析中繼資料(包含實際路徑、載入來源、SHA-256 雜湊值) |
|
|
101
|
+
| `symbols` | 底層直通介面:直接存取透過 `dlopen` 匯出的底層 C 符號 |
|
|
102
|
+
|
|
103
|
+
### 視窗旗標(Flags)
|
|
104
|
+
|
|
105
|
+
透過按位元 OR(`|`)組合傳入 `opts.flags`:
|
|
106
|
+
|
|
107
|
+
| 旗標 | 作用說明 |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `FLAG_FRAMELESS` | 無邊框視窗模式,移除系統預設標題列,交由前端頁面自行繪製 |
|
|
110
|
+
| `FLAG_RESIZABLE` | 允許使用者調整視窗大小 |
|
|
111
|
+
| `FLAG_CENTER` | 啟動時於目前顯示器工作區置中顯示(有邊框與無邊框模式均生效) |
|
|
112
|
+
| `FLAG_TOPMOST` | 視窗保持最上層顯示 |
|
|
113
|
+
| `FLAG_DEFER_SHOW` | 建立視窗後保持隱藏狀態,等待明確呼叫 `show()` |
|
|
114
|
+
| `FLAG_ACCEPT_DROP` | 允許視窗接收外部檔案或資料夾拖放事件 |
|
|
115
|
+
| `FLAG_START_MAXIMIZED` | 視窗首次顯示即處於最大化狀態,避免從一般尺寸跳變至最大化的中間閃爍畫格 |
|
|
116
|
+
|
|
117
|
+
## 子路徑模組
|
|
118
|
+
|
|
119
|
+
核心視窗控制之外的擴充能力均拆分於獨立子路徑下,可依需求匯入。
|
|
120
|
+
|
|
121
|
+
### `bun-app-window/ipc` — 型別化雙向通訊與事件匯流排
|
|
122
|
+
|
|
123
|
+
宿主與前端頁面共用同一份 TypeScript 型別宣告,用戶端透過泛型在編譯期完成方法簽章與事件負載驗證:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
// api.ts —— 宿主與頁面共同匯入的型別契約
|
|
127
|
+
export type Api = { greet(name: string): string };
|
|
128
|
+
export type Events = { windowState: { maximized: boolean } };
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// 宿主端
|
|
133
|
+
import { createIpcServer } from "bun-app-window/ipc";
|
|
134
|
+
|
|
135
|
+
const ipc = createIpcServer({
|
|
136
|
+
service: { greet: (name: string) => `Hello ${name}` },
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
serveOnSafePort({
|
|
140
|
+
fetch(req) { /* 處理靜態頁面與驗證 */ },
|
|
141
|
+
websocket: {
|
|
142
|
+
open: (ws) => ipc.attach(ws),
|
|
143
|
+
message: (ws, raw) => ipc.handle(ws, raw),
|
|
144
|
+
close: (ws) => ipc.detach(ws),
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
ipc.emit("windowState", { maximized: false }); // 向頁面主動推播事件
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// 頁面端
|
|
153
|
+
import { connect } from "bun-app-window/ipc";
|
|
154
|
+
import type { Api, Events } from "./api";
|
|
155
|
+
|
|
156
|
+
const api = await connect<Api, Events>();
|
|
157
|
+
await api.greet("Ada"); // 回傳型別自動推導為 string
|
|
158
|
+
api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
預設情況下,`connect` 會從目前頁面 URL 的查詢參數 `?t=` 中讀取驗證 Token 並連線至 `/ws`。若需自訂連線方式,可明確傳入 `url` 或自訂 `channel` 實例。
|
|
162
|
+
|
|
163
|
+
### `bun-app-window/titlebar` — Windows 11 風格標題列
|
|
164
|
+
|
|
165
|
+
用於在無邊框模式下快速掛載符合 Windows 11 視覺規範的自訂標題列:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
// 頁面端
|
|
169
|
+
import { mountTitleBar, bridgeFromWindowService } from "bun-app-window/titlebar";
|
|
170
|
+
|
|
171
|
+
const bar = mountTitleBar({
|
|
172
|
+
title: "我的應用程式",
|
|
173
|
+
icon: "/icon.svg",
|
|
174
|
+
bridge: bridgeFromWindowService(api.window),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
宿主端需在 IPC 服務中公開對應的四個視窗控制方法:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
// 宿主端
|
|
184
|
+
createIpcServer({
|
|
185
|
+
service: {
|
|
186
|
+
window: {
|
|
187
|
+
minimize: () => shell.minimize(),
|
|
188
|
+
toggleMaximize: () => shell.toggleMaximize(),
|
|
189
|
+
close: () => shell.close(),
|
|
190
|
+
drag: () => shell.drag(),
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
實作細節:
|
|
197
|
+
|
|
198
|
+
- 原生拖曳體驗:拖曳操作透過呼叫 `shell.drag()` 交由底層發送 `WM_NCLBUTTONDOWN`(`HTCAPTION`)訊息處理,完整保留 Windows 11 貼齊版面配置(Snap Layouts)與連按兩下最大化等系統級行為。
|
|
199
|
+
- 樣式自訂與圖示方案:色彩與尺寸均透過 CSS 變數(`--ws-tb-*`)控制,覆寫變數即可實作佈景主題切換。按鈕圖示預設採用相容性較佳的幾何向量符號;若設定 `icons: "fluent"` 則切換為 `Segoe Fluent Icons` 字型渲染(注意:若目標系統缺少該字型,按鈕將退回顯示為缺字方框)。
|
|
200
|
+
|
|
201
|
+
### `bun-app-window/state` — 視窗位置與尺寸持久化
|
|
202
|
+
|
|
203
|
+
用於自動記錄並在下次啟動時還原視窗的座標、尺寸及最大化狀態:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { attachPlacementStore, loadPlacement, restorePlacement } from "bun-app-window/state";
|
|
207
|
+
|
|
208
|
+
const FILE = "C:\\Users\\me\\AppData\\Roaming\\MyApp\\window.json";
|
|
209
|
+
const shell = await openShell({ url, flags: FLAG_DEFER_SHOW });
|
|
210
|
+
|
|
211
|
+
const saved = await loadPlacement(FILE);
|
|
212
|
+
if (saved) restorePlacement(shell, saved);
|
|
213
|
+
|
|
214
|
+
shell.show();
|
|
215
|
+
|
|
216
|
+
const store = attachPlacementStore(shell, FILE); // 定時輪詢狀態,視窗靜止時不觸發寫入磁碟
|
|
217
|
+
process.on("beforeExit", () => store.flush());
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
底層原生實作已妥善處理以下邊界場景:
|
|
221
|
+
|
|
222
|
+
- 最大化狀態下儲存的是視窗「還原後的正常矩形(Restore Rect)」而非全螢幕尺寸。
|
|
223
|
+
- 跨顯示器不同縮放比例下的 DPI 座標換算。
|
|
224
|
+
- 螢幕拓撲變化(如拔除外接螢幕)後的螢幕外座標越界驗證與修正。
|
|
225
|
+
|
|
226
|
+
若無需背景輪詢,亦可直接呼叫 `capturePlacement` 與 `savePlacement` 執行單次手動讀寫。
|
|
227
|
+
|
|
228
|
+
### `bun-app-window/debug` — 診斷與除錯介面
|
|
229
|
+
|
|
230
|
+
提供底層視窗度量與命中測試(Hit-Test)資訊。注意:診斷介面不屬於穩定 API 契約,後續版本可能隨時調整。
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { debugOf } from "bun-app-window/debug";
|
|
234
|
+
|
|
235
|
+
const d = debugOf(shell);
|
|
236
|
+
d.metrics(); // 取得 pos / win / client / work / border / round 等幾何度量
|
|
237
|
+
d.dpi(); // 取得目前 DPI 感知層級與縮放係數
|
|
238
|
+
d.hittest(x, y);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### `bun-app-window/native` — 預編譯動態連結程式庫本體
|
|
242
|
+
|
|
243
|
+
該子路徑解析到 `dist/libshell.dll` 本身,`import.meta.resolve()` 拿到的是可直接複製到 exe 旁邊的真實路徑:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import nativeLibPath from "bun-app-window/native" with { type: "file" };
|
|
247
|
+
|
|
248
|
+
await Bun.write(join(dirname(process.execPath), "libshell.dll"), Bun.file(nativeLibPath));
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`with { type: "file" }` 不能省略,否則封裝器會把 DLL 當模組解析。此入口用於自行封裝的場景;走 `bun build --compile` 請直接用 `bun-app-window/embedded`,它不必碰檔案系統。
|
|
252
|
+
|
|
253
|
+
## 常見場景與最佳實踐
|
|
254
|
+
|
|
255
|
+
### 1. 停用瀏覽器預設行為
|
|
256
|
+
|
|
257
|
+
WebView2 預設保留了完整的瀏覽器互動語意。建構桌面應用程式時,通常建議將其全部停用:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import { BROWSER_UI_ALL } from "bun-app-window";
|
|
261
|
+
|
|
262
|
+
const shell = await openShell({ url, browserUi: BROWSER_UI_ALL });
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
亦可依需求組合以下旗標:
|
|
266
|
+
|
|
267
|
+
| 旗標 | 停用的瀏覽器行為 |
|
|
268
|
+
| --- | --- |
|
|
269
|
+
| `BROWSER_UI_MENU` | 預設網頁右鍵選單(Edge 快顯功能表) |
|
|
270
|
+
| `BROWSER_UI_KEYS` | 瀏覽器內建快速鍵(如 `Ctrl+P` / `Ctrl+F` / `Ctrl+S`) |
|
|
271
|
+
| `BROWSER_UI_STATUSBAR` | 左下角連結懸停狀態列 |
|
|
272
|
+
| `BROWSER_UI_SWIPE` | 觸控板雙指橫滑上一頁 / 下一頁手勢 |
|
|
273
|
+
| `BROWSER_UI_AUTOFILL` | 表單自動填入提示 |
|
|
274
|
+
|
|
275
|
+
註:`F11`、`F12` 及 `Ctrl±` 縮放由視窗外殼層直接接管,不受上述旗標影響。
|
|
276
|
+
|
|
277
|
+
### 2. 原生右鍵快顯功能表
|
|
278
|
+
|
|
279
|
+
停用預設瀏覽器選單後,可透過 `showContextMenu` 喚起基於 Win32 `TrackPopupMenu` 繪製的原生選單。頁面無法直接呼叫它——需先在 IPC 服務上暴露:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
// 宿主端:將選單能力掛載到 window 服務
|
|
283
|
+
createIpcServer({
|
|
284
|
+
service: {
|
|
285
|
+
window: {
|
|
286
|
+
showContextMenu: (x: number, y: number, items: string) =>
|
|
287
|
+
shell.showContextMenu(x, y, items),
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
// 頁面端觸發選單
|
|
295
|
+
document.addEventListener("contextmenu", async (e) => {
|
|
296
|
+
e.preventDefault();
|
|
297
|
+
await api.window.showContextMenu(
|
|
298
|
+
Math.round(e.screenX),
|
|
299
|
+
Math.round(e.screenY),
|
|
300
|
+
"open|開啟\nrefresh|重新整理\n-\nquit|結束"
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// 頁面端監聽選單點擊結果
|
|
305
|
+
window.addEventListener("shell-msg", (e) => {
|
|
306
|
+
if (e.detail.t === "menu") console.log("選取選單項目 ID:", e.detail.id);
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
選單項目定義採用換行符號分隔的字串格式:每行格式為 `id|顯示文字`,單獨一行 `-` 表示分隔線。
|
|
311
|
+
|
|
312
|
+
### 3. 接收外部檔案拖放
|
|
313
|
+
|
|
314
|
+
啟用 `FLAG_ACCEPT_DROP` 後,視窗即可接收來自檔案總管(Windows Explorer)的檔案或資料夾拖放:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
// 宿主端
|
|
318
|
+
const shell = await openShell({ url, flags: FLAG_ACCEPT_DROP });
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
```js
|
|
322
|
+
// 頁面端接收路徑清單
|
|
323
|
+
window.addEventListener("shell-msg", (e) => {
|
|
324
|
+
if (e.detail.t === "drop") console.log("拖入檔案路徑:", e.detail.paths);
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
針對管理員提權場景(High 完整性層級行程接收來自 Medium 層級檔案總管的訊息),底層已透過 `ChangeWindowMessageFilterEx` 自動放行 UIPI 訊息過濾。
|
|
329
|
+
|
|
330
|
+
### 4. 前端頁面桌面化樣式建議
|
|
331
|
+
|
|
332
|
+
視窗外殼層不會向頁面隱式注入任何 DOM 或全域樣式。為消除「網頁感」,建議應用程式於前端自行補充以下基礎規則:
|
|
333
|
+
|
|
334
|
+
```css
|
|
335
|
+
html, body { cursor: default; }
|
|
336
|
+
body { user-select: none; }
|
|
337
|
+
input, textarea, .log { user-select: text; cursor: auto; }
|
|
338
|
+
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
```html
|
|
342
|
+
<input type="text" spellcheck="false" autocomplete="off" autocapitalize="off">
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
尤其建議為輸入框設定 `spellcheck="false"`,否則瀏覽器拼字檢查極易在檔案路徑或專業數值下方繪製紅色波浪底線,破壞桌面應用程式的原生觀感。
|
|
346
|
+
|
|
347
|
+
### 5. 編譯為單一可執行檔(.exe)
|
|
348
|
+
|
|
349
|
+
使用 `bun build --compile` 封裝時,可透過匯入 `bun-app-window/embedded` 將原生 DLL 一併內嵌至產物中:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { createShell } from "bun-app-window";
|
|
353
|
+
import { embeddedDll } from "bun-app-window/embedded"; // 將 DLL 位元組內嵌至封裝產物
|
|
354
|
+
|
|
355
|
+
const shell = await createShell({ url, embedded: embeddedDll });
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
bun build --compile --windows-hide-console src/index.ts --outfile dist/app.exe
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
提示:
|
|
363
|
+
|
|
364
|
+
- 僅透過副作用匯入 `import "bun-app-window/embedded";` 同樣生效。
|
|
365
|
+
- 封裝桌面端應用程式時請務必加上 `--windows-hide-console` 參數,否則啟動時將伴隨彈出黑色主控台(Console)視窗。
|
|
366
|
+
|
|
367
|
+
## 底層機制與互動參考
|
|
368
|
+
|
|
369
|
+
### 原生動態連結程式庫載入順序
|
|
370
|
+
|
|
371
|
+
執行階段依以下優先順序尋找並載入 `libshell.dll`:
|
|
372
|
+
|
|
373
|
+
1. 明確路徑:若傳入 `opts.nativeLibPath`,則直接載入該路徑並略過後續推斷。
|
|
374
|
+
2. 內嵌位元組釋放:若包含編譯期內嵌位元組,將其釋放至 `%LOCALAPPDATA%\<appName>\runtime` 目錄下(採用內容雜湊值命名,自動清理歷史舊版本)。
|
|
375
|
+
3. 可執行檔同級目錄:尋找目前 `.exe` 所在目錄下的 `libshell.dll`。
|
|
376
|
+
4. 開發態路徑:依序尋找套件根目錄下的 `.build/libshell.dll` 與 `dist/libshell.dll`。
|
|
377
|
+
|
|
378
|
+
### 內建快速鍵與滑鼠行為
|
|
379
|
+
|
|
380
|
+
| 觸發方式 | 預設行為 |
|
|
381
|
+
| --- | --- |
|
|
382
|
+
| `Ctrl +` / `Ctrl -` / `Ctrl 0` | 頁面縮放:放大一階 / 縮小一階 / 重設為 100% |
|
|
383
|
+
| `Ctrl + 滑鼠滾輪` | 頁面縮放:放大 / 縮小 |
|
|
384
|
+
| `F11` | 切換全螢幕模式(隱藏視窗邊框並覆蓋系統工作列) |
|
|
385
|
+
| `F5` / `Ctrl + R` | 重新載入目前頁面 |
|
|
386
|
+
| `F12` | 開啟 WebView2 開發人員工具 |
|
|
387
|
+
| `Alt + Space` / 標題列右鍵 | 彈出 Windows 系統視窗選單 |
|
|
388
|
+
|
|
389
|
+
## 開發與驗證
|
|
390
|
+
|
|
391
|
+
本專案工具鏈基於 Bun 1.4.3 與 TypeScript 7.0.2 建構,`tsconfig.json` 開啟全量嚴格模式檢查。
|
|
392
|
+
|
|
393
|
+
**本專案不使用遠端 CI。** 該帳戶的 GitHub Actions 因 billing lock 無法分配 runner;而專案本身是單平台、單人開發、零外部服務相依的相依套件,遠端 CI 的邊際價值低於其維護成本,因此品質把關改為本機執行。建議入口:
|
|
394
|
+
|
|
395
|
+
```powershell
|
|
396
|
+
bun run scripts/gate.ts # 對目前工作區執行三段把關
|
|
397
|
+
bun run scripts/gate.ts --clean # 同上,但在 HEAD 的乾淨副本內執行
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
`-Clean` 重現了託管 runner 的條件:將 `HEAD` clone 到 `%TEMP%`,依 lockfile 安裝相依套件,再於其中執行把關。如此一來,本機產物(`.build/`、`node_modules/`、`vendor/webview2/`)便無法掩蓋缺少的檔案。耗時約兩秒。未提交的變更會被提示並排除於乾淨副本之外——如需驗證工作區現況,請移除 `-Clean`。
|
|
401
|
+
|
|
402
|
+
品質把關分三段:`bun check`、`bun test`,以及消費方驗收(`scripts/verify-pack.ts`)。第三段會把 tarball 打包到 `%TEMP%`,安裝進一個臨時專案,對該專案執行型別檢查,再執行 `scripts/consumer-probe.ts`——40 條斷言涵蓋全部子路徑、DLL 是否真的從 `node_modules` 被 `dlopen`、以及 `libInfo.source` 是否為 `package`。可離線執行,約 0.4 秒,且預設拒絕:任何未在允許清單登記的檔案被打進套件,把關就會失敗。只有在改動把關腳本本身時才用 `-SkipPack`。
|
|
403
|
+
|
|
404
|
+
底層指令仍可直接呼叫:
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
bun check # 執行 TypeScript 型別檢查
|
|
408
|
+
bun test # 執行自動化測試套件(位於 tests/,共 182 個測試案例)
|
|
409
|
+
bun run demo # 啟動互動式自我檢測範例程式(需於桌面工作階段下執行)
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
需注意:`bun run demo` 與「編譯為單一可執行檔」的驗收(`AGENTS.md` 中的形態 B)皆需要互動式桌面,本就不具備 CI 可行性,屬於構造上的本機專屬項目。
|
|
413
|
+
|
|
414
|
+
注意:每當修改 `src/shell/` 或 `package.json` 後,「作為外部專案相依套件安裝」這條鏈路(`AGENTS.md` 中的形態 A)已由消費方驗收階段自動涵蓋——它隨品質把關一起執行,不需要額外記得。只剩形態 B(編譯單一 `.exe`)仍需於桌面工作階段手動執行一次。
|
|
415
|
+
|
|
416
|
+
自動化測試或截圖腳本若需透過 Win32 `EnumWindows` 過濾視窗控制代碼(HWND),請依據視窗模式比對對應的視窗類別名稱(Window Class):
|
|
417
|
+
|
|
418
|
+
| 視窗模式 | 對應視窗類別名稱 |
|
|
419
|
+
| --- | --- |
|
|
420
|
+
| 標準有邊框模式(未傳 `FLAG_FRAMELESS`) | `webview` |
|
|
421
|
+
| 無邊框模式(傳入 `FLAG_FRAMELESS`) | `webview_shell` |
|
|
422
|
+
|
|
423
|
+
## 致謝
|
|
424
|
+
|
|
425
|
+
- 底層跨平台視窗抽象源自 [webview/webview](https://github.com/webview/webview) `0.12.0`(MIT 授權,作者 Serge Zaitsev · Steffen André Langnes)。儲存庫內 `vendor/webview/` 目錄為其原始碼原樣拷貝,未作任何侵入式修改。
|
|
426
|
+
- WebView2 C/C++ SDK 源自 [Microsoft.Web.WebView2](https://www.nuget.org/packages/Microsoft.Web.WebView2) `1.0.1150.38`(NuGet 套件隨儲存庫發佈以支援離線建構),執行環境由作業系統提供。
|
|
427
|
+
- Co-authored-by DeepSeek
|
|
428
|
+
|
|
429
|
+
## 授權條款
|
|
430
|
+
|
|
431
|
+
本專案基於 [MIT License](./LICENSE) 開源發佈。`vendor/webview/` 目錄同樣遵循 MIT 授權條款,其原始著作權聲明完整保留於對應原始碼檔案中。
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
+
"name": "bun-app-window",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"description": "Native WebView2 window shell for Bun — frameless or system-decorated windows, DPI-aware geometry, typed IPC, system-accurate title bar",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "Dolores <tiancong.li@hotmail.com> (https://github.com/TiancongLx)",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/TiancongLx/bun-app-window.git"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/TiancongLx/bun-app-window#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/TiancongLx/bun-app-window/issues"
|
|
16
|
+
},
|
|
17
|
+
"os": ["win32"],
|
|
18
|
+
"cpu": ["x64"],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"bun",
|
|
21
|
+
"webview",
|
|
22
|
+
"webview2",
|
|
23
|
+
"frameless",
|
|
24
|
+
"window",
|
|
25
|
+
"desktop",
|
|
26
|
+
"ffi",
|
|
27
|
+
"titlebar",
|
|
28
|
+
"ipc"
|
|
29
|
+
],
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./src/shell/index.ts",
|
|
32
|
+
"./embedded": "./src/shell/embedded.ts",
|
|
33
|
+
"./debug": "./src/shell/debug.ts",
|
|
34
|
+
"./state": "./src/shell/state.ts",
|
|
35
|
+
"./ipc": "./src/shell/ipc.ts",
|
|
36
|
+
"./titlebar": "./src/ui/titlebar.ts",
|
|
37
|
+
"./native": {
|
|
38
|
+
"types": "./src/shell/native-asset.d.ts",
|
|
39
|
+
"default": "./dist/libshell.dll"
|
|
40
|
+
},
|
|
41
|
+
"./package.json": "./package.json"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"src/shell",
|
|
45
|
+
"src/ui/titlebar.ts",
|
|
46
|
+
"dist/libshell.dll",
|
|
47
|
+
"README.md",
|
|
48
|
+
"README.zh-CN.md",
|
|
49
|
+
"README.zh-TW.md",
|
|
50
|
+
"CHANGELOG.md",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
],
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build:native": "pwsh -NoProfile -ExecutionPolicy Bypass -File native/build.ps1",
|
|
55
|
+
"build:exe": "pwsh -NoProfile -ExecutionPolicy Bypass -File native/build-pack.ps1",
|
|
56
|
+
"demo": "bun run ./src/host.ts",
|
|
57
|
+
"pack": "bun pm pack"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/bun": "^1.4.3",
|
|
61
|
+
"typescript": "7.0.2"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"bun": ">=1.4.3"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* assets.d.ts — 打包器资源导入的模块声明
|
|
3
|
+
*
|
|
4
|
+
* `with { type: "file" }` 的导入在运行期是字符串路径,但 TypeScript 需要知道
|
|
5
|
+
* 这类模块存在,否则 `import dll from "...dll" with { type: "file" }` 会报 TS2307。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
declare module "*.dll" {
|
|
9
|
+
const path: string;
|
|
10
|
+
export default path;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
declare module "*.html" {
|
|
14
|
+
const content: string;
|
|
15
|
+
export default content;
|
|
16
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Shell } from "./index";
|
|
2
|
+
import { readBuffer } from "./native";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 排障面(调试入口)。
|
|
6
|
+
*
|
|
7
|
+
* 为什么单独一个入口:公共 API 只承诺产品向能力。把 `hittest` / `metrics` /
|
|
8
|
+
* `dpi` / `selfBase` / `selfPath` 留在主入口,等于把它们也变成契约——
|
|
9
|
+
* 将来改一次诊断输出的格式,就成了破坏性变更。放到子路径后,
|
|
10
|
+
* 主入口的 `Shell` 类型可以稳定演进,而排障手段仍随手可用。
|
|
11
|
+
*
|
|
12
|
+
* import { openShell } from "bun-app-window";
|
|
13
|
+
* import { debugOf } from "bun-app-window/debug";
|
|
14
|
+
*
|
|
15
|
+
* 这些接口随时可能变化,生产代码不应依赖。
|
|
16
|
+
*/
|
|
17
|
+
export type ShellDebug = {
|
|
18
|
+
/** 客户区坐标 → Win32 命中码(HT* 常量,1 = HTCLIENT) */
|
|
19
|
+
hittest(x: number, y: number): number;
|
|
20
|
+
/** 一次性几何快照:pos / win / client / work / border / round */
|
|
21
|
+
metrics(): string;
|
|
22
|
+
/** DPI 感知级别与实际监视器 DPI */
|
|
23
|
+
dpi(): string;
|
|
24
|
+
/** 原生库自身模块基址(判断是否内存映射) */
|
|
25
|
+
selfBase(): bigint;
|
|
26
|
+
/** 原生库自身文件路径 */
|
|
27
|
+
selfPath(): string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** 取得某个 shell 的排障接口。底层是同一个 dlopen 句柄,无需二次加载。 */
|
|
31
|
+
export function debugOf(shell: Shell): ShellDebug {
|
|
32
|
+
const s = shell.symbols;
|
|
33
|
+
return {
|
|
34
|
+
hittest: (x, y) => s.shell_debug_hittest(x, y),
|
|
35
|
+
metrics: () => readBuffer((p, len) => s.shell_debug_metrics(p, len)),
|
|
36
|
+
dpi: () => readBuffer((p, len) => s.shell_debug_dpi(p, len)),
|
|
37
|
+
selfBase: () => s.shell_self_base(),
|
|
38
|
+
selfPath: () => readBuffer((p, len) => s.shell_self_path(p, len)),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* embedded.ts — 可选入口:让 `bun build --compile` 把原生库打进 exe
|
|
3
|
+
*
|
|
4
|
+
* 用法(编译型消费者):
|
|
5
|
+
* import { createShell } from "bun-app-window";
|
|
6
|
+
* import { embeddedDll } from "bun-app-window/embedded";
|
|
7
|
+
* const shell = await createShell({ url, embedded: embeddedDll });
|
|
8
|
+
*
|
|
9
|
+
* 也可以只写 `import "bun-app-window/embedded";` —— resolve 会自动从
|
|
10
|
+
* `Bun.embeddedFiles` 里找到它,不需要显式传参。
|
|
11
|
+
*
|
|
12
|
+
* 非编译型消费者**不要**导入本文件:它带来的静态 `type: "file"` 依赖
|
|
13
|
+
* 会在没有 dist/libshell.dll 的场景下让打包失败。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// 消费方的 tsconfig 不 include node_modules,因此包内的 assets.d.ts 不会被自动纳入。
|
|
17
|
+
// 用三斜线引用把 *.dll 的模块声明钉在本文件上,否则消费者 `bun check` 会报 TS2307。
|
|
18
|
+
/// <reference path="./assets.d.ts" />
|
|
19
|
+
|
|
20
|
+
// 这条 import 的唯一作用是让打包器把 DLL 记为「需要内嵌」
|
|
21
|
+
import dllPath from "../../dist/libshell.dll" with { type: "file" };
|
|
22
|
+
|
|
23
|
+
const DLL_PATTERN = /^libshell.*\.dll$/i;
|
|
24
|
+
|
|
25
|
+
/** 返回内嵌 DLL 的字节;不在编译产物内或未内嵌时返回 null */
|
|
26
|
+
export async function embeddedDll(): Promise<Uint8Array | null> {
|
|
27
|
+
const files = (Bun as unknown as { embeddedFiles?: ReadonlyArray<Blob & { name: string }> })
|
|
28
|
+
.embeddedFiles;
|
|
29
|
+
if (!files || files.length === 0) return null;
|
|
30
|
+
|
|
31
|
+
const hit = files.find((f) => DLL_PATTERN.test(f.name));
|
|
32
|
+
if (!hit) return null;
|
|
33
|
+
|
|
34
|
+
const buf = new Uint8Array(await hit.arrayBuffer());
|
|
35
|
+
if (buf.length < 2 || buf[0] !== 0x4d || buf[1] !== 0x5a) {
|
|
36
|
+
throw new Error(`embedded native library is not a PE file (${hit.name})`);
|
|
37
|
+
}
|
|
38
|
+
return buf;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 打包器记录的 DLL 路径(仅排障时有用,运行期不要用它读文件) */
|
|
42
|
+
export const embeddedDllPath: string = dllPath;
|