forked from imbytecat/fullstack-starter
refactor(desktop): 替换 WebUI 为 Electron + electron-vite 桌面壳方案
- 使用 electron-vite 构建 main/preload,electron-builder 打包分发 - main process: dev 模式直连 localhost:3000,生产模式 spawn sidecar binary - 添加 loading 页面,server 就绪前显示加载动画 - 更新 catalog 依赖: electron, electron-vite, electron-builder - 移除 @webui-dev/bun-webui 依赖
This commit is contained in:
@@ -1,55 +0,0 @@
|
||||
import { WebUI } from '@webui-dev/bun-webui'
|
||||
|
||||
const DEV_SERVER_URL = 'http://localhost:3000'
|
||||
const SERVER_READY_TIMEOUT_MS = 10_000
|
||||
|
||||
const isServerReady = async (url: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(url, { method: 'HEAD' })
|
||||
return response.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const waitForServer = async (
|
||||
url: string,
|
||||
timeoutMs = SERVER_READY_TIMEOUT_MS,
|
||||
): Promise<boolean> => {
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
if (await isServerReady(url)) return true
|
||||
await Bun.sleep(100)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
console.log('Starting Furtherverse Desktop...')
|
||||
|
||||
console.log('Waiting for server at', DEV_SERVER_URL)
|
||||
const ready = await waitForServer(DEV_SERVER_URL)
|
||||
if (!ready) {
|
||||
console.error(
|
||||
'Dev server not responding. Run `bun dev` in apps/server first.',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('Opening window at', DEV_SERVER_URL)
|
||||
|
||||
const win = new WebUI()
|
||||
win.setSize(1200, 800)
|
||||
win.setCenter()
|
||||
await win.show(DEV_SERVER_URL)
|
||||
|
||||
console.log('Window opened. Waiting for close...')
|
||||
await WebUI.wait()
|
||||
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Failed to start:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
122
apps/desktop/src/main/index.ts
Normal file
122
apps/desktop/src/main/index.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { join } from 'node:path'
|
||||
import { app, BrowserWindow, shell } from 'electron'
|
||||
|
||||
const DEV_SERVER_URL = 'http://localhost:3000'
|
||||
const PROD_SERVER_PORT = 23_410
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
let serverProcess: ReturnType<typeof spawn> | null = null
|
||||
|
||||
const isServerReady = async (url: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch(url, { method: 'HEAD' })
|
||||
return response.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const waitForServer = async (
|
||||
url: string,
|
||||
timeoutMs = 15_000,
|
||||
): Promise<boolean> => {
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
if (await isServerReady(url)) return true
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 200))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const spawnServer = (): string => {
|
||||
const binaryName = process.platform === 'win32' ? 'server.exe' : 'server'
|
||||
const binaryPath = join(process.resourcesPath, binaryName)
|
||||
|
||||
serverProcess = spawn(binaryPath, [], {
|
||||
env: {
|
||||
...process.env,
|
||||
PORT: String(PROD_SERVER_PORT),
|
||||
HOST: '127.0.0.1',
|
||||
},
|
||||
stdio: 'pipe',
|
||||
})
|
||||
|
||||
serverProcess.stdout?.on('data', (data: Buffer) => {
|
||||
console.log(`[server] ${data.toString().trim()}`)
|
||||
})
|
||||
|
||||
serverProcess.stderr?.on('data', (data: Buffer) => {
|
||||
console.error(`[server] ${data.toString().trim()}`)
|
||||
})
|
||||
|
||||
serverProcess.on('error', (err) => {
|
||||
console.error('Failed to start server:', err)
|
||||
})
|
||||
|
||||
return `http://127.0.0.1:${PROD_SERVER_PORT}`
|
||||
}
|
||||
|
||||
const getServerUrl = (): string => {
|
||||
if (!app.isPackaged) {
|
||||
return DEV_SERVER_URL
|
||||
}
|
||||
return spawnServer()
|
||||
}
|
||||
|
||||
const createWindow = async () => {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.mjs'),
|
||||
sandbox: true,
|
||||
},
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
shell.openExternal(url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
mainWindow.show()
|
||||
|
||||
const serverUrl = getServerUrl()
|
||||
|
||||
console.log(`Waiting for server at ${serverUrl}...`)
|
||||
const ready = await waitForServer(serverUrl)
|
||||
if (!ready) {
|
||||
console.error(
|
||||
app.isPackaged
|
||||
? 'Server binary did not start in time.'
|
||||
: 'Dev server not responding. Run `bun dev` in apps/server first.',
|
||||
)
|
||||
app.quit()
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`Loading ${serverUrl}`)
|
||||
mainWindow.loadURL(serverUrl)
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow)
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('before-quit', () => {
|
||||
if (serverProcess) {
|
||||
serverProcess.kill()
|
||||
serverProcess = null
|
||||
}
|
||||
})
|
||||
1
apps/desktop/src/preload/index.ts
Normal file
1
apps/desktop/src/preload/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {}
|
||||
40
apps/desktop/src/renderer/index.html
Normal file
40
apps/desktop/src/renderer/index.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Furtherverse</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0a0a0a;
|
||||
color: #fafafa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
.loader {
|
||||
text-align: center;
|
||||
}
|
||||
.spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid #333;
|
||||
border-top-color: #fafafa;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
p { font-size: 14px; color: #888; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="loader">
|
||||
<div class="spinner"></div>
|
||||
<p>Starting server…</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user