mixone
Version:
A cross-platform desktop application framework that enables development using JavaScript, HTML5, Electron API and Node.js. Build Windows, macOS and Linux apps with Vue/React and system APIs - that's why we call it mixone.A powerful Electron development in
454 lines (428 loc) • 16 kB
JavaScript
const { BrowserWindow, ipcMain, app } = require('electron');
const path = require('path');
const fs = require('fs');
// 保持对窗口对象的全局引用
const iconv = require('iconv-lite');
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(iconv.encode(message + '\n', 'gb2312'));
};
console.error = (...args) => {
const message = args.map(arg => {
if (typeof arg === 'string') {
return filterString(arg);
}
return arg;
}).join(' ');
process.stderr.write(iconv.encode(message + '\n', 'gb2312'));
};
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(`📡 获取到开发服务器地址: ${url}`);
return url;
}
} catch (err) {
console.error('读取开发服务器信息失败3:', 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;
});
}
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(`找不到窗口: ${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 = {}) {
windowPath = windowPath == '/' ? 'windows' : windowPath.trim('/');
windowPath = windowPath.indexOf('/') === -1 ? windowPath : path.join(...windowPath.split('/'));
const windowJsonPath = path.join(app.getAppPath(), windowPath, 'window.json');
const windowConfig = JSON.parse(fs.readFileSync(windowJsonPath, 'utf-8'));
if (!windowConfig) {
throw new Error(`Window config not found: ${windowPath}`);
}
const predictedId = this.lastWinId + 1;
windowConfig.webPreferences = windowConfig.webPreferences || {};
const windowOptions = {
icon: path.join(__dirname, '../assets/favicon.ico'),
...windowConfig,
...options,
webPreferences: {
...windowConfig.webPreferences,
preload:path.join(app.getAppPath(), windowPath, 'preload.js')
}
};
windowOptions['webPreferences']['nodeIntegration'] = true;
windowOptions['webPreferences']['contextIsolation'] = true;
const _windowOptions = windowOptions;
_windowOptions['webPreferences']['additionalArguments'] = [`--window-id=${predictedId}`];
const win = new BrowserWindow(windowOptions);
// 在创建窗口后立即更新 lastWinId
this.lastWinId = win.id;
// 验证预测是否正确
// 验证预测是否正确
if (predictedId !== win.id) {
console.warn(`❗ 窗口 ID 预测不准确: 预测=${predictedId}, 实际=${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('❌ 事件转发失败:', 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('❌ 事件转发失败:', eventName, e);
}
});
});
if (isDevelopmentMode()) {
const devServerUrl = getDevServerUrl();
if (devServerUrl) {
let urlPath = windowPath.replace(/^windows[\/\\]?/, '');
urlPath = urlPath ? '/' + urlPath : '';
const windowUrl = `${devServerUrl}${urlPath}/index.html`;
console.log(`🔍 开发模式加载: ${windowUrl}`);
await win.loadURL(windowUrl);
// win.webContents.openDevTools();
} else {
console.log('⚠️ 未找到开发服务器URL,使用文件加载模式');
await win.loadFile(path.join(__dirname, '../windows', windowConfig.name, 'index.html'));
}
} else {
const indexPath = path.join(app.getAppPath(), windowPath, 'dist', 'index.html');
await win.loadFile(indexPath);
}
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('🔄 恢复窗口状态...', 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('窗口不存在2', 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('窗口不存在3', 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(`❌ 找不到目标窗口: ${winId}`);
return false;
}
try {
win.webContents.send('windowEvent:'+winId,{eventName, data});
return true;
} catch (error) {
console.error(`❌ 发送消息失败: ${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(`❌ 广播到窗口 ${winId} 失败: ${error.message}`);
}
}
}
async openModalWindow(parentWinId, windowPath, options = {}) {
const parentWindow = this.getWindow(parentWinId);
if (!parentWindow) {
throw new Error(`父窗口不存在: ${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;