Skip to content

Kapitel 8: Seiteninteraktion und Frontend-Integration

In diesem Kapitel lernen Sie, wie Sie Frontend-Frameworks (Vue/React) in Electron integrieren und die Interaktion zwischen Renderer und Hauptprozess optimieren.

8.1 Renderer-Prozess und Frontend-Framework-Integration

Vue-Projekt in Electron integrieren (vue-cli-plugin-electron-builder)

Schritt 1: Vue-Projekt erstellen

bash
# Vue CLI installieren (falls nicht vorhanden)
npm install -g @vue/cli

# Neues Vue-Projekt erstellen
vue create meine-vue-electron-app
cd meine-vue-electron-app

# Electron-Plugin hinzufügen
vue add electron-builder

Projektstruktur nach Integration

meine-vue-electron-app/
├── public/
│   └── index.html
├── src/
│   ├── background.js    # Electron Hauptprozess
│   ├── App.vue          # Vue Hauptkomponente
│   ├── main.js         # Vue Eintrittspunkt
│   └── views/
│       ├── Home.vue
│       └── About.vue
├── package.json
└── vue.config.js

background.js (Hauptprozess)

javascript
// src/background.js
import { app, protocol, BrowserWindow } from 'electron';
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
import path from 'path';

const isDevelopment = process.env.NODE_ENV !== 'production';

// Zustandssicherung (macOS)
protocol.registerSchemesAsPrivileged([
  { scheme: 'app', privileges: { secure: true, standard: true } }
]);

async function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      // Sicherheit: Preload verwenden
      preload: path.join(__dirname, 'preload.js'),
      // Node.js Integration (Vorsicht!)
      nodeIntegration: false,
      contextIsolation: true
    }
  });

  if (process.env.WEBPACK_DEV_SERVER_URL) {
    // Im Entwicklungsmodus: Dev-Server-URL laden
    await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL);
    if (!process.env.IS_TEST) win.webContents.openDevTools();
  } else {
    createProtocol('app');
    win.loadURL('app://./index.html');
  }
}

// App-Bereitschaft
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
});

app.on('ready', async () => {
  createWindow();
});

React-Projekt in Electron integrieren

Schritt 1: React-Projekt mit create-react-app erstellen

bash
# React-Projekt erstellen
npx create-react-app meine-react-electron-app
cd meine-react-electron-app

# Electron installieren
npm install electron --save-dev

# Concurrently und wait-on installieren (für Entwicklung)
npm install concurrently wait-on --save-dev

package.json konfigurieren

json
{
  "name": "meine-react-electron-app",
  "version": "0.1.0",
  "main": "public/electron.js",
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "electron": "electron .",
    "electron-dev": "concurrently -k \"npm start\" \"wait-on http://localhost:3000 && npm run electron\""
  }
}

electron.js (Hauptprozess)

javascript
// public/electron.js
const { app, BrowserWindow } = require('electron');
const path = require('path');

function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      nodeIntegration: false,
      contextIsolation: true
    }
  });

  // Im Entwicklungsmodus: React Dev-Server laden
  const startUrl = process.env.ELECTRON_START_URL || 
    `file://${path.join(__dirname, '../build/index.html')}`;
  
  win.loadURL(startUrl);
  
  // DevTools öffnen (nur Entwicklung)
  win.webContents.openDevTools();
}

app.whenReady().then(createWindow);

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
});

8.2 Renderer-Prozess ruft Hauptprozess-APIs auf (remote-Modul, veraltet)

Das Problem mit dem remote-Modul

In älteren Electron-Versionen gab es das remote-Modul, mit dem Renderer-Prozesse direkt auf Hauptprozess-APIs zugreifen konnten.

javascript
// VERALTET! Nicht mehr verwenden!
const { remote } = require('electron');
const mainProcessModule = remote.require('./main-module');

Warum wurde remote entfernt?

GrundErklärung
SicherheitXSS-Angriffe konnten auf Hauptprozess zugreifen
PerformanceOverhead durch IPC bei jedem Zugriff
ArchitekturVerstößt gegen Prozess-Isolation

Aktuelle Lösung: Preload-Skripte + IPC

javascript
// preload.js (sichere Brücke)
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('api', {
  // Hauptprozess-Funktionen über IPC aufrufen
  getAppVersion: () => ipcRenderer.invoke('get-app-version'),
  minimizeWindow: () => ipcRenderer.send('minimize-window'),
  showNotification: (title, body) => 
    ipcRenderer.invoke('show-notification', { title, body })
});
javascript
// main.js (Hauptprozess)
const { ipcMain, app } = require('electron');

ipcMain.handle('get-app-version', () => {
  return app.getVersion();
});

ipcMain.on('minimize-window', (event) => {
  const win = BrowserWindow.fromWebContents(event.sender);
  win.minimize();
});

ipcMain.handle('show-notification', async (event, { title, body }) => {
  const notification = new Notification({ title, body });
  notification.show();
  return { success: true };
});
html
<!-- index.html (Renderer) -->
<script>
  async function getVersion() {
    const version = await window.api.getAppVersion();
    console.log('App-Version:', version);
  }

  function minimize() {
    window.api.minimizeWindow();
  }

  async function notify() {
    await window.api.showNotification('Hallo', 'Nachricht');
  }
</script>

8.3 Datensynchronisation zwischen Frontend-Seite und Hauptprozess (IPC + localStorage)

Szenario: Daten zwischen Renderer und Hauptprozess synchronisieren

Strategie

Renderer (localStorage)  ←→  IPC  ←→  Main (electron-store)

main.js (Hauptprozess mit electron-store)

javascript
const { app, ipcMain } = require('electron');
const Store = require('electron-store');

const store = new Store();

// Einstellungen abrufen
ipcMain.handle('get-settings', () => {
  return store.get('settings') || {};
});

// Einstellungen speichern
ipcMain.handle('save-settings', (event, settings) => {
  store.set('settings', settings);
  return { success: true };
});

// Einstellung abrufen
ipcMain.handle('get-setting', (event, key) => {
  return store.get(`settings.${key}`);
});

// Einstellung speichern
ipcMain.handle('set-setting', (event, key, value) => {
  store.set(`settings.${key}`, value);
  return { success: true };
});

preload.js (IPC-Brücke)

javascript
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('settingsAPI', {
  getSettings: () => ipcRenderer.invoke('get-settings'),
  saveSettings: (settings) => ipcRenderer.invoke('save-settings', settings),
  getSetting: (key) => ipcRenderer.invoke('get-setting', key),
  setSetting: (key, value) => ipcRenderer.invoke('set-setting', key, value)
});

renderer.js (Renderer mit localStorage + IPC Sync)

javascript
// Einstellungen beim Start laden (von Hauptprozess)
async function loadSettings() {
  try {
    const settings = await window.settingsAPI.getSettings();
    
    // In localStorage speichern (für schnellen Zugriff)
    localStorage.setItem('settings', JSON.stringify(settings));
    
    // UI aktualisieren
    applySettings(settings);
  } catch (error) {
    console.error('Fehler beim Laden der Einstellungen:', error);
  }
}

// Einstellungen speichern (in Hauptprozess UND localStorage)
async function saveSettings(newSettings) {
  try {
    // In Hauptprozess speichern (persistent)
    await window.settingsAPI.saveSettings(newSettings);
    
    // In localStorage speichern (schneller Zugriff)
    localStorage.setItem('settings', JSON.stringify(newSettings));
    
    // UI aktualisieren
    applySettings(newSettings);
    
    console.log('Einstellungen gespeichert');
  } catch (error) {
    console.error('Fehler beim Speichern der Einstellungen:', error);
  }
}

// Einstellungen anwenden
function applySettings(settings) {
  // Theme anwenden
  if (settings.theme === 'dark') {
    document.body.classList.add('dark-theme');
  } else {
    document.body.classList.remove('dark-theme');
  }
  
  // Sprache anwenden
  if (settings.language) {
    setLanguage(settings.language);
  }
}

// Beim Laden der Seite
document.addEventListener('DOMContentLoaded', () => {
  loadSettings();
});

8.4 Seiten-Routing-Verwaltung (Single Page Application Routing, angepasst für Desktop)

Vue Router in Electron

javascript
// src/router/index.js (Vue)
import { createRouter, createWebHashHistory } from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';

const routes = [
  { path: '/', name: 'Home', component: Home },
  { path: '/about', name: 'About', component: About }
];

const router = createRouter({
  // In Electron: Hash-Modus verwenden (kein Server erforderlich)
  history: createWebHashHistory(),
  routes
});

export default router;

React Router in Electron

javascript
// src/App.js (React)
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';

function App() {
  return (
    // In Electron: HashRouter verwenden
    <BrowserRouter basename="/">
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Routing-Helfer für Electron

javascript
// preload.js
contextBridge.exposeInMainWorld('navigationAPI', {
  navigate: (path) => {
    window.location.hash = path;
  },
  getCurrentPath: () => {
    return window.location.hash.slice(1) || '/';
  }
});

8.5 Praxisbeispiel: Vue/React-Seiten schreiben und mit Hauptprozess interagieren

Vollständiges Beispiel: Vue + Electron

Projektstruktur

vue-electron-app/
├── src/
│   ├── background.js      # Electron Hauptprozess
│   ├── preload.js         # IPC-Brücke
│   ├── App.vue            # Hauptkomponente
│   ├── main.js            # Vue Eintrittspunkt
│   ├── router/
│   │   └── index.js      # Vue Router
│   └── views/
│       ├── Home.vue
│       └── Settings.vue
├── package.json
└── public/
    └── index.html

src/preload.js

javascript
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  // Fenstersteuerung
  minimize: () => ipcRenderer.send('window-minimize'),
  maximize: () => ipcRenderer.send('window-maximize'),
  close: () => ipcRenderer.send('window-close'),
  
  // Einstellungen
  getSettings: () => ipcRenderer.invoke('get-settings'),
  saveSettings: (settings) => ipcRenderer.invoke('save-settings', settings),
  
  // Benachrichtigungen
  showNotification: (title, body) => 
    ipcRenderer.invoke('show-notification', { title, body })
});

src/background.js (Teilauszug)

javascript
import { app, BrowserWindow, ipcMain } from 'electron';
import path from 'path';

let win;

function createWindow() {
  win = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false
    }
  });

  if (process.env.WEBPACK_DEV_SERVER_URL) {
    win.loadURL(process.env.WEBPACK_DEV_SERVER_URL);
  } else {
    win.loadFile('dist/index.html');
  }
}

// Fenstersteuerung
ipcMain.on('window-minimize', () => win.minimize());
ipcMain.on('window-maximize', () => {
  if (win.isMaximized()) win.restore();
  else win.maximize();
});
ipcMain.on('window-close', () => win.close());

// Einstellungen (vereinfacht)
ipcMain.handle('get-settings', () => {
  return { theme: 'light', language: 'de' };
});

ipcMain.handle('save-settings', (event, settings) => {
  console.log('Einstellungen gespeichert:', settings);
  return { success: true };
});

// Benachrichtigungen
ipcMain.handle('show-notification', (event, { title, body }) => {
  const notification = new Notification({ title, body });
  notification.show();
});

app.whenReady().then(createWindow);

src/views/Home.vue

vue
<template>
  <div class="home">
    <h1>Willkommen in meiner Electron-App!</h1>
    
    <div class="toolbar">
      <button @click="minimize">Minimieren</button>
      <button @click="maximize">Maximieren</button>
      <button @click="close">Schließen</button>
    </div>

    <div class="actions">
      <button @click="showNotification">
        Benachrichtigung senden
      </button>
      <button @click="$router.push('/settings')">
        Einstellungen
      </button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Home',
  methods: {
    minimize() {
      window.electronAPI.minimize();
    },
    maximize() {
      window.electronAPI.maximize();
    },
    close() {
      window.electronAPI.close();
    },
    async showNotification() {
      await window.electronAPI.showNotification(
        'Hallo von Vue!',
        'Dies ist eine Benachrichtigung.'
      );
    }
  }
};
</script>

<style scoped>
.home { padding: 20px; }
.toolbar { margin: 20px 0; }
.toolbar button { margin-right: 10px; padding: 8px 16px; }
.actions { margin-top: 20px; }
</style>

src/views/Settings.vue

vue
<template>
  <div class="settings">
    <h1>Einstellungen</h1>
    
    <div class="setting-item">
      <label>Theme:</label>
      <select v-model="settings.theme">
        <option value="light">Hell</option>
        <option value="dark">Dunkel</option>
      </select>
    </div>

    <div class="setting-item">
      <label>Sprache:</label>
      <select v-model="settings.language">
        <option value="de">Deutsch</option>
        <option value="en">English</option>
      </select>
    </div>

    <button @click="save">Speichern</button>
    <button @click="$router.push('/')">Zurück</button>
  </div>
</template>

<script>
export default {
  name: 'Settings',
  data() {
    return {
      settings: {
        theme: 'light',
        language: 'de'
      }
    };
  },
  async mounted() {
    // Einstellungen laden
    const saved = await window.electronAPI.getSettings();
    this.settings = saved;
  },
  methods: {
    async save() {
      await window.electronAPI.saveSettings(this.settings);
      alert('Einstellungen gespeichert!');
    }
  }
};
</script>

<style scoped>
.settings { padding: 20px; }
.setting-item { margin: 15px 0; }
.setting-item label { display: inline-block; width: 100px; }
button { margin: 10px 5px; padding: 8px 16px; }
</style>

Zusammenfassung

In diesem Kapitel haben Sie gelernt:

  • Vue- und React-Projekte in Electron zu integrieren
  • Das veraltete remote-Modul und dessen moderne Alternativen zu verstehen
  • Daten zwischen Renderer und Hauptprozess mit IPC + localStorage zu synchronisieren
  • SPA-Routing in Electron-Anwendungen zu implementieren
  • Ein praktisches Vue+Electron-Projekt zu erstellen

Im nächsten Kapitel werden wir Netzwerkanfragen und lokalen Speicher behandeln.

Frei für alle Anfänger