Appearance
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-builderProjektstruktur 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.jsbackground.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-devpackage.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?
| Grund | Erklärung |
|---|---|
| Sicherheit | XSS-Angriffe konnten auf Hauptprozess zugreifen |
| Performance | Overhead durch IPC bei jedem Zugriff |
| Architektur | Verstöß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.htmlsrc/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.
