Lesson 07-Electron Multi-Window Applications

Multi-Window Architecture Design

Window Manager Pattern

// WindowManager.js - Window Manager Implementation
const { BrowserWindow, ipcMain } = require('electron');
const path = require('path');

class WindowManager {
  constructor() {
    this.windows = new Map(); // Store all window instances
    this.setupIPC(); // Initialize IPC communication
  }

  // Create new window
  createWindow(windowId, options = {}) {
    if (this.windows.has(windowId)) {
      console.warn(`Window ${windowId} already exists`);
      return this.windows.get(windowId);
    }

    const defaultOptions = {
      width: 800,
      height: 600,
      webPreferences: {
        nodeIntegration: false,
        contextIsolation: true,
        preload: path.join(__dirname, 'preload.js')
      }
    };

    const win = new BrowserWindow({
      ...defaultOptions,
      ...options
    });

    // Window lifecycle management
    win.on('closed', () => {
      this.windows.delete(windowId);
      win = null; // Release reference
    });

    this.windows.set(windowId, win);
    return win;
  }

  // Get window instance
  getWindow(windowId) {
    return this.windows.get(windowId);
  }

  // Close window
  closeWindow(windowId) {
    const win = this.getWindow(windowId);
    if (win) {
      win.close();
    }
  }

  // Setup IPC communication
  setupIPC() {
    // Create window
    ipcMain.handle('window:create', (event, windowId, options) => {
      return this.createWindow(windowId, options);
    });

    // Close window
    ipcMain.handle('window:close', (event, windowId) => {
      this.closeWindow(windowId);
    });

    // Window communication
    ipcMain.handle('window:send', (event, windowId, channel, ...args) => {
      const win = this.getWindow(windowId);
      if (win) {
        win.webContents.send(channel, ...args);
      }
    });
  }

  // Get all windows
  getAllWindows() {
    return Array.from(this.windows.values());
  }
}

// Export singleton
module.exports = new WindowManager();

Window Creation and Communication

Window Creation Example

// main.js - Main Process
const { app } = require('electron');
const WindowManager = require('./WindowManager');

app.whenReady().then(() => {
  // Create main window
  const mainWindow = WindowManager.createWindow('main', {
    width: 1024,
    height: 768,
    title: 'Main Window'
  });

  mainWindow.loadFile('main.html');

  // Example: Delayed creation of settings window
  setTimeout(() => {
    WindowManager.createWindow('settings', {
      parent: mainWindow, // Set parent window
      modal: false,
      width: 600,
      height: 400,
      title: 'Settings'
    }).loadFile('settings.html');
  }, 2000);
});

Inter-Window Communication

Event-Based Bus Pattern

// eventBus.js - Cross-window event bus
const { ipcRenderer } = require('electron');

class EventBus {
  constructor() {
    this.channels = new Map();
  }

  // Subscribe to event
  on(channel, callback) {
    if (!this.channels.has(channel)) {
      this.channels.set(channel, new Set());
    }
    this.channels.get(channel).add(callback);

    // Listen for messages from main process
    ipcRenderer.on(`event:${channel}`, (_, ...args) => {
      callback(...args);
    });
  }

  // Emit event
  emit(channel, ...args) {
    // Send to main process for broadcasting
    ipcRenderer.send('event:broadcast', channel, ...args);
  }
}

// Export singleton
module.exports = new EventBus();

Main Process Event Forwarding

// main.js - Event forwarding
const { ipcMain } = require('electron');
const WindowManager = require('./WindowManager');

// Event forwarding system
ipcMain.on('event:broadcast', (event, channel, ...args) => {
  // Get all windows
  const windows = WindowManager.getAllWindows();

  // Broadcast to all windows
  windows.forEach(win => {
    if (!win.isDestroyed()) {
      win.webContents.send(`event:${channel}`, ...args);
    }
  });
});

Renderer Process Communication Example

// renderer.js - Renderer process usage example
const eventBus = require('./eventBus');

// Subscribe to event
eventBus.on('data-updated', (data) => {
  console.log('Received data update:', data);
  document.getElementById('data-display').textContent = JSON.stringify(data);
});

// Emit event
document.getElementById('update-btn').addEventListener('click', () => {
  eventBus.emit('request-data', { requestId: Date.now() });
});

Window Lifecycle Management

Window State Persistence

// windowState.js - Window state management
const { ipcRenderer } = require('electron');
const Store = require('electron-store');

const store = new Store();

class WindowState {
  constructor(windowId) {
    this.windowId = windowId;
    this.stateKey = `window-state-${windowId}`;
  }

  // Save window state
  saveState(bounds) {
    store.set(this.stateKey, bounds);
  }

  // Load window state
  loadState() {
    return store.get(this.stateKey, {
      x: undefined,
      y: undefined,
      width: 800,
      height: 600
    });
  }
}

// Usage example
ipcRenderer.on('window:will-move', (_, bounds) => {
  windowState.saveState(bounds);
});

// Apply state when creating window
const initialState = windowState.loadState();
const win = new BrowserWindow({
  ...initialState,
  // Other configurations...
});

Window Restoration Strategy

// Window restoration manager
class WindowRestorer {
  constructor() {
    this.pendingWindows = new Map();
  }

  // Register window to restore
  registerWindow(windowId, createFn) {
    this.pendingWindows.set(windowId, createFn);
  }

  // Restore window
  restoreWindow(windowId) {
    const createFn = this.pendingWindows.get(windowId);
    if (createFn) {
      createFn();
      this.pendingWindows.delete(windowId);
    }
  }
}

// Main process usage example
const restorer = new WindowRestorer();

// Restore windows on app startup
app.whenReady().then(() => {
  const shouldRestore = store.get('should-restore-windows', true);

  if (shouldRestore) {
    const openWindows = store.get('open-windows', []);
    openWindows.forEach(windowId => {
      restorer.restoreWindow(windowId);
    });
  }
});

// Save state on window close
ipcMain.on('window:close', (event, windowId) => {
  const win = WindowManager.getWindow(windowId);
  if (win) {
    const bounds = win.getBounds();
    store.set(`window-state-${windowId}`, bounds);

    // Record open windows
    const openWindows = store.get('open-windows', []);
    if (!openWindows.includes(windowId)) {
      store.set('open-windows', [...openWindows, windowId]);
    }
  }
});

Advanced Window Modes

// modalWindow.js - Modal window management
class ModalWindow {
  constructor(parentWindow, options = {}) {
    this.parentWindow = parentWindow;
    this.options = {
      width: 400,
      height: 300,
      parent: parentWindow,
      modal: true,
      ...options
    };
  }

  create(contentPath) {
    this.win = new BrowserWindow(this.options);
    this.win.loadFile(contentPath);

    // Focus parent window when modal closes
    this.win.on('closed', () => {
      if (!this.win.isDestroyed()) {
        this.win = null;
      }
      this.parentWindow.focus();
    });

    return this.win;
  }
}

// Usage example
const parentWin = WindowManager.getWindow('main');
const modal = new ModalWindow(parentWin);
modal.create('modal.html');

Document-Associated Windows

// documentWindows.js - Document-associated window management
class DocumentWindows {
  constructor() {
    this.documentWindows = new Map(); // fileId -> Set<windowId>
  }

  // Create window for document
  createWindowForDocument(fileId, windowId, options) {
    if (!this.documentWindows.has(fileId)) {
      this.documentWindows.set(fileId, new Set());
    }

    const winSet = this.documentWindows.get(fileId);
    winSet.add(windowId);

    // Create window
    const win = WindowManager.createWindow(windowId, options);

    // Update mapping on window close
    win.on('closed', () => {
      winSet.delete(windowId);
      if (winSet.size === 0) {
        this.documentWindows.delete(fileId);
      }
    });

    return win;
  }

  // Get all windows associated with a document
  getWindowsForDocument(fileId) {
    const winSet = this.documentWindows.get(fileId);
    return winSet ? Array.from(winSet).map(WindowManager.getWindow) : [];
  }
}

// Usage example
const docWindows = new DocumentWindows();
docWindows.createWindowForDocument('doc-123', 'editor-1', {
  title: 'Document Editor - Document 123'
});

Performance Optimization Strategies

Window Lazy Loading

// Lazy loading window manager
class LazyWindowManager {
  constructor() {
    this.windowPromises = new Map(); // windowId -> Promise<BrowserWindow>
  }

  // Get window (lazy loading)
  async getWindow(windowId, createFn) {
    if (this.windowPromises.has(windowId)) {
      return this.windowPromises.get(windowId);
    }

    const promise = (async () => {
      if (WindowManager.hasWindow(windowId)) {
        return WindowManager.getWindow(windowId);
      }

      const win = await createFn();
      return win;
    })();

    this.windowPromises.set(windowId, promise);
    return promise;
  }

  // Clear cache
  clearCache(windowId) {
    this.windowPromises.delete(windowId);
  }
}

// Usage example
const lazyManager = new LazyWindowManager();

// Create window on first access
async function showSettings() {
  const win = await lazyManager.getWindow('settings', () => {
    return WindowManager.createWindow('settings', {
      /* Configuration */
    });
  });

  win.show();
}

Resource Isolation Strategy

// Resource-isolated window configuration
const isolatedWindow = new BrowserWindow({
  // ...
  webPreferences: {
    sandbox: true, // Enable sandbox
    contextIsolation: true,
    preload: path.join(__dirname, 'isolatedPreload.js'), // Dedicated preload script
    partition: 'persist:isolated' // Isolated session partition
  }
});

// Isolated preload script (isolatedPreload.js)
const { contextBridge } = require('electron');

// Expose only necessary APIs
contextBridge.exposeInMainWorld('isolatedAPI', {
  fetchData: () => ipcRenderer.invoke('isolated-fetch')
});
Share your love