import { app, BrowserWindow, protocol } from "electron"; import path from "path"; import fs from "fs"; import Module from "module"; // 加速 Electron 启动:跳过 GPU 信息收集,减少初始化耗时 app.commandLine.appendSwitch("disable-gpu-shader-disk-cache"); app.commandLine.appendSwitch("disable-features", "CalculateNativeWinOcclusion"); declare const __APP_VERSION__: string | undefined; const SKIP_ENTRIES = new Set(["logs", "oss", "db2.sqlite"]); function copyDir(src: string, dest: string, overwrite = false): void { if (!fs.existsSync(src)) return; fs.mkdirSync(dest, { recursive: true }); for (const entry of fs.readdirSync(src, { withFileTypes: true })) { if (SKIP_ENTRIES.has(entry.name)) continue; const srcPath = path.join(src, entry.name); const destPath = path.join(dest, entry.name); if (entry.isDirectory()) { copyDir(srcPath, destPath, overwrite); } else if (overwrite || !fs.existsSync(destPath)) { fs.copyFileSync(srcPath, destPath); } } } function initializeData(): void { const srcDir = path.join(process.resourcesPath, "data"); const destDir = path.join(app.getPath("userData"), "data"); const updateJsonFile = path.join(destDir, "version.txt"); const currentVersion = typeof __APP_VERSION__ !== "undefined" ? __APP_VERSION__ : "0.0.0"; const userVersion = fs.existsSync(updateJsonFile) ? fs.readFileSync(updateJsonFile, "utf8") : null; if (!userVersion) { copyDir(srcDir, destDir); return; } if (userVersion !== currentVersion) { for (const dir of ["serve", "web"]) { const dest = path.join(destDir, dir); if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true }); copyDir(path.join(srcDir, dir), dest); } copyDir(srcDir, destDir); try { const data = JSON.parse(fs.readFileSync(updateJsonFile, "utf8")); data.version = currentVersion; fs.writeFileSync(updateJsonFile, JSON.stringify(data, null, 4), "utf8"); } catch {} } } //获取全部依赖路径,优先从 unpacked 加载原生模块,其他模块从 asar 加载 function getNodeModulesPaths(): string[] { const paths: string[] = []; if (app.isPackaged) { // external 依赖(原生模块)在 unpacked 目录 const unpackedNodeModules = path.join(process.resourcesPath, "app.asar.unpacked", "node_modules"); if (fs.existsSync(unpackedNodeModules)) { paths.push(unpackedNodeModules); } // 普通依赖在 asar 内 const asarNodeModules = path.join(process.resourcesPath, "app.asar", "node_modules"); paths.push(asarNodeModules); } else { paths.push(path.join(process.cwd(), "node_modules")); } return paths; } //动态加载 function requireWithCustomPaths(modulePath: string): any { const appNodeModulesPaths = getNodeModulesPaths(); // 保存原始方法 const originalNodeModulePaths = (Module as any)._nodeModulePaths; // 临时修改模块路径解析 (Module as any)._nodeModulePaths = function (from: string): string[] { const paths = originalNodeModulePaths.call(this, from); // 将主程序的 node_modules 添加到前面 for (let i = appNodeModulesPaths.length - 1; i >= 0; i--) { const p = appNodeModulesPaths[i]; if (!paths.includes(p)) { paths.unshift(p); } } return paths; }; try { // 清除缓存确保加载最新 delete require.cache[require.resolve(modulePath)]; return require(modulePath); } finally { // 恢复原始方法 (Module as any)._nodeModulePaths = originalNodeModulePaths; } } let mainWindow: BrowserWindow | null = null; let loadingWindow: BrowserWindow | null = null; const loadingHtml = `data:text/html;charset=utf-8,${encodeURIComponent(`

正在启动服务…

`)}`; function showLoading(): void { loadingWindow = new BrowserWindow({ width: 1000, height: 700, minWidth: 800, minHeight: 500, frame: false, resizable: false, maximizable: false, minimizable: false, show: true, backgroundColor: "#ffffff", autoHideMenuBar: true, titleBarStyle: "hidden", titleBarOverlay: { color: "#ffffff", symbolColor: "#333333", height: 36, }, }); loadingWindow.setMenuBarVisibility(false); loadingWindow.removeMenu(); loadingWindow.on("closed", () => { loadingWindow = null; }); void loadingWindow.loadURL(loadingHtml); } function closeLoading(): void { if (loadingWindow && !loadingWindow.isDestroyed()) { loadingWindow.close(); loadingWindow = null; } } function createMainWindow(): Promise { return new Promise((resolve) => { const win = new BrowserWindow({ width: 1000, height: 700, minWidth: 800, minHeight: 500, frame: false, show: false, autoHideMenuBar: true, resizable: true, thickFrame: true, }); mainWindow = win; win.setMenuBarVisibility(false); win.removeMenu(); win.on("closed", () => { mainWindow = null; }); win.once("ready-to-show", () => { closeLoading(); win.show(); resolve(); }); const isDev = process.env.NODE_ENV === "dev" || !app.isPackaged; if (process.env.VITE_DEV) { void win.loadURL("http://localhost:50188"); } else { const htmlPath = isDev ? path.join(process.cwd(), "data", "web", "index.html") : path.join(app.getPath("userData"), "data", "web", "index.html"); void win.loadFile(htmlPath); } }); } let closeServeFn: (() => Promise) | undefined; protocol.registerSchemesAsPrivileged([ { scheme: "toonflow", privileges: { secure: true, supportFetchAPI: true, corsEnabled: true, }, }, ]); app.whenReady().then(async () => { // 立即显示加载窗口(data URL + backgroundColor,瞬间可见) showLoading(); try { let servePath: string; if (app.isPackaged) { // 生产环境:让出主线程一次,确保 loading 窗口渲染后再做耗时文件拷贝 await new Promise((r) => setTimeout(r, 0)); initializeData(); servePath = path.join(app.getPath("userData"), "data", "serve", "app.js"); } else { // 开发环境:直接加载源码(tsx 通过 -r tsx 注册了 require 钩子) servePath = path.join(process.cwd(), "src", "app.ts"); } // 使用自定义路径加载模块 const mod = requireWithCustomPaths(servePath); closeServeFn = mod.closeServe; const port = await mod.default(true); process.env.PORT = port; await new Promise((resolve, reject) => { setTimeout(() => { resolve(); }, 2000); }); // 注册协议处理器 protocol.handle("toonflow", (request) => { const url = new URL(request.url); const pathname = url.hostname.toLowerCase(); const handlers: Record object> = { getport: () => ({ port: port }), windowminimize: () => { mainWindow?.minimize(); return { ok: true }; }, windowmaximize: () => { if (mainWindow?.isMaximized()) { mainWindow.unmaximize(); } else { mainWindow?.maximize(); } return { ok: true }; }, windowclose: () => { app.exit(0); return { ok: true }; }, apprestart: () => { // 延迟执行,让响应先返回给前端 setTimeout(() => { app.relaunch(); app.exit(0); }, 500); return { ok: true, message: "应用即将重启" }; }, windowismaximized: () => ({ maximized: mainWindow?.isMaximized() ?? false, }), opendevtool: () => { mainWindow?.webContents.openDevTools(); return { ok: true }; }, openurlwithbrowser: () => { const search = url.searchParams; const targetUrl = search.get("url"); if (targetUrl) { const { shell } = require("electron"); shell.openExternal(targetUrl); return { ok: true }; } else { return { ok: false, error: "缺少url参数" }; } }, }; const handler = handlers[pathname]; const responseData = handler ? handler() : { error: "未知接口" }; return new Response(JSON.stringify(responseData), { headers: { "Content-Type": "application/json", "Cache-Control": "no-store", }, }); }); // 服务启动成功,创建主窗口(主窗口 ready-to-show 时自动关闭loading) await createMainWindow(); } catch (err) { console.error("[服务启动失败]:", err); await createMainWindow(); } }); app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); }); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { createMainWindow(); } }); app.on("before-quit", async (event) => { if (closeServeFn) await closeServeFn(); });