Appearance
Kapitel 12: Fortgeschrittene Praxis
In diesem Kapitel bearbeiten wir zwei umfassende Projekte, die sich an der Unternehmensentwicklung orientieren.
Praxis 4: Einfacher Browser
12.1 Anforderungsanalyse
Ein einfacher Browser mit grundlegenden Funktionen.
Benutzeranforderungen
✓ Adressleiste für URL-Eingabe
✓ Seite laden
✓ Vorwärts-/Rückwärts-Navigation
✓ Aktualisieren
✓ Fenstersteuerung
✓ Lade-Status anzeigen
✓ FehlerbehandlungTechnische Architektur
Hauptprozess (main.js):
- BrowserWindow erstellen
- Navigationsevents überwachen
- Fenstersteuerung
Renderer-Prozess (index.html/renderer.js):
- Adressleiste
- Navigationbuttons
- Webview für Seitenanzeige
- Lade-StatusProjektstruktur
simple-browser/
├── main.js # Hauptprozess
├── preload.js # IPC-Brücke
├── package.json
├── index.html # UI
└── styles.css # Styling12.2 Kernimplementierung
main.js (Hauptprozess)
javascript
const { app, BrowserWindow, ipcMain, shell } = require('electron');
const path = require('path');
let mainWin;
function createWindow() {
mainWin = new BrowserWindow({
width: 1400,
height: 900,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
webviewTag: true // Webview aktivieren
}
});
mainWin.loadFile('index.html');
// Externe Links im Standardbrowser öffnen
mainWin.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});
}
// Navigationsevents an Renderer senden
function setupNavigationEvents() {
const contents = mainWin.webContents;
contents.on('did-start-loading', () => {
mainWin.webContents.send('navigation-event', { type: 'start-loading' });
});
contents.on('did-stop-loading', () => {
mainWin.webContents.send('navigation-event', { type: 'stop-loading' });
});
contents.on('page-title-updated', (event, title) => {
mainWin.webContents.send('navigation-event', { type: 'title-updated', title });
});
contents.on('page-favicon-updated', (event, favicons) => {
mainWin.webContents.send('navigation-event', { type: 'favicon-updated', favicons });
});
contents.on('new-window', (event, url) => {
event.preventDefault();
shell.openExternal(url);
});
}
// IPC-Handler für Navigation
ipcMain.on('navigate', (event, url) => {
const contents = mainWin.webContents;
// URL validieren
let validatedUrl = url;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
validatedUrl = 'https://' + url;
}
contents.loadURL(validatedUrl).catch(error => {
mainWin.webContents.send('navigation-event', {
type: 'error',
error: error.message
});
});
});
ipcMain.on('go-back', () => {
if (mainWin.webContents.canGoBack()) {
mainWin.webContents.goBack();
}
});
ipcMain.on('go-forward', () => {
if (mainWin.webContents.canGoForward()) {
mainWin.webContents.goForward();
}
});
ipcMain.on('reload', () => {
mainWin.webContents.reload();
});
ipcMain.on('stop', () => {
mainWin.webContents.stop();
});
ipcMain.on('window-minimize', () => {
mainWin.minimize();
});
ipcMain.on('window-maximize', () => {
if (mainWin.isMaximized()) {
mainWin.restore();
} else {
mainWin.maximize();
}
});
ipcMain.on('window-close', () => {
mainWin.close();
});
ipcMain.handle('get-current-url', () => {
return mainWin.webContents.getURL();
});
ipcMain.handle('get-history', () => {
// Vereinfachte History (in echter App würde man eine Datenbank nutzen)
return [];
});
app.whenReady().then(() => {
createWindow();
setupNavigationEvents();
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
setupNavigationEvents();
}
});preload.js (IPC-Brücke)
javascript
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('browserAPI', {
// Navigation
navigate: (url) => ipcRenderer.send('navigate', url),
goBack: () => ipcRenderer.send('go-back'),
goForward: () => ipcRenderer.send('go-forward'),
reload: () => ipcRenderer.send('reload'),
stop: () => ipcRenderer.send('stop'),
// Fenstersteuerung
minimize: () => ipcRenderer.send('window-minimize'),
maximize: () => ipcRenderer.send('window-maximize'),
close: () => ipcRenderer.send('window-close'),
// Abfragen
getCurrentUrl: () => ipcRenderer.invoke('get-current-url'),
getHistory: () => ipcRenderer.invoke('get-history'),
// Events empfangen
onNavigationEvent: (callback) => {
ipcRenderer.on('navigation-event', (event, data) => callback(data));
}
});index.html (UI)
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Einfacher Browser</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- Toolbar -->
<div class="toolbar">
<div class="window-controls">
<button class="control close" onclick="window.browserAPI.close()"></button>
<button class="control minimize" onclick="window.browserAPI.minimize()"></button>
<button class="control maximize" onclick="window.browserAPI.maximize()"></button>
</div>
<div class="navigation">
<button onclick="goBack()" title="Zurück">←</button>
<button onclick="goForward()" title="Vorwärts">→</button>
<button onclick="reload()" title="Aktualisieren">↻</button>
</div>
<div class="url-bar">
<input type="text" id="urlInput" placeholder="URL eingeben..." onkeydown="handleUrlKeydown(event)">
<button onclick="navigate()" title="Los">Los</button>
<button id="stopButton" onclick="stop()" title="Stop" style="display: none;">✕</button>
</div>
</div>
<!-- Lade-Status -->
<div class="status-bar" id="statusBar">
<span id="statusText">Bereit</span>
<div class="progress-bar" id="progressBar" style="display: none;"></div>
</div>
<!-- Webview für Seitenanzeige -->
<webview
id="webview"
src="https://www.google.com"
autosize="on"
style="width: 100%; height: calc(100vh - 80px);"
></webview>
<!-- Fehleranzeige -->
<div class="error-overlay" id="errorOverlay" style="display: none;">
<div class="error-content">
<h2>Fehler beim Laden der Seite</h2>
<p id="errorMessage"></p>
<button onclick="retry()">Erneut versuchen</button>
</div>
</div>
<script src="renderer.js"></script>
</body>
</html>renderer.js
javascript
// renderer.js
// DOM-Elemente
const urlInput = document.getElementById('urlInput');
const statusText = document.getElementById('statusText');
const statusBar = document.getElementById('statusBar');
const progressBar = document.getElementById('progressBar');
const errorOverlay = document.getElementById('errorOverlay');
const errorMessage = document.getElementById('errorMessage');
const stopButton = document.getElementById('stopButton');
const webview = document.getElementById('webview');
// Navigation
function navigate() {
let url = urlInput.value.trim();
if (!url) return;
// Automatisch https:// hinzufügen
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
urlInput.value = url;
}
window.browserAPI.navigate(url);
hideError();
}
function goBack() {
window.browserAPI.goBack();
}
function goForward() {
window.browserAPI.goForward();
}
function reload() {
window.browserAPI.reload();
hideError();
}
function stop() {
window.browserAPI.stop();
}
function retry() {
reload();
}
// URL-Eingabe (Enter-Taste)
function handleUrlKeydown(event) {
if (event.key === 'Enter') {
navigate();
}
}
// Navigationsevents empfangen
window.browserAPI.onNavigationEvent((data) => {
switch (data.type) {
case 'start-loading':
statusText.textContent = 'Lädt...';
progressBar.style.display = 'block';
stopButton.style.display = 'inline-block';
hideError();
break;
case 'stop-loading':
statusText.textContent = 'Fertig';
progressBar.style.display = 'none';
stopButton.style.display = 'none';
updateUrl();
break;
case 'title-updated':
document.title = data.title + ' - Einfacher Browser';
break;
case 'error':
showError(data.error);
statusText.textContent = 'Fehler';
progressBar.style.display = 'none';
stopButton.style.display = 'none';
break;
}
});
// URL aktualisieren
async function updateUrl() {
try {
const url = await window.browserAPI.getCurrentUrl();
urlInput.value = url;
} catch (error) {
console.error('Fehler beim Abrufen der URL:', error);
}
}
// Fehler anzeigen
function showError(message) {
errorMessage.textContent = message;
errorOverlay.style.display = 'flex';
}
// Fehler verstecken
function hideError() {
errorOverlay.style.display = 'none';
}
// Webview-Events
webview.addEventListener('did-start-loading', () => {
statusText.textContent = 'Lädt...';
progressBar.style.display = 'block';
});
webview.addEventListener('did-stop-loading', () => {
statusText.textContent = 'Fertig';
progressBar.style.display = 'none';
updateUrl();
});
webview.addEventListener('page-title-updated', (event) => {
document.title = event.title + ' - Einfacher Browser';
});
webview.addEventListener('did-fail-load', (event) => {
if (event.errorCode !== -3) { // -3 = Abbruch
showError(event.errorDescription || 'Unbekannter Fehler');
statusText.textContent = 'Fehler';
progressBar.style.display = 'none';
}
});
// Initial URL setzen
urlInput.value = 'https://www.google.com';
// Tastaturkurzel
document.addEventListener('keydown', (event) => {
if (event.ctrlKey || event.metaKey) {
switch (event.key) {
case 'l':
event.preventDefault();
urlInput.select();
break;
case 'r':
event.preventDefault();
reload();
break;
}
}
});styles.css
css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background: #f0f0f0;
overflow: hidden;
}
/* Toolbar */
.toolbar {
display: flex;
align-items: center;
padding: 8px;
background: #f5f5f5;
border-bottom: 1px solid #ccc;
gap: 8px;
height: 48px;
}
/* Fenstersteuerung (macOS-Style) */
.window-controls {
display: flex;
gap: 6px;
}
.control {
width: 12px;
height: 12px;
border-radius: 50%;
border: none;
cursor: pointer;
padding: 0;
}
.control.close {
background: #ff5f57;
}
.control.minimize {
background: #ffbd2e;
}
.control.maximize {
background: #28ca42;
}
/* Navigation */
.navigation {
display: flex;
gap: 4px;
}
.navigation button {
width: 32px;
height: 32px;
border: none;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.navigation button:hover {
background: #e0e0e0;
}
/* URL-Leiste */
.url-bar {
flex: 1;
display: flex;
gap: 4px;
}
#urlInput {
flex: 1;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 16px;
font-size: 14px;
outline: none;
}
#urlInput:focus {
border-color: #4A90D9;
}
#urlInput {
flex: 1;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 16px;
font-size: 14px;
outline: none;
}
.url-bar button {
padding: 6px 16px;
border: none;
background: #4A90D9;
color: white;
border-radius: 4px;
cursor: pointer;
}
.url-bar button:hover {
background: #357ABD;
}
/* Statusleiste */
.status-bar {
padding: 4px 12px;
background: #e8e8e8;
font-size: 12px;
color: #666;
display: flex;
align-items: center;
gap: 8px;
height: 24px;
}
.progress-bar {
flex: 1;
height: 2px;
background: #e0e0e0;
border-radius: 1px;
overflow: hidden;
}
.progress-bar::after {
content: '';
display: block;
width: 40%;
height: 100%;
background: #4A90D9;
animation: progress 1s infinite linear;
}
@keyframes progress {
0% { transform: translateX(-100%); }
100% { transform: translateX(350%); }
}
/* Fehleroverlay */
.error-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.error-content {
background: white;
padding: 32px;
border-radius: 8px;
text-align: center;
max-width: 400px;
}
.error-content h2 {
color: #d32f2f;
margin-bottom: 16px;
}
.error-content button {
margin-top: 16px;
padding: 8px 24px;
background: #4A90D9;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.error-content button:hover {
background: #357ABD;
}12.3 Praxis: Browser-Kernfunktionen implementieren
Vollständiges Beispiel siehe oben.
Praxis 5: Electron + Vue Desktop-Anwendung
12.4 Anforderungsanalyse
Eine Vue-basierte Desktop-Anwendung mit Interaktion zwischen Frontend und Hauptprozess.
Benutzeranforderungen
✓ Vue-Projekt in Electron integrieren
✓ Datendarstellung im Frontend
✓ Benutzerinteraktion
✓ Lokaler Speicher (electron-store)
✓ IPC-Kommunikation12.5 Kernimplementierung
Projektsetup
bash
# Vue-Projekt erstellen
vue create vue-electron-app
cd vue-electron-app
# Electron-Plugin hinzufügen
vue add electron-buildersrc/background.js (Hauptprozess)
javascript
import { app, protocol, BrowserWindow } from 'electron';
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
import path from 'path';
import Store from 'electron-store';
const store = new Store();
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } }
]);
async function createWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true
}
});
if (process.env.WEBPACK_DEV_SERVER_URL) {
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');
}
}
// IPC-Handler
const { ipcMain } = require('electron');
// Daten abrufen
ipcMain.handle('get-data', async (event) => {
const data = store.get('appData') || {
users: [
{ id: 1, name: 'Max Mustermann', email: 'max@example.com' },
{ id: 2, name: 'Anna Schmidt', email: 'anna@example.com' }
],
settings: {
theme: 'light',
language: 'de'
}
};
return data;
});
// Daten speichern
ipcMain.handle('save-data', async (event, data) => {
store.set('appData', data);
return { success: true };
});
// Einstellungen abrufen
ipcMain.handle('get-settings', (event) => {
return store.get('settings') || { theme: 'light', language: 'de' };
});
// Einstellungen speichern
ipcMain.handle('save-settings', (event, settings) => {
store.set('settings', settings);
// Theme anwenden
const win = BrowserWindow.getFocusedWindow();
if (win) {
win.webContents.send('settings-changed', settings);
}
return { success: true };
});
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();
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});public/preload.js (IPC-Brücke)
javascript
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
// Datenverwaltung
getData: () => ipcRenderer.invoke('get-data'),
saveData: (data) => ipcRenderer.invoke('save-data', data),
// Einstellungen
getSettings: () => ipcRenderer.invoke('get-settings'),
saveSettings: (settings) => ipcRenderer.invoke('save-settings', settings),
// Events
onSettingsChanged: (callback) => {
ipcRenderer.on('settings-changed', (event, settings) => callback(settings));
}
});src/App.vue
vue
<template>
<div id="app" :class="`theme-${settings.theme}`">
<header>
<h1>Vue + Electron App</h1>
<nav>
<router-link to="/">Startseite</router-link>
<router-link to="/users">Benutzer</router-link>
<router-link to="/settings">Einstellungen</router-link>
</nav>
</header>
<main>
<router-view />
</main>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
settings: {
theme: 'light',
language: 'de'
}
};
},
async mounted() {
// Einstellungen laden
this.settings = await window.electronAPI.getSettings();
// Auf Einstellungsänderungen hören
window.electronAPI.onSettingsChanged((newSettings) => {
this.settings = newSettings;
this.applyTheme();
});
this.applyTheme();
},
methods: {
applyTheme() {
if (this.settings.theme === 'dark') {
document.body.classList.add('dark-theme');
} else {
document.body.classList.remove('dark-theme');
}
}
}
};
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
}
body.dark-theme {
background: #333;
color: white;
}
#app {
min-height: 100vh;
}
header {
background: #4A90D9;
color: white;
padding: 16px;
}
header h1 {
margin-bottom: 8px;
}
nav a {
color: white;
text-decoration: none;
margin-right: 16px;
padding: 4px 8px;
border-radius: 4px;
}
nav a:hover {
background: rgba(255, 255, 255, 0.2);
}
main {
padding: 20px;
}
.theme-dark main {
background: #333;
color: white;
}
</style>src/views/Home.vue
vue
<template>
<div class="home">
<h2>Willkommen</h2>
<p>Dies ist eine Vue + Electron Desktop-Anwendung.</p>
<div class="stats">
<div class="stat-card">
<h3>Benutzer</h3>
<p class="stat-number">{{ userCount }}</p>
</div>
<div class="stat-card">
<h3>Theme</h3>
<p class="stat-text">{{ settings.theme }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Home',
data() {
return {
userCount: 0,
settings: {
theme: 'light',
language: 'de'
}
};
},
async mounted() {
const data = await window.electronAPI.getData();
this.userCount = data.users ? data.users.length : 0;
this.settings = await window.electronAPI.getSettings();
}
};
</script>
<style scoped>
.home {
max-width: 800px;
margin: 0 auto;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-top: 24px;
}
.stat-card {
background: #f5f5f5;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.stat-number {
font-size: 32px;
font-weight: bold;
color: #4A90D9;
margin-top: 8px;
}
.stat-text {
font-size: 20px;
margin-top: 8px;
}
.theme-dark .stat-card {
background: #555;
color: white;
}
</style>src/views/Users.vue
vue
<template>
<div class="users">
<h2>Benutzerverwaltung</h2>
<button @click="showAddForm = !showAddForm" class="add-button">
{{ showAddForm ? 'Abbrechen' : 'Benutzer hinzufügen' }}
</button>
<div v-if="showAddForm" class="add-form">
<input v-model="newUser.name" placeholder="Name">
<input v-model="newUser.email" placeholder="E-Mail">
<button @click="addUser">Speichern</button>
</div>
<table class="user-table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>E-Mail</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>
<button @click="deleteUser(user.id)" class="delete-button">Löschen</button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
name: 'Users',
data() {
return {
users: [],
showAddForm: false,
newUser: {
name: '',
email: ''
}
};
},
async mounted() {
await this.loadUsers();
},
methods: {
async loadUsers() {
const data = await window.electronAPI.getData();
this.users = data.users || [];
},
async addUser() {
if (!this.newUser.name || !this.newUser.email) {
alert('Bitte Name und E-Mail eingeben');
return;
}
const data = await window.electronAPI.getData();
const newId = this.users.length > 0
? Math.max(...this.users.map(u => u.id)) + 1
: 1;
const updatedUsers = [...this.users, { ...this.newUser, id: newId }];
await window.electronAPI.saveData({
...data,
users: updatedUsers
});
this.showAddForm = false;
this.newUser = { name: '', email: '' };
await this.loadUsers();
},
async deleteUser(id) {
if (!confirm('Möchten Sie diesen Benutzer wirklich löschen?')) {
return;
}
const data = await window.electronAPI.getData();
const updatedUsers = this.users.filter(u => u.id !== id);
await window.electronAPI.saveData({
...data,
users: updatedUsers
});
await this.loadUsers();
}
}
};
</script>
<style scoped>
.users {
max-width: 800px;
margin: 0 auto;
}
.add-button {
padding: 8px 16px;
background: #4A90D9;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-bottom: 16px;
}
.add-form {
background: #f5f5f5;
padding: 16px;
border-radius: 8px;
margin-bottom: 16px;
}
.add-form input {
display: block;
width: 100%;
padding: 8px;
margin-bottom: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.add-form button {
padding: 8px 16px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.user-table {
width: 100%;
border-collapse: collapse;
margin-top: 16px;
}
.user-table th,
.user-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ccc;
}
.user-table th {
background: #f5f5f5;
}
.delete-button {
padding: 4px 12px;
background: #f44336;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.theme-dark .add-form {
background: #555;
}
.theme-dark .user-table th {
background: #555;
}
</style>src/views/Settings.vue
vue
<template>
<div class="settings">
<h2>Einstellungen</h2>
<div class="setting-item">
<label>Theme:</label>
<select v-model="settings.theme" @change="saveSettings">
<option value="light">Hell</option>
<option value="dark">Dunkel</option>
</select>
</div>
<div class="setting-item">
<label>Sprache:</label>
<select v-model="settings.language" @change="saveSettings">
<option value="de">Deutsch</option>
<option value="en">English</option>
</select>
</div>
<div class="setting-item">
<label>Benachrichtigungen:</label>
<input
type="checkbox"
v-model="settings.notifications"
@change="saveSettings"
>
</div>
</div>
</template>
<script>
export default {
name: 'Settings',
data() {
return {
settings: {
theme: 'light',
language: 'de',
notifications: true
}
};
},
async mounted() {
this.settings = await window.electronAPI.getSettings();
},
methods: {
async saveSettings() {
await window.electronAPI.saveSettings(this.settings);
alert('Einstellungen gespeichert!');
}
}
};
</script>
<style scoped>
.settings {
max-width: 600px;
margin: 0 auto;
}
.setting-item {
margin: 16px 0;
display: flex;
align-items: center;
gap: 16px;
}
.setting-item label {
min-width: 150px;
font-weight: bold;
}
.setting-item select,
.setting-item input {
padding: 8px;
font-size: 14px;
}
</style>12.6 Praxis: Vue+Electron-Projekt aufsetzen und interagieren
Vollständiges Beispiel siehe oben.
Zusammenfassung
In diesem Kapitel haben Sie gelernt:
- Einen einfachen Browser mit Navigation zu implementieren
- Webview-Events zu überwachen und zu verarbeiten
- Eine Vue+Electron-Anwendung aufzusetzen
- IPC-Kommunikation zwischen Vue und Hauptprozess zu realisieren
- Lokale Datenspeicherung mit
electron-storezu implementieren
Im nächsten Kapitel werden wir das Packaging und die Veröffentlichung behandeln.
