UNPKG

mixone

Version:

MixOne is a Node scaffolding tool implemented based on Vite, used for compiling HTML5, JavasCript, Vue, React and other codes. It supports packaging Web applications with multiple HTML entry points (BS architecture) and desktop installation packages (CS a

622 lines (595 loc) 22.3 kB
const { BrowserWindow, ipcMain, app } = require('electron'); const path = require('path'); const fs = require('fs'); const { URL } = require('url'); // 保持对窗口对象的全局引用 function filterString(str) { return str.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\x00-\x7F\u4E00-\u9FFF]/g, ''); } // 重写控制台输出方法 console.log = (...args) => { const message = args.map(arg => { if (typeof arg === 'string') { return filterString(arg); } return arg; }).join(' '); process.stdout.write(message); }; console.error = (...args) => { const message = args.map(arg => { if (typeof arg === 'string') { return filterString(arg); } return arg; }).join(' '); process.stderr.write(message); }; function isDevelopmentMode() { // 检查命令行参数是否有 --dev 标志 const isDev = process.argv.includes('--dev'); // 检查环境变量 const nodeEnv = process.env.NODE_ENV; const isDevEnv = nodeEnv === 'development' || nodeEnv === 'dev'; return isDev || isDevEnv; } function getDevServerUrl() { try { const serverInfoPath = path.join(__dirname, 'dev-server.json'); if (fs.existsSync(serverInfoPath)) { const serverInfo = JSON.parse(fs.readFileSync(serverInfoPath, 'utf-8')); let url = serverInfo.url; // 处理可能的 IPv6 地址 if (url.includes('://::') || url.includes('://::1') || url.includes('://[::') || url.includes('://[::1')) { // 替换为 localhost url = url.replace(/:\/{2}(\[)?::(1)?(\])?/, '://localhost'); } // console.log(`Get the development server address: ${url}`); return url; } } catch (err) { console.error('Failed to read the information of the development server:', err); } return null; } // ... 在 WindowManager 类外部添加工具函数 function serializeEventArgs(args) { return args.map(arg => { if (arg === null || arg === undefined) return arg; if (arg instanceof Error) { return { message: arg.message, name: arg.name, stack: arg.stack }; } // 处理 Electron 对象 if (arg.constructor && arg.constructor.name === 'Event') { return { type: arg.type, timeStamp: arg.timeStamp }; } if (typeof arg === 'object') { // 只保留可序列化的属性 return JSON.parse(JSON.stringify(arg, (key, value) => { if (value && typeof value === 'object' && !Array.isArray(value)) { const cleaned = {}; for (let k in value) { try { JSON.stringify(value[k]); cleaned[k] = value[k]; } catch (e) { // 忽略不可序列化的属性 } } return cleaned; } return value; })); } return arg; }); } function isFilePath(baseWithoutHash){ if(baseWithoutHash.endsWith('/')){ return false; } const lastSlashIndex = baseWithoutHash.lastIndexOf('/'); const lastPart = baseWithoutHash.substring(lastSlashIndex + 1); if(lastPart.includes('.')){ return true; } return false; } function farmatWindowName(windowPath) { if(!windowPath){ return false } const [baseWithoutHash] = windowPath.split('#'); if(!isFilePath(baseWithoutHash)){ windowPath = (windowPath.endsWith('/') ? windowPath : windowPath + '/') + 'index.html'; } return windowPath; } function isWindowDir(fullPath){ if(fullPath.endsWith('-window')){ return true } if(fs.existsSync(path.join(fullPath,'window.json'))){ return true } return false } function findWindowDir(windowFullFilePath){ let arr = windowFullFilePath.split(path.sep); let isLoop = true; while(isLoop){ let lastNode = arr.pop(); if(lastNode == 'windows'){ return path.join(arr.join(path.sep),'windows'); } else { let dir = arr.join(path.sep); if(isWindowDir(dir)){ return dir; } } if(arr.length){ isLoop = false } } return false; } function appendLog(message) { const logFilePath = path.join(app.getPath('userData'), 'logs', 'main_process.log'); if(!fs.existsSync(path.dirname(logFilePath))) { fs.mkdirSync(path.dirname(logFilePath), { recursive: true }); } fs.appendFileSync(logFilePath, message + "/r/n", (err) => { if (err) console.error('Failed to write uncaughtException to log file:', err); }); } class WindowManager { constructor() { this.windows = new Map(); this.configs = new Map(); this.lastWinId = 0; // 添加跟踪最后使用的窗口 ID this.setupWindowPositionTracking(); this.setupWindowObjectActions(); } setupWindowPositionTracking() { ipcMain.on('window-moved', (event, { winId, bounds }) => { const win = this.windows.get(winId); if (win) { win.lastBounds = bounds; } }); } setupWindowObjectActions() { ipcMain.handle('window-obj-action', async (event, { method, winId, type, args }) => { try { const win = this.getWindow(winId); if (!win) { throw new Error(`Can't find the window:${winId}`); } if (type === 'property') { // 获取窗口属性 return { success: true, result: win[method] }; } else if (type === 'method') { // 调用窗口方法 const result = await win[method](...(args || [])); return { success: true, result }; } } catch (error) { return { success: false, error: error.message }; } }); } // 对所有窗口广播,传入winId,与自己有关的都取。 owerWindowBroadcastToAllWindows(eventName, args, sourceWinId) { for (const [_, winInfo] of this.windows) { winInfo.window.webContents.send('broadcast-window-event', { eventName, args, winId: sourceWinId }); } } owerWindowObjBroadcastToAllWindows(eventName, args, sourceWinId) { for (const [_, winInfo] of this.windows) { winInfo.window.webContents.send('broadcast-window-obj-event', { eventName, args, winId: sourceWinId }); } } async openWindow(windowPath, options = {}) { let windowConfig = {},windowFullDir,windowFullFilePath,willOpenUrl; if(['http://','https://','file:///'].find(item => windowPath.startsWith(item))){ if(['http://','https://'].find(item=>windowPath.startsWith(item))) { if(isDevelopmentMode()){ const devServerUrl = getDevServerUrl(); if (devServerUrl) { const myUrl = new URL(windowPath); const devUrl = new URL(devServerUrl); if(myUrl.hostname == devUrl.hostname && myUrl.host == devUrl.host){//这里只是处理 windowFullFilePath windowFullDir用于得到options let formatPathname = myUrl.pathname.split('/') if(!isDevelopmentMode()){ formatPathname.splice(formatPathname.length - 2,0,'dist'); } let _formatPathname = formatPathname.join(path.sep) windowFullFilePath = path.join(app.getAppPath(),'windows',_formatPathname); windowFullDir = findWindowDir(windowFullFilePath); if(windowFullDir === false){ console.log(`Can't find the window dir:${windowFullFilePath}`) return } } } } willOpenUrl = windowPath; } else { windowPath = farmatWindowName(windowPath); windowFullFilePath = windowPath.replace('file:///','').split('/').join(path.sep); windowFullDir = findWindowDir(windowFullFilePath); if(windowFullDir === false){ console.log(`Can't find the window dir:${windowFullFilePath}`) return } willOpenUrl = windowFullFilePath; } } else { //判断是否有文件名。 // 是否不为真,取消打开窗口 //是否有#号,hash模式。去除#后字符串 //是否以‘/’结尾,若是则无文件名 //是否有扩展名,若有则指定文件了。否则无文件名 //给无扩展名的文件增加默认文件。 windowPath = farmatWindowName(windowPath); if(!windowPath){ console.log(`The window name you entered is incorrect`) return false } let windowPaths = windowPath.split('/'); if(app.isPackaged){ windowFullFilePath = path.join(app.getAppPath(),'out','build','windows',...windowPaths); willOpenUrl = windowFullFilePath } else { if(isDevelopmentMode()){ const devServerUrl = getDevServerUrl(); if (devServerUrl) { windowFullFilePath = path.join(app.getAppPath(),'windows',...windowPaths); willOpenUrl = `${devServerUrl}${windowPaths.join('/')}`; console.log(`Development mode loading:${willOpenUrl}`); // win.webContents.openDevTools(); } else { console.log('The development server URL was not found. Use the file loading mode'); } } else { windowFullFilePath = path.join(app.getAppPath(),'build','windows',...windowPaths); willOpenUrl = windowFullFilePath; } } windowFullDir = findWindowDir(windowFullFilePath); if(windowFullDir === false){ console.log(`Can't find the window dir:${windowFullFilePath}`) return } } function loadWindowJson(windowFullDir) { let windowConfig = {} if(!windowFullDir){//如果是http(https)协议的非localhost,windowFullDir是undefined,还有端口没有指定。 return windowConfig; } const windowJsonPath = path.join(windowFullDir, 'window.json'); try { windowConfig = fs.existsSync(windowJsonPath) ? JSON.parse(fs.readFileSync(windowJsonPath, 'utf-8')) : {}; } catch (err) { console.error('Parsing window configuration failed:', err.message); windowConfig = {} } return windowConfig; } windowConfig = loadWindowJson(windowFullDir); // windowPath = path.join('out',windowPath); // console.log('app.getAppPath()') //生产 E:\work\electron\demo-vue3-5\dist\packager\win-unpacked\resources\app.asar // console.log(app.getAppPath()) //开发环境到out let fromWinId = 0 if(options.fromWinId) { fromWinId = options.fromWinId; delete options.fromWinId } const predictedId = this.lastWinId + 1; windowConfig.webPreferences = windowConfig.webPreferences || {}; if(windowConfig.framework){ delete windowConfig.framework; } const windowOptions = { icon: path.join(__dirname, '../assets/favicon.ico'), ...windowConfig, ...options, webPreferences: { ...windowConfig.webPreferences } }; if(windowFullDir){//如果是http(https)协议的非localhost,windowFullDir是undefined,还有端口没有指定。 windowOptions['webPreferences']['preload'] = path.join(windowFullDir, 'preload.js'); } windowOptions['webPreferences']['nodeIntegration'] = true; windowOptions['webPreferences']['contextIsolation'] = true; const _windowOptions = windowOptions; _windowOptions['webPreferences']['additionalArguments'] = [`--window-id=${predictedId}`,`--REFERER-window-id=${fromWinId}`]; windowOptions['name'] = windowOptions['name'] ? windowOptions['name'] : 'Mixone'; if(!windowOptions['width'] && !windowOptions['height']) { windowOptions['width'] = 950; windowOptions['height'] = 700; } if(windowConfig.autoOffset){ if(fromWinId){ let fromWin = this.getWindow(fromWinId) let [ox, oy] = fromWin.getPosition(); let [owidth,oheight] = fromWin.getSize(); if(fromWin){ if(owidth==windowOptions['width']){ windowOptions.x = ox + 20; } if(oheight==windowOptions['height']){ windowOptions.y = oy + 20; } } } delete windowConfig.autoOffset; delete windowConfig.windowOptions; } if(!['http://','https://'].find(item=>willOpenUrl.startsWith(item))){ if(!fs.existsSync(willOpenUrl)) { console.warn(`The window file does not exist: ${willOpenUrl}`); return; } } const win = new BrowserWindow(windowOptions); // 在创建窗口后立即更新 lastWinId this.lastWinId = win.id; // 验证预测是否正确 // 验证预测是否正确 if (predictedId !== win.id) { console.warn(`Inaccurate window ID prediction: Prediction =${predictedId}, actual =${win.id}`); } // 监听窗口 webContents 的所有事件并转发 const forwardEvents = [ 'did-finish-load','did-fail-load','did-fail-provisional-load','did-frame-finish-load','did-start-loading','did-stop-loading','dom-ready','page-title-updated','page-favicon-updated','content-bounds-updated','did-create-window','will-navigate','will-frame-navigate','did-start-navigation','will-redirect','did-redirect-navigation','did-navigate','did-frame-navigate','did-navigate-in-page','will-prevent-unload','render-process-gone','unresponsive','responsive','plugin-crashed','destroyed', // 'input-event', 'before-input-event','enter-html-full-screen','leave-html-full-screen','zoom-changed','blur','focus','devtools-open-url','devtools-search-query','devtools-opened','devtools-closed','devtools-focused','certificate-error','select-client-certificate','login','found-in-page','media-started-playing','media-paused','audio-state-changed','did-change-theme-color','update-target-url', // 'cursor-changed', 'context-menu','select-bluetooth-device','paint','devtools-reload-page','will-attach-webview','did-attach-webview', // 'console-message', 'preload-error','ipc-message','ipc-message-sync','preferred-size-changed', // 'frame-created' ]; this.windows.set(win.id, { window: win, winId: win.id, path: windowPath, windowOptions, }); forwardEvents.forEach(eventName => { if(eventName==='destroyed'){return} win.webContents.on(eventName, (...args) => { try { const serializedArgs = serializeEventArgs(args); this.owerWindowBroadcastToAllWindows(eventName, serializedArgs, win.id); // 广播事件到所有窗口,为什么要广播,这主要是因为其他窗口可能需要监听新创建的窗口相关内容,这里就是因为创建窗口要监听它的时间。 } catch (e) { console.error('Event forwarding failed:', eventName, e); } }); }); const windowObjEvents = [//,'closed' 在electron22以下转发不了 'page-title-updated','close','query-session-end','session-end','unresponsive','responsive','blur','focus','show','hide','ready-to-show','maximize','unmaximize','minimize','restore','will-resize','resize','resized','will-move','move','moved','enter-full-screen','leave-full-screen','enter-html-full-screen','leave-html-full-screen','always-on-top-changed','app-command','swipe','rotate-gesture','sheet-begin','sheet-end','new-window-for-tab','system-context-menu' ] windowObjEvents.forEach(eventName => { win.on(eventName, (...args) => { try { const serializedArgs = serializeEventArgs(args); this.owerWindowObjBroadcastToAllWindows(eventName, serializedArgs, win.id); // 广播事件到所有窗口,为什么要广播,这主要是因为其他窗口可能需要监听新创建的窗口相关内容,这里就是因为创建窗口要监听它的时间。 } catch (e) { console.error('Event forwarding failed:', eventName, e); } }); }); if(['http://','https://'].find(item=>willOpenUrl.startsWith(item))){ await win.loadURL(willOpenUrl); } else { await win.loadFile(willOpenUrl); } this.setupWindowCommunication(win, windowPath); win.on('closed', () => { // this.windows.delete(win.id); }); win.webContents.on('destroyed', () => { this.windows.delete(win.id); }); return win; } async _openWindow(windowPath, options = {}) { let win = await this.openWindow(windowPath, options); let winInfo = this.windows.get(win.id) let _winInfo = {}; for (const key in winInfo) { if(key !== 'window'){ _winInfo[key] = winInfo[key] } } return _winInfo; } async restoreWindow(windowPath, state) { if(windowPath.indexOf('/')===0){ windowPath = windowPath.trim('/'); } if(windowPath.indexOf(path.sep)===0){ windowPath = windowPath.trim(path.sep); } windowPath = windowPath.indexOf('/') === -1 ? windowPath : path.join(...windowPath.split('/')); console.log('Restore the window state...', windowPath); let win = await this.openWindow(windowPath, {}); // 加载原始URL await win.loadURL(state.url); if(state.bounds){ win.setBounds(state.bounds); } // 恢复窗口状态 if (state.isMaximized) { win.maximize(); } else if (state.isMinimized) { win.minimize(); } else if (state.isFullScreen) { win.setFullScreen(true); } } setupWindowCommunication(win) { ipcMain.on(`window-message:${win.id}`, (event, { winId, channel, data }) => { const targetWindow = this.getWindowByWinId(winId); targetWindow.webContents.send(channel, data); }); ipcMain.on(`window-broadcast:${win.id}`, (event, { channel, data }) => { for (const [winId, winInfo] of this.windows) { if (winId !== win.id) { winInfo.window.webContents.send(channel, data); } } }); } getWindowInfo(winId) { const winInfo = this.windows.get(winId); if(!winInfo){ console.log('The window does not exist.', winId); return null; } return winInfo ? winInfo : null; } _getWindowInfo(winId) { let winInfo = this.getWindowInfo(winId); let _winInfo = {}; for (const key in winInfo) { if(key !== 'window'){ _winInfo[key] = winInfo[key] } } return _winInfo; } getWindow(winId) { const winInfo = this.windows.get(winId); if(!winInfo){ console.log('The window does not exist.', winId); return null; } return winInfo ? winInfo.window : null; } getAllWindow() { return Array.from(this.windows.values()).map(winInfo => { return winInfo; }); } _getAllWindow() { return Array.from(this.windows.values()).map(winInfo => { let _winInfo = {}; for (const key in winInfo) { if(key !== 'window'){ _winInfo[key] = winInfo[key] } } return _winInfo; }); } // 保存窗口状态 saveWindowState(winId) { const winInfo = this.windows.get(winId); if (!winInfo) return null; const win = winInfo.window; return { bounds: win.getBounds(), isMaximized: win.isMaximized(), isMinimized: win.isMinimized(), isFullScreen: win.isFullScreen() }; } // 向指定窗口发送消息 sendToWindow(winId, eventName, data) { const win = this.getWindow(winId); if (!win) { console.error(`The target window cannot be found: ${winId}`); return false; } try { win.webContents.send('windowEvent:'+winId,{eventName, data}); return true; } catch (error) { console.error(`Message sending failed: ${error.message}`); return false; } } // 广播消息给所有窗口(可选排除发送者) broadcast(channel, data, excludeWinIds = []) { for (const [winId, winInfo] of this.windows) { if (excludeWinIds.includes(winId)) continue; try { winInfo.window.webContents.send('broadcast',{channel, data}); } catch (error) { console.error(`Broadcast failed to window ${winId}: ${error.message}`); } } } async openModalWindow(parentWinId, windowPath, options = {}) { const parentWindow = this.getWindow(parentWinId); if (!parentWindow) { throw new Error(`The parent window does not exist: ${parentWinId}`); } // 合并选项,强制设置 parent 和 modal const modalOptions = { ...options, parent: parentWindow, modal: true }; const win = await this.openWindow(windowPath, modalOptions); // 在父窗口记录中添加子窗口信息 const parentInfo = this.windows.get(parentWinId); if (!parentInfo.children) { parentInfo.children = {}; } parentInfo.children[win.id] = this._getWindowInfo(win.id); this.windows.delete(parentWinId); this.windows.set(parentWinId, parentInfo); // 当子窗口关闭时,从父窗口记录中移除 win.on('close', () => { if(parentInfo.children && parentInfo.children[win.id]){ delete parentInfo.children[win.id]; } }); return win; } async _openModalWindow(parentWinId, windowPath, options = {}) { const win = await this.openModalWindow(parentWinId, windowPath, options); let winInfo = this.windows.get(win.id); let _winInfo = {}; for (const key in winInfo) { if(key !== 'window'){ _winInfo[key] = winInfo[key]; } } _winInfo['windowOptions']['parent'] = 'un serialize'; return _winInfo; } } const windowManager = new WindowManager(); module.exports = windowManager;