App Entwicklung für Webentwickler - von der responsiven Web App zur nativen App

24.11.2021
Seit der Veröffentlichung des iPhones wurde intensiv versucht die App-Entwicklung für Webentwickler auf möglichst einfache Weise zugänglich zu machen. Die einfachste aller Möglichkeiten, um aus einer responsiven Web App eine echte native App zu machen, ist jedoch, die Web App einfach zu "wrappern", also in eine native WebView zu packen. Diese Möglichkeit wollen wir nachfolgend detailliert beschreiben.

Das Problem der Frameworks für viele Webentwickler

Von auf HTML5/JS/CSS basierenden Frameworks wie CORDOVA oder IONIC bis hin zu den neueren Cross-Plattform-Frameworks wie REACT NATIVE, FLUTTER oder NATIVESCRIPT. In der Tat sind diese Frameworks sehr erfolgreich und ermöglichen die plattformübergreifende Entwicklung von Apps auf einer einheitlichen Code-Basis.

In einer Hinsicht sind jedoch alle Frameworks bislang gescheitert. Sie erfordern eine meist mehr als weniger intensive Einarbeitung des Webentwicklers. Bei bestehenden Webprojekten ist eine umfassende Neuprogrammierung oft unumgänglich. Das ist gerade für vielbeschäftigte Webentwickler eine große Hürde im beruflichen Alltag. Dies gilt auch für auf HTML5/JS/CSS basierenden Frameworks, da diese ausschließlich auf Single-Page-Applikationen setzen. Somit sind insbesondere klassische PHP-basierte Webprojekte nicht kompatibel.

Dabei bieten native Apps einige klare Vorteile gegenüber von Webseiten. Sei es die Nutzung nativer Gerätefunktionen, die prominente Sichtbarkeit beim Nutzer auf dessen Smartphone-Desktop und die Verfügbarkeit über den Apple AppStore oder den Google PlayStore.

Das Wrappern von Webprojekten als Lösungsweg

Die Anfrage von Kunden zur Weiterentwicklung des bestehenden Webprojekts zu einer eigenständigen App für Android & iOS stellt somit oft ein Dilemma dar. Entweder man nimmt es selbst in die Hand und muss sich erstmal intensiv weiterbilden. Oder man gibt den Auftrag weiter und der Kunde muss finanziell quasi eine Neuentwicklung des bestehenden Projekts finanzieren. Doch es gibt einen dritten Lösungsweg, der es ermöglicht, die App Entwicklung zu 95% selbst und ohne große Weiterbildung umzusetzen: Das Wrappern von Webprojekten.

Kurz gesagt entwickelt man dabei das Webprojekt zu einer responsiven Webseite weiter, die im Look&Feel einer App entspricht. Diese Seite wird anschließend, wie in der folgenden Abbildung dargestellt, in einem nativen Wrapper für die jeweilige Plattform als App veröffentlicht. Für den Nutzer ist es eine App - technisch gesehen ist es einfach eine Webseite.

Der native Wrapper erlaubt es, das Projekt als eigenständige App nutzbar zu machen und gibt zugleich Zugriff auf die nativen Gerätefunktionen. Der Wrapper instanziert eine Webview, die das gesamte Display füllt und lädt innerhalb dieser Webview die URL. Die Performance einer Webview kann mit einer nativ entwickelten App oder auch auf Cross-Plattform-Frameworks basierenden App nicht mithalten. Aufgrund der fortschreitenden Leistungsfähigkeit der Mobilgeräte ist der Unterschied für den Nutzer immer weniger spürbar.

In diesem Artikel bieten wir dir ein Codebeispiel, wie du plattformunabhängig und einfach eine Kommunikation zwischen den nativen Gerätefunktionen und deiner WebApp herstellen kannst.

App Entwicklung - was man dafür benötigt

Ganz ohne etwas Mehraufwand ist der Weg zur App allerdings nicht zu machen:

  1. In Design und Funktionalität man sich mit den Anforderungen von Google & Apple an die Gestaltung von Apps auseinandersetzen. Es hat sich hier eine spezifische UI und UX entwickelt, die es zu berücksichtigen gilt. Insbesondere Apple legt bei der Veröffentlichung von Apps wert darauf, dass die App "mehr" sein muss, als eine Webseite.
  2. Man benötigt einen Entwicklungspartner, der den nativen Wrapper für Android & iOS entwickelt und den Veröffentlichungsprozess übernimmt. Das sind die 5% externe Entwicklungsarbeit, die man mit einkalkulieren muss.
  3. Weiterhin benötigt man für die Veröffentlichung einer App Konten in den entsprechenden App Stores bei Google und Apple und muss sich in die jeweiligen Backends einarbeiten.
  4. Das Debugging ist im nativen Kontext etwas komplizierter und erfordert oft die Nutzung von appspezifischen Analysetools.

Die JavaScript-Bridge

Nun sind wir also soweit: Wir haben dem Webprojekt ein appspezfisches UI verpasst und Ihr Entwicklungspartner hat für Sie native Android- und iOS-Wrapper entwickelt, in denen das Webprojekt als App geladen wird. Nun wollen wir die Vorteile des Wrapper nutzen und auf native Funktionen zugreifen. Hierfür bieten sowohl Android als auch iOS eine sogenannten Javascript-Bridge. Diese ermöglicht, wie in Abbildung 2 dargestellt, die Kommunikation zwischen dem in der Webview geladenen Javascript-Code und dem Wrapper. Innerhalb der Webview wird die Javascript-Bridge als globales Objekt injiziert und ist somit ansprechbar. Umgekehrt kann der Wrapper den Javascript-Code innerhalb der Webview ansprechen. Da die Kommunikation mit der jeweiligen Plattform unterschiedlich ist, implementieren wir ein plattformspezifisches Bridge-Objekt, welches die Kommunikation mit der jeweiligen Plattform übernimmt. Bevor wir uns dieser Bridge widmen, wollen wir uns erstmal an einem einfachen Beispiel anschauen, wie die Kommunikation mit der nativen Javascript-Bridge funktioniert.

Dabei richten wir den Blick nicht auf den Wrapper (dessen Programmierung muss dich an dieser Stelle nicht kümmern), sondern auf den Javascript-Code.

Ein praktisches Beispiel für iOS & Android

Du möchtest den Nutzernamen dauerhaft auf dem Gerät speichern und ihn jederzeit auslesen und verändern können. Dein Entwicklungspartner stellt dir eine Javascriptbridge unter dem Kürzel JSB zur Verfügung mit den Funktionen saveUserName() und getUserName(), damit du den Nutzernamen speichern und auslesen kannst.

Wenn du den Nutzernamen speichern willst, kannst du die Funtion saveUserName() auf Android direkt ansprechen. Der Abruf des Nutzernamens erfolgt über die Funktion getUserName() analog zum Speichern. Da die Webview im gleichen Thread wie der native Code läuft, ist eine synchrone Kommunikation möglich:

//### Kommunikation mit ANDROID		

//# Speichern des Nutzernamens
const userName = "Max Mustermann";
window.JSB.saveUserName(userName);


//# Auslesen des Nutzernamens
const userName = window.JSB.getUserName();

Auf iOS ist die direkte Ansprache der Funktionen der Javascriptbridge nicht möglich. Hier wird ein Objekt übergeben, das den Funktionsnamen sowie die erforderlichen Daten enthält. Auf iOS läuft die Webview in einem separaten Thread. Somit ist die Kommunikation zwischen Webview und Wrapper asynchron und es muss mit festen Callback-Funktionen gearbeitet werden. Daher stellen wir zum Abruf des Nutzernamens die Callback-Funktion onGetUserName(data) bereit, welche vom Wrapper aufgerufen wird:

//### Kommunikation mit iOS

//# Speichern des Nutzernamens
const jsbObject = {
	id: "saveUserName",
	userName: "Max Mustermann"
}
window.webkit.messageHandlers.JSB.postMessage(jsbObject)


//# Auslesen des Nutzernamens
		
// Diese Callback-Funktion empfängt den Nutzernamen vom Wrapper
function onGetUserName(result) {
	const userName = result;
}

const jsbObject = {
	id: "getUserName",
}
// Aufforderung an den Wrapper, den Nutzernamen auszulesen
window.webkit.messageHandlers.JSB.postMessage(jsbObject)

Mit diesem einfachen Code können wir nun sowohl auf Android wie auch auf iOS den Nutzernamen speichern. Allerdings ist diese Vorgehensweise aus zwei Gründen doch eher unpraktisch:

  1. Wir müssten vor jedem Funktionsaufruf prüfen, auf welcher Plattform der Code gerade ausgeführt wird.
  2. Wir können den Code bei der Entwicklung in einer Browersumgebung nicht testen. Daher wollen wir den Code im nächsten Schritt um eine Browersumgebung erweitern und vereinheitlichen.

Ein plattformübergreifender Code

Hierfür nutzen wir die mit ES6 eingeführte Klasse. Während in der Webentwicklung viel Aufwand um die Browserkompatibilität betrieben werden muss, basieren die nativen Webviews auf mobile Chrome und mobile Safari. Beide Browser unterstützen die neuesten ES6-Features.

Die WebBridge

Als Ausgangspunkt schaffen wir eine Klasse für die Browersumgebung und implementieren dort die Funktionen saveUserName() und getUserName(). In der Browersumgebung speichern wir den Nutzernamen im LocalStorage. Anschließend instanzieren wir die neue Klasse und nutzen die Funktionen:

//*** Bridge-Klasse für die Browersumgebung
class WebBridge {
			
	// Speichern des Nutzernamens
	saveUserName(userName) {
		localStorage.setItem("userName", userName);
	}
			
	// Auslesen des Nutzernamens
	getUserName() {
		const userName = localStorage.getItem("userName");
		return userName;
	}
			
}

/* Test */

// Instanzieren der WebBridge-Klasse
const bridge = new WebBridge();

// Speichern des Nutzernamens
const userName = "Max Mustermann";
bridge.saveUserName(userName);

// Auslesen des Nutzernamens
const userName = bridge.getUserName();

Die Webbridge als Parent-Klasse für Android und iOS

Somit haben wir die Möglichkeit geschaffen, den Code in einer Browserumgebung auszuführen. Bevor wir diesen nun für Android und iOS adaptieren, wollen wir unsere Aufmerksamkeit noch der Vereinheitlichung des Codes widmen. Wie oben bereits beschrieben, ist die Kommunikation zwischen dem Wrapper und der Webview auf Android sowie in der Browersumgebung synchron und auf iOS asynchron. Es empfiehlt sich daher für die Vereinheitlichung des Codes die Bridge-Klasse grundsätzlich auf asynchrone Kommunikation auszulegen. Hierfür bedienen wir uns eines Promise. Dabei dient uns die WebBridge als Parent-Klasse, die wir anschließend für die anderen Bridge-Klassen beerben. Im Falle dieses Beispiels wäre dies nicht zwingend notwendig. Bei nativen Funktionen, welche auch auf Android asynchron sind, bietet es sich jedoch an, diese bereits in der Parent-Klasse zu implementieren und anschließend zu vererben.

/* Bridge-Klasse für die Browersumgebung */

class WebBridge {

  // Speichern des Nutzernamens
  saveUserName(userName) {
    localStorage.setItem("userName", userName);
  }

  // Auslesen des Nutzernamens
  getUserName() {
    // Die Funktion gibt ein Promise zurück, das in der Zukunft erfüllt wird
    return new Promise((resolve) => {
      const userName = localStorage.getItem("userName");
      resolve(userName);
    });
  }
}


/* Test */

// Instanzieren der WebBridge-Klasse
const bridge = new WebBridge();

// Speichern des Nutzernamens
const userName = "Max Mustermann";
bridge.saveUserName(userName);

// Auslesen des Nutzernamens
// Wir fordern den Nutzernamen an, übernehmen das Promise und erhalten den Nutzernamen,
// wenn das Promise aufgelöst ist
bridge.getUserName().then((result) => {
	const userName = result;
	console.log(userName);
}); 

Die AndroidBridge

Nun haben wir also eine Web-Bridge, die das Speichern und das asynchrone Auslesen des Nutzernamens ermöglicht. Im nächsten Schritt erstellen wir eine Android-Bridge, welche die Webbridge beerbt und somit ihre Funktionen nutzen oder überschreiben kann.

/* Bridge-Klasse für die Android-Webview */

class AndroidBridge extends WebBridge {

	// Wir überschreiben die saveUserName-Funktion
	saveUserName(userName) {
		window.JSB.saveUserName(userName);
	}

	// Wir überschreiben die getUserName-Funktion, wobei sich der Code lediglich
	// im Auslesen des Nutzernamens unterscheidet.
	getUserName() {
		// Die Funktion gibt ein Promise zurück, das in der Zukunft erfüllt wird
    	return new Promise((resolve) => {
      		const userName = window.JSB.getUsername();
      		resolve(userName);
    	});
	}
}

/* Test */

// Instanzieren der AndroidBridge-Klasse
const bridge = new AndroidBridge();

// Speichern des Nutzernamens
const userName = "Max Mustermann";
bridge.saveUserName(userName);

// Auslesen des Nutzernamens
// Wir fordern den Nutzernamen an, übernehmen das Promise und erhalten den Nutzernamen,
// wenn das Promise aufgelöst ist
bridge.getUserName().then((result) => {
	const userName = result;
	console.log(userName);
}); 

Die iOSBridge

Die iOS-Bridge unterscheidet sich von der Web- und AndroidBridge, da hier aufgrund der Asynchronität die zusätzliche Callback-Funktion onGetUserName(userName) implementiert werden muss. Um dennoch ein Promise zurückgeben zu können, nutzen wir ein CustomEvent und ein EventTarget. Auf die Funktion des CustomEvent wird hier nicht näher eingegangen, da diese im Netz bereits vielfältig und bestens erläutert wird.

/* Bridge-Klasse für die iOS-Webview */

class IOSBridge extends WebBridge {

	constructor() {
		// bei Vererbung muss der Konstruktor der Parent-Klasse aufgerufen werden
		super();

		// Wir instanzieren ein Klassenweit verfügbares EventTarget
		this.userNameEvent = new EventTarget();
  	}

	// Wir überschreiben die saveUserName-Funktion
	saveUserName(userName) {
		const jsbObject = {
			id: "saveUserName",
			userName: userName
		}
		window.webkit.messageHandlers.JSB.postMessage(jsbObject)
	}

	// Wir überschreiben die getUserName-Funktion und setzen einen Event-Listener
	// um den Callback vom iOS-Wrapper innerhalb des Promise zurückzugeben
	getUserName() {
		// Die Funktion gibt ein Promise zurück, das in der Zukunft erfüllt wird
		return new Promise((resolve) => {
			// Wir setzen einen EventListener. Sobald dieses Event ausgelöst wird
			// erhalten wird den Nutzernamen und lösen das Promise damit auf
			this.userNameEvent.addEventListener("userName", (result) => {
				// wir lesen den Nutzernamen aus dem result-Objekt aus
				const userName = result.detail.userName;
				// wir geben das Promise zurück
				resolve(userName);
			});
			
			const jsbObject = {
				id: "getUserName"
			};
			window.webkit.messageHandlers.JSB.postMessage(jsbObject)
		});
	}

	// Als Callback-Funktion für den iOS-Wrapper implementieren wir eine Funktion
	// welche den Nutzernamen vom iOS-Wrapper erhällt und dann mit dem Event "userName" übergibt

  	onGetUserName(userName) {
		// Sobald diese Funktion aufgerufen wird, wird das Event "userName" ausgelöst und der userName übergeben
		this.userNameEvent.dispatchEvent(
			new CustomEvent("userName", { detail: { userName: userName } })
		);
	}
}


/* Test */

// Instanzieren der IOSBridge-Klasse
const bridge = new IOSBridge();

// Speichern des Nutzernamens
const userName = "Max Mustermann";
bridge.saveUserName(userName);

// Auslesen des Nutzernamens
// Wir fordern den Nutzernamen an, übernehmen das Promise und erhalten den Nutzernamen,
// wenn das Promise aufgelöst ist
bridge.getUserName().then((result) => {
	const userName = result;
	console.log(userName);
}); 

Die passende Bridge automatisch wählen

Wir haben nun sowohl für Android, als auch für iOS und eine Browserumgebung eine passende Bridge-Klasse, um den Nutzernamen zu speichern und auszulesen. Die Anwendung ist unabhängig von der Plattform, wir müssen lediglich die passende Bridge-Klasse instanzieren. Dies können wir automatisieren, in dem wir prüfen, ob und welche globalen Javascript-Bridges von der Plattform zur Verfügung gestellt werden. Im Fall von iOS reicht die Prüfung, ob "webkit" zur Verfügung steht.

/* Test */

let bridge = null;

// Instanzieren der plattformspezifischen Bridge-Klasse
if (typeof window.JSB != "undefined") {
	// JSB wurde gefunden, daher läuft der Code in einer Android-Webview
  	bridge = new AndroidBridge();
} else if (typeof window.webkit != "undefined") {
	// Webkit wurde gefunden, daher läuft der Code in einer iOS-Webview
  	bridget = new IOSBridge();
} else {
	// Keine Javascript-Bridge wurde gefunden, daher muss es sich um eine Browersumgebung handeln
  	bridge = new WebBridge();
}

// Speichern des Nutzernamens
const userName = "Max Mustermann";
bridge.saveUserName(userName);

// Auslesen des Nutzernamens
// Wir fordern den Nutzernamen an, übernehmen das Promise und erhalten den Nutzernamen,
// wenn das Promise aufgelöst ist
bridge.getUserName().then((result) => {
  const userName = result;
  console.log(userName);
});

Hiermit können wir nun den Nutzernamen einheitlich speichern und auslesen, unabhängig davon, auf welcher Plattform unser Javascript-Code läuft und ob diese Plattform synchron oder asynchron kommuniziert.

Fazit

Anhand dieses Beispielcodes kannst du nun in deinem Webprojekt jederzeit den Nutzernamen speichern und auslesen, unabhängig davon, ob das Projekt in einem Browser oder innerhalb einer Webview auf Android oder iOS läuft. Das Speichern eines Nutzernamens ist sicherlich kein ausreichender Grund, um eine App zu entwickeln. Aber das Beispiel verdeutlich, wie man ein Webprojekt um native Gerätefunktionen erweitern kann. Zum Beispiel in Form von Push-Benachrichtigungen: Durch die Implementierung von FirebaseMessaging im Wrapper kannst du das Messaging-Token auslesen und Serverseitig Push-Benachrichtigungen an deine Nutzer senden. Als weitere Beispiele können auch der Zugriff auf die Kontakte, Standortdaten oder die Kamerafunktion genannt werden.

Die Vorgehensweise, ein bestehendes Webprojekt in einem Wrapper als App zu veröffentlichen, eignet sich besonders für bestehende Single-Page-Applikationenn aufgrund der hohen Responsivität. Aber auch PHP-basierte Webprojekte können hiermit gut umgesetzt werden. Natürlich bleibt es einem auch hier nicht erspart, die Nutzeroberfläche entsprechend anzupassen und sich in das Einmaleins der App-Veröffentlichung einzuarbeiten. Zudem benötigt man einen Entwicklungspartner für den nativen Wrapper. Dieser kann jedoch unabhängig von dem Webprojekt entwickelt werden. Somit bleibt die Entwicklung der eigentlichen Web-App vollständig in deiner Hand.

Fazit zum Wrappern von Web Apps

Die Veröffentlichung einer App in Form einer gewrapperten Web-App bietet dem Webentwickler eine relativ einfache Erweiterung des Portfolios und dem Kunden einen kostengünstigen Weg, seine bestehenden Webprodukte um eine App für Android und iOS zu erweitern.

Autor

Fritz Matthäus, App Entwickler

Mevi Media GmbH



Anfrage-Entwickler-zur-Programmierung
6 Entwickler in
3 Tagen finden

Geben Sie Ihre Anfrage zur App Entwicklung ein und erhalten Sie kostenlos Angebote aus 5.000+ Entwicklern