zurück zum Artikel

Cross-Plattform-Entwicklung mit Ionic 4, Stencil und Capacitor, Teil 1

Norbert Frank
Cross-Plattform-Entwicklung mit Ionic 4, Stencil und Capacitor (Teil 1)

Mit Version 4 hat sich Ionic nach Angular auch für die Frameworks React und Vue.js geöffnet. Grund genug, es sich im Detail anzusehen.

Ionic ist ein generisches UI-Framework zur einfachen Entwicklung von Apps mit Webtechniken. Die Ionic-Gründer sind vor einigen Jahren mit der Vision gestartet, Entwickler beim Erstellen mobiler Web-Apps zu unterstützen, die sich für Endanwender wie native Apps der jeweiligen Plattform darstellen. Für die ersten drei Versionen des Frameworks war Ionic noch zu einigen technischen Kompromissen gezwungen und stark an das JavaScript-Framework Angular gebunden. Mit Version 4 hat man sich mit Web Components von dieser Historie gelöst. Was das bedeutet und welche Möglichkeiten Ionic und die Schwesterprojekte Stencil und Capacitor nun bieten, betrachtet dieser Artikel.

Wer heute Ionic einsetzt, wird in Support-Foren und bei der Suche nach Blogbeiträgen und Tutorials im Internet vor die Herausforderung gestellt, jeweils zuzuordnen, auf welche Version sich ein Beitrag bezieht. Das kann für Einsteiger verwirrend sein und lässt sich besser nachvollziehen, wenn man die Versionshistorie von Ionic kennt. Das Team hinter Ionic begann 2012 mit der Arbeit an einem UI-Framework für die Erstellung mobiler Apps. Am Anfang hatten sie noch das Ziel, die UI-Komponenten für alle damals verbreiteten Frontend-Frameworks anzubieten. Neben Angular 1 waren das Backbone und Knockout.js.

Schnell mussten sie allerdings feststellen, dass dies zu drei verschiedenen Versionen des Ionic-Frameworks geführt hätte, die sie separat mit hohem Aufwand hätten entwickeln müssen. Da Angular 1 zu der Zeit massiv an Popularität gewann, war die Entscheidung naheliegend, sich nur darauf zu konzentrieren.

Im November 2013 erschien das erste Alpha-Release, im Mai 2015 schließlich die finale Version 1.0. Auf Basis von Angular 1 konnten Anwender mit Ionic 1 mobile Apps mit Webtechniken erstellen. Auch in der ersten Version brachte Ionic schon einen eigenen Router und Funktionen wie Page-Transitions mit. Die Integration mit Cordova zur Kapselung als native App übernahm das Ionic Command Line Interface (CLI) weitgehend automatisch, was insbesondere für Anfänger ein großer Vorteil war und bis heute ist.

Als sich mit Angular 2 viele Konzepte änderten, war auch das Ionic-Team gezwungen, Ionic 2 auf Basis davon neu zu entwickeln. Ionic 2 erschien Anfang 2017 und setzte zwar auf die neue Version des JavaScript-Frameworks, enthielt aber eigene Build-Tools und einen eigenen Router, da man damals die Angular-eigene Lösung für (noch) nicht geeignet hielt. Schon im März 2017 erschien Ionic 3 auf Basis von Angular 4. Ionic 3 wird weiterhin gewartet und ist noch sehr verbreitet.

Da sich die Webentwicklung seit der ersten Version deutlich weiterentwickelt hatte, konnte das Framework mit Version 4 die ursprüngliche Vision wieder aufgreifen, ein Set von UI-Komponenten unabhängig von konkreten Frontend-Frameworks anbieten zu können. Der Schlüssel dazu sind Web Components. Mit der Anfang 2019 erschienenen Version 4 von Ionic liegen alle UI-Komponenten als eigenständige Web Components vor. Sie haben keine Abhängigkeiten zu weiteren Libraries und Frameworks und sind daher flexibel einsetzbar. Um den Entwicklungskomfort zu erhöhen, existieren fertige Integrationen für Angular, React und Vue.js. Im Rahmen der Entwicklung von Ionic 4 entstanden auch der Web-Components-Compiler Stencil und die Cordova-Alternative Capacitor. Ionic 4 ist dadurch deutlich mächtiger und flexibler als seine Vorgänger.

Da die Ionic-Komponenten nun als Web Components vorliegen, können Entwickler sie wie normale HTML-Elemente einsetzen. Dazu reicht es, die JavaScript- und CSS-Definition der Library einzubinden:

<link href="https://unpkg.com/@ionic/core@latest/css/ionic.bundle.css" rel="stylesheet" />
<script src="https://unpkg.com/@ionic/core@latest/dist/ionic.js"></script>

Anschließend kann man die Ionic-Komponenten wie in folgendem Beispiel verwenden:

<ion-app>
    <ion-header>
        <ion-toolbar mode="ios">
            <ion-title>Ionic 4</ion-title>
        </ion-toolbar>
    </ion-header>
    <ion-content>
        <ion-card button="true" onclick="helloWorld()">
            <img src="https://images.unsplash.com/photo-1560732488-6b0df240254a?ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80" />
            <ion-card-header>
                <ion-card-subtitle>Ionic 4</ion-card-subtitle>
                <ion-card-title>Ionic 4 Web Components</ion-card-title>
            </ion-card-header>
            <ion-card-content>
                Die Ionic Web Components können wie normale HTML-Elemente eingesetzt werden!
            </ion-card-content>
        </ion-card>
    </ion-content>
</ion-app>

Obwohl hier ein einzelnes JavaScript-Bundle eingebunden ist, wird die Definition der einzelnen Komponenten erst asynchron geladen, wenn die jeweilige Komponente eingesetzt wird. Ionic nutzt dabei ein Verfahren, zunächst nur eine leere Proxy-Definition der Web Component zu registrieren. Dadurch bleibt der initiale Download auch für eine dreistellige Anzahl an Komponenten sehr klein. Erst wenn eine Web Component im DOM  Verwendung findet, lädt Ionic deren Funktionen asynchron und stellt sie bereit. Dabei kommen in zeitgemäßen Browsern dynamische Importe von ES-Modulen zum Einsatz [1]. Durch dieses Verfahren wird eine optimale Startzeit der App erzielt. Gleichzeitig kann das Framework nahezu beliebig viele UI-Komponenten enthalten. Für die Entwicklung ist es komfortabel, dass nur ein einziges Bundle einzubinden ist.

Die Web Components können Nutzer über HTML-Attribute konfigurieren. Einige Web Components haben zusätzlich auch eine JavaScript-API. Um sie zu verwenden, benötigt man eine Referenz auf das zugehörige DOM-Element:

const modalController = document.querySelector("ion-modal-controller");
await modalController.componentOnReady();
   
const modalElement = await modalController.create({
    component: "modal-content"
});
   
await modalElement.present();

Das obige Beispiel ermittelt eine ion-modal-controller-Komponente im DOM, deren asynchrone Initiierung componentOnReady abwartet und anschließend die create-Methode aufruft, um einen modalen Dialog zu erzeugen. Die Anzeige des Dialogs erfolgt anschließend mit present.

Um die Ionic-Komponenten einzusetzen, muss man keine Web Components selbst schreiben. Sie können aber sehr hilfreich sein, wenn man auf ein zusätzliches JavaScript-Framework verzichten möchte. So kann man zum Beispiel den Inhalt eines modalen Dialogs als Web Component definieren:

customElements.define(
    "modal-content",
    class extends HTMLElement {
        connectedCallback() {
            this.innerHTML = `
                <ion-header>
                    <ion-toolbar>
                    <ion-title>Ein modaler Dialog</ion-title>
                    </ion-toolbar>
                </ion-header>
                <ion-content padding>
                    Der Inhalt dieses Dialogs ist selbst eine Web Component!
                </ion-content>`;
        }
    }
);

Die hier definierte Web Component modal-content kann dann wie im vorhergehenden Beispiel gezeigt die ion-modal-controller-Komponente in der create-Methode referenzieren, um den Inhalt eines modalen Dialogs festzulegen.

Auch wenn man keine eigenen Web Components entwickelt, ist etwas Verständnis des Shadow DOM in Bezug auf das Styling von Ionic-Komponenten von Vorteil. Wer von früheren Ionic-Versionen gewohnt war, über CSS das innere Aussehen der Ionic-Komponenten beliebig anpassen zu können, wird sich umgewöhnen müssen. Durch den Einsatz von Web Components mit einem Shadow DOM kann man nur noch die von Ionic offiziell vorgesehenen Attribute anpassen. Das mag nach einem Nachteil klingen, erzwingt aber, dass man die Komponenten tatsächlich nur in der vorgesehenen Weise einsetzt. Das ist spätestens beim nächsten Versions-Upgrade von Vorteil und erspart langfristig viel Wartungsaufwand.

Ein einfaches Beispiel stellt die Möglichkeiten dar. Die Ionic-Komponente ion-title zur Anzeige eines Titels in Toolbars wird auf folgende Weise im Quellcode definiert:

<ion-title>Ein modaler Dialog</ion-title>

Das führt zur Laufzeit zu einer Darstellung, die in den Developer-Tools des Browsers ungefähr so aussieht:

<ion-title>
    #shadow-root (open)
    <div class="toolbar-title">
        <slot>
          #text
        </slot>
    </div>
    "Ein modaler Dialog"
</ion-title>

Die Web Component ion-title hat im Shadow DOM ein div-Element mit der CSS-Klasse toolbar-title und darin ist ein Slot für den Titeltext gekapselt.

Das umschließende ion-title ist das sogenannte Host-Element und ist über normales CSS erreichbar. Die folgende CSS-Regel führt daher zu einem roten Rand:

ion-title {
    border: 1px solid red;
}

Das div-Element mit der CSS-Klasse toolbar-title befindet sich hingegen im Shadow DOM. Daher hat folgende CSS-Regel keine Auswirkung:

ion-toolbar .toolbar-title {
    color: red;
}

CSS-Regeln im normalen DOM können keinen Einfluss auf den Shadow-DOM nehmen. Aber es gibt eine Ausnahme dafür: CSS-Custom-Properties werden auch an den Shadow-DOM weitergegeben. Die Ionic-Komponenten bieten daher viele Styling-Optionen in Form von CSS-Custom-Properties an. Für ion-title gibt es beispielsweise ein Attribut für die Farbdefinition: --color. Über folgendes CSS lässt sich daher ein roter Titel definieren:

ion-title {
    --color: red;
}

Innerhalb des div-Elements befindet sich wie zuvor gezeigt ein Slot als Platzhalter für weiteren Inhalt. Der Inhalt des Slots ist im Beispiel der Text "Ein modaler Dialog". Wie in den Developer-Tools angedeutet, befindet sich dieser Text nicht im Shadow-DOM, sondern im normalen DOM und wird lediglich in den Slot des Shadow-DOMs projiziert. Da sich der Inhalt im normalen DOM befindet, können Entwickler ihn über CSS ansprechen. Eine rote Farbe des Titels kann man daher auch so umsetzen:

<ion-title><span class="red">Ein modaler Dialog</span></ion-title>

.red {
    color: red;
}

Das span-Element mit der CSS-Klasse red befindet sich im normalen DOM, weshalb die CSS-Regel für die Klasse den gewünschten Effekt hat.

Da viele Ionic-Komponenten mit Slots arbeiten, sind die Einschränkungen in Bezug auf die Styling-Möglichkeiten durch den Shadow DOM in der Praxis eher selten ein Problem. Durch den Einsatz von CSS-Custom-Properties haben die Komponenten eine Styling-API und die interne Struktur ist sauber gekapselt. In Kombination mit den Slots ergeben sich vielfältige Anwendungsmöglichkeiten.

Die Ionic-Komponenten können Entwickler als eigenständige Web Components direkt einsetzen. Für größere Projekte bringen moderne Frontend-Frameworks aber viele Vorteile mit. Es wäre möglich, die Ionic Web Components direkt in einem Framework wie Angular oder React einzusetzen, das erfordert aber, dass man selbst die Integration in das Komponentenkonzept des gewünschten Frameworks vornimmt.

Um die Arbeit mit Ionic zu vereinfachen, hat das Ionic-Team die Integration für Angular, React und Vue.js übernommen und stellt sie fertig bereit. Dadurch lassen sich die Ionic-Komponenten einsetzen, als wären sie für das jeweilige Framework entwickelt worden. Zusätzlich ist für jedes der drei Frameworks dessen Standard-Router integriert. Dadurch wird es möglich, bei Seitenwechseln automatisch eine geeignete Animation einzublenden und dabei auch zwischen Vorwärts- und Rückwärtsnavigation zu unterscheiden.

Angular hat aufgrund der Historie die ausgereifteste Unterstützung. Ein neues Projekt initiiert man mit dem Ionic CLI per ionic start. Das Ionic CLI nutzt den Angular-CLI-Befehl ng create zur Erzeugung des Projekts, so dass die Projektstruktur dem Angular-Standard entspricht. Es gibt verschiedene Start-Templates, die man als Ausgang für das eigene Projekt nutzen kann.

Die Ionic-Komponenten lassen sich wie Angular-Komponenten nutzen und im Vergleich zu Ionic 3 hat sich in der Verwendung wenig geändert. Intern kommen aber nun leichtgewichtige Wrapper zum Einsatz, um die Web Components zu kapseln. Neben den Komponenten stellt Ionic einige Angular-Services zur Steuerung von Overlay-Komponenten wie modalen Dialogen, Popups und Alerts oder Funktionen wie virtuelles Scrolling zur Verfügung.

Um einen modalen Dialog zu öffnen, benötigt man daher nicht wie im vorhergehenden Beispiel ein ion-modal-controller-Element  im DOM, sondern erzeugt den Dialog über den ModalController-Service:

import { Component } from "@angular/core";
import { ModalController } from "@ionic/angular";
import { UserEditPage } from "../user-edit/user-edit.page";
   
@Component({
    selector: "app-home",
    templateUrl: "home.page.html",
    styleUrls: ["home.page.scss"]
})
export class HomePage {
   
    constructor(public modalCtrl: ModalController) {}
   
    async openEditModal() {
        const modal = await this.modalCtrl.create({
            component: UserEditPage,
        });
        await modal.present();
    }
}

Die React-Integration von Ionic ist mittlerweile allgemein verfügbar. Auch hier sorgt die Integration dafür, dass Entwickler die Komponenten so nutzen können, als ob es native React-Komponenten wären. Ein Projekt lässt sich per Ionic CLI mit ionic start --type=react initialisieren. Dafür nutzt das Ionic CLI intern create-react-app und erzeugt ein TypeScript-Projekt. Als Router dient der React-Router.

Die Namen der Komponenten notiert man in Pascal Case im Gegensatz zum Kebab Case bei Angular: aus <ion-button> wird <IonButton>. Ansonsten ist die Verwendung sehr ähnlich zu Angular, da es sich natürlich auch weiterhin um die gleichen Web Components handelt.

Da React kein Service-Konzept bietet, kann man die API für modale Dialoge und andere Funktionen über eine Wrapper-Komponente ansprechen. Ein modaler Dialog wird mittels IonModal definiert und die Sichtbarkeit über eine Variable gesteuert. Darüber können Anwender auch Lifecycle-Callbacks definieren. In folgendem Beispiel wird der Komponente UserEdit eine Funktion zum Schließen des Dialogs als Render Prop übergeben:

<IonModal
    isOpen={this.state.showEditModal}
    onDidDismiss={() => this.setState(() => ({ showEditModal: false }))}
>
    <UserEdit
        dismissModal={() => this.setState(() => ({ showEditModal: false }))}
    ></UserEdit>
</IonModal>

Zu beachten ist, dass man alle eingesetzten Komponenten aus @ionic/react importieren muss.

Die Vue.js-Integration befindet sich aktuell noch im Beta-Status. Das Ionic CLI unterstützt noch keine Erzeugung eines Ionic-Vue-Projekts. Stattdessen fügt man Ionic als Plug-in zu einem vorhandenen Vue-Projekt hinzu:

npm install @ionic/vue

In der main.js des Projekts wird das Plug-in geladen und zusätzlich eine Ionic-CSS-Datei eingebunden:

import Vue from 'vue';
import App from './App.vue';
import router from './router';
   
import Ionic from '@ionic/vue';
import '@ionic/core/css/ionic.bundle.css';
   
Vue.use(Ionic);
Vue.config.productionTip = false;
   
new Vue({
  router,
  render: h => h(App)
}).$mount('#app');

Das Plug-in registriert die Ionic-Komponenten global. Entwickler können sie daher direkt verwenden.

Über eine Vue-Prototyp-Erweiterung macht Ionic die Controller der Overlay-Komponenten in der gesamten Anwendung verfügbar. Man kann sie über this.$ionic aufrufen.

Ein modaler Dialog lässt sich somit wie folgt öffnen:

const modal = await this.$ionic.modalController.create({
    component: UserEdit,
});
   
await modal.present();

Derzeit unterstützen die Formular-Komponenten von Ionic noch kein Binding mittels v-model. Da v-model aber nur eine Kurzform für die Übergabe des Werts und Registrierung eines Event-Listeners ist, können Entwickler folgende Form verwenden:

<ion-input 
    :value="name"
    @ionInput="name = $event.target.value"
></ion-input>

Da Ionic 3 nur für Angular zur Verfügung steht, kann ein Upgrade auch nur Angular-Projekte betreffen. Die empfohlene Vorgehensweise zum Upgrade auf Ionic 4 ist, ein neues Projekt mit Ionic 4 anzulegen und dann die Funktionen Schritt für Schritt aus der alten App zu übernehmen. Die meisten Ionic-Komponenten sind in ihrer Verwendung unverändert. Trotzdem kann ein Upgrade für eine komplexe Anwendung mehrere Tage Arbeit bedeuten, denn es findet gleichzeitig auch ein Angular-Upgrade von Version 4 auf 7 statt und es sind diverse kleinere Anpassungen zu berücksichtigen.

Hier nun ein kurzer Überblick über die wesentlichen Änderungen:

Die meisten Umstellungsaktivitäten sind Fleißarbeit. Es gibt ein Set von TS-Lint-Regeln [2], die bei der Umstellung der Markup-Anpassungen unterstützen und einen Teil auch automatisch übernehmen können. Bei RxJS hat es von der Version 5 auf 6 größere Veränderungen gegeben. Über das Paket rxjs-compat kann man aber Kompatibilität zu Version 5 erreichen, sodass keine Anpassungen am eigenen Code erforderlich sind. Man sollte aber überlegen, ob nicht direkt die Umstellung auf die Syntax von Version 6 sinnvoll ist.

Beim Umstellen auf den Angular-Router ist man damit konfrontiert, dass der Ionic-Router und die Controller für Overlays das Laden von Seiten über die Angabe der Seiten-ID als String erlaubten. Zukünftig wird eine Referenz auf die Komponente benötigt. Wenn man die Seiten in eigene Module kapselt, bedeutet das festzulegen, ob Overlay-Komponenten in einem eigenen Modul definiert werden oder zusätzlich im Modul der aufrufenden Seite integriert sind. Die Komponente muss man dann in den EntryComponents des Moduls aufführen. Weiterhin muss die API der Overlay-Komponenten von synchronen Aufrufen auf eine asynchrone API umgestellt werden.

In der Ionic-Dokumentation unter "Guide | Building | Migration" ist die offiziell empfohlene Vorgehensweise dokumentiert.

Die Ionic-UI-Komponenten können mit der Version 4 als generische Web Components flexibler als bisher eingesetzt werden. Für die bekannten Frontend-Frameworks Angular, React und Vue.js stehen fertige Integrationen bereit, die den Komfort erhöhen. Im zweiten Teil des Artikels werden die Schwesterprojekte Capacitor und Stencil betrachtet, die Ionic um interessante Möglichkeiten ergänzen.

Norbert Frank
ist seit mehr als 15 Jahren in der IT als Entwickler, Berater und Softwarearchitekt tätig. Er ist passionierter Full-Stack-Entwickler und JavaScript-Enthusiast bei der Lucom GmbH. (bbo [3])


URL dieses Artikels:
https://www.heise.de/-4572461

Links in diesem Artikel:
[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports
[2] https://github.com/ionic-team/v4-migration-tslint
[3] mailto:bbo@ix.de