10: Veröffentlichung & Hosting

Ihr baut jetzt also eine App – aber was, wenn sie dann endlich einmal fertig ist? Darum geht es in dieser Session.

Inhalt

  1. Wie geht man mit PWA-Events richtig um und welche Optionen habe ich im Manifest?
  2. Was passiert bei npm run build?
  3. Was ist CI/CD?
  4. Was ist dieses Let’s Encrypt und warum ist das so wichtig?
  5. Wie kann ich PWAs kostenfrei hosten?
  6. Praxis / Nächste Sessions

Wie geht man mit PWA-Events richtig um und welche Optionen habe ich im Manifest?

Wir haben während des Kurses ganz oft davon gesprochen, dass wir eine PWA entwickeln, aber eure Apps werden erst dann zu PWAs, wenn sie „verpackt“ werden. Das geschieht vollautomatisch durch das cli-plugin-pwa, das installiert wurde als wir während des Setups unseres Projekts mit der Vue CLI die Option „PWA Support“ ausgewählt haben. Um unsere PWA-Funktionalität testen zu können, müssen wir unsere Apps also erst einmal mit npm run build verpacken und dann über einen lokalen Webserver laufen lassen – mehr dazu im nächsten Abschnitt.

Jetzt soll es erst einmal darum gehen, welche Optionen ihr dem PWA-Plugin übergeben könnt, und welche Dinge ihr außerdem beachten solltet, um euren Nutzern eine gute Erfahrung zu bieten. Eine genaue Beschreibung aller Optionen findet ihr in der Dokumentation des Plugins. Ich werde hier nur auf die für den Anfang wichtigsten eingehen.

Konfigurationsoptionen

All diese Optionen könnt ihr in der Datei vue.config.js in eurem Projektordner unter der Eigenschaft pwa eintragen – müsst es aber nicht, denn sie haben alle Standardeigenschaften. Verwendet also nur die Optionen, die ihr ändern möchtet.

  • appleMobileWebAppCapable ist standardmäßig deaktiviert, da iOS <11.3 keine PWA-Unterstützung bietet. Wenn ihr diese Option auf 'yes' stellt, wird eure App auch auf iOS wie eine PWA behandelt, aber es könnte zu Problemen führen, wenn eine alte iOS-Version verwendet wird. Mehr dazu hier.

  • appleMobileWebAppStatusbarStyle hat standardmäßig den Wert 'default'. Diese Eigenschaft gibt an, wie mit der iOS-Statusleiste umgegangen werden soll. 'default' ergibt eine weiße Statusleiste mit schwarzem Text. 'black' lässt die Statusleiste wie einen komplett schwarzen Balken erscheinen (ohne Text) und 'black-translucent' lässt die Statusleiste halbtranparent als Overlay über eurer App erscheinen – sorgt also dafür, dass ihr oben etwas „Fleisch“ (padding) habt, wenn ihr diese Option verwendet!

  • manifestOptions ist ein Objekt, in dem ihr die Optionen für euer Web App Manifest ablegen könnt. Das Manifest ist neben dem Service Worker das Herzstück eurer PWA, ohne es ist sie nicht installierbar. Das Manifest wird ebenfalls für euch generiert, in dieser Option könnt ihr die folgenden Standardwerte verändern (in Klammern angegeben):

    • name (pwa.name): der Name eurer App, der beim Starten und unter dem Icon angezeigt wird
    • short_name (pwa.name): ein verkürzter Name, der angezeigt wird, wenn nicht genug Platz für name da ist
    • description: hier könnt ihr eine kurze Beschreibung eurer App ablegen
    • start_url ('.'): die URL auf der eure App starten soll (sollte immer auf '/' gesetzt werden, da es ansonsten zu 404-Fehlern kommen kann)
    • display ('standalone'): dieser Wert gibt an, wie die App sich verhalten soll. Mit 'standalone' wird sie sich wie eine normale App verhalten, aber es gibt z.B. noch 'fullscreen', wenn die App den gesamten Bildschirm einnehmen soll, oder 'browser', wenn man die Schaltflächen des Browsers sehen können soll. Meistens ist 'standalone' aber durchaus in Ordnung
    • theme_color (pwa.themeColor): die Farbe, die die Statusleiste der App auf Android haben soll
    • background_color: die Hintergrundfarbe, die der Startbildschirm eurer App haben soll (wenn die App gestartet und nur das Icon und der Name angezeigt werden während sie lädt)
    • orientation: wenn ihr hier 'portrait' angebt, wird sich eure App nicht automatisch drehen, wenn ihr euer Smartphone dreht
  • msTileColor gibt an, welche Farbe die Kachel eurer App im Windows-Startmenü haben soll

  • name ist der Name eurer App, der auch im Manifest benutzt wird, solltet ihr in den manifestOptions keinen Anderen angeben. Er ist standardmäßig auf den Wert der „name“ Eigenschaft in eurer package.json eingestellt

  • themeColor ist die Farbe, die auch im Manifest benutzt wird um die Fensterfarbe, bzw. die Farbe der Statusleiste unter Android festzulegen. Hier bietet es sich an die Primärfarbe eurer App, oder einen leicht dunkleren Ton davon zu benutzen

  • workboxOptions ist ein Objekt, in das ihr Optionen für die Generierung eures Service Workers eingeben könnt. Was genau das ist ist zu kompliziert für diesen Kurs, aber wenn ihr dort die Eigenschaft skipWaiting auf true stellt, könnt ihr die App auf eine neue Version aktualisieren, ohne sie neu starten zu müssen, weshalb ich euch das stark empfehle.

Tipp:

Wie gesagt: ihr müsst nur die Optionen verwenden, die ihr ändern wollt. Ich empfehle euch, sie einfach alle mal auszuprobieren und zu sehen, was sich ändert. Persönlich passe ich meistens nur die Farben und den Namen an und setze skipWaiting auf true – für den Rest reichen die Standardwerte vollkommen aus.

Service Worker Events

Der Service Worker ist ein Skript, welches euch das cli-plugin-pwa generiert und das sich darum kümmert, dass eure App offline laufen und installiert werden kann. Das ist natürlich nur eine sehr grobe Beschreibung, denn Service Worker können noch viel mehr, aber für diesen Kurs ist das zunächst alles, was ihr wissen müsst. Mehr Infos gibt es hier, aber wie gesagt, das ist für unsere Zwecke nicht weiter relevant.

Der Service Worker gibt eine Reihe von Events aus, auf die ihr reagieren könnt, um zum Beispiel in der App anzuzeigen, wenn die Internetverbindung abbricht, oder es ein Update für die App gibt. Die Vue CLI macht uns auch das leicht und generiert uns die Datei src/registerServiceWorker.js in der wir einfach unsere Funktionen für das jeweilige Event schreiben können.

Hier bietet es sich zum Beispiel an über Vuex mit dem Rest eurer App zu kommunizieren. Hier ist meine Version der Datei für Done in Time:

/* eslint-disable no-console */

import { register } from 'register-service-worker';

// importiere den Vuex Store, um mit dem Rest der App zu kommunizieren
import Store from '@/store';

if (process.env.NODE_ENV === 'production') {
  register(`${process.env.BASE_URL}service-worker.js`, {
    ready() {
      if (window.location.host.includes('localhost') || window.location.host.includes('172.0.0.1')) {
        console.log(
          'App is being served from cache by a service worker.\n'
          + 'For more details, visit https://goo.gl/AFskqB',
        );
      }
    },
    registered() {
      if (window.location.host.includes('localhost') || window.location.host.includes('172.0.0.1')) console.log('Service worker has been registered.');
    },
    // diese Funktion wird aufgerufen, sobald der Service Worker initialisiert hat
    // und die App offline verfügbar ist
    cached() {
      if (window.location.host.includes('localhost') || window.location.host.includes('172.0.0.1')) console.log('Content has been cached for offline use.');
      // über die addToast-Mutation kann ich in MEINER App Benachrichtigungen anzeigen,
      // ihr könnt euch auch eine solche Funktionalität einbauen
      Store.commit('addToast', { message: 'Done in Time has been fully cached on your device and will be available offline from now on', type: 'positive' });
    },
    updatefound() {
      if (window.location.host.includes('localhost') || window.location.host.includes('172.0.0.1')) console.log('New content is downloading.');
    },
    // diese Funktion wird aufgerufen sobald ein Update für eure App heruntergeladen wurde
    async updated(registration) {
      if (window.location.host.includes('localhost') || window.location.host.includes('172.0.0.1')) console.log('New content is available; please refresh.');
      // ich speichere hier in der Datenbank, dass der Changelog angezeigt werden soll
      await Store.dispatch('setUserProperty', { key: 'forceChangelog', value: true });
      // und sende dem Nutzer dann eine Benachrichtigung, dass er die App nur neu laden
      // muss indem er einen Button klickt, um die neue Version zu verwenden
      // das funktioniert NUR, wenn die option skipWaiting in den workboxOptions auf true gesetzt ist!
      // Ansonsten wird die App erst aktualisiert, wenn sie komplett geschlossen und neu geöffnet wird
      Store.commit('addToast', {
        action: () => {
          if (registration.waiting) registration.waiting.postMessage({ type: 'SKIP_WAITING' });
          window.location.reload(true);
        },
        actionLabel: 'Refresh',
        message: 'A new version of Done in Time is available, refresh to use the newest version.',
        timeout: -1,
      });
    },
    offline() {
      if (window.location.host.includes('localhost') || window.location.host.includes('172.0.0.1')) console.log('No internet connection found. App is running in offline mode.');
    },
    error(error) {
      if (window.location.host.includes('localhost') || window.location.host.includes('172.0.0.1')) console.error('Error during service worker registration:', error);
      Store.commit('addToast', { message: `There was an error registering the service worker: ${error.message}` });
    },
  });
}

Wie ihr seht, benutze ich eigentlich nur die cached() und updated() Funktionen, die anderen geben mir nur Statusupdates, so lange die App lokal auf meinem PC (unter „localhost“) läuft. Was ich hier mache ist komplett optional, verbessert aber die Nutzererfahrung denke ich sehr, da der Nutzer bescheid weiß, was gerade im Hintergrund passiert.

Tipp:

Baut diese Funktionalität ruhig ein, wenn ihr möchtet, aber schaut, dass vorher der Rest eurer App gut funktioniert!

Das Install-Prompt

Sobald der Browser merkt, dass sich eure App installieren lässt (sofern der Browser das unterstützt), sendet er das beforeinstallprompt-Event. Wenn ihr dieses Event nicht abfangt, sehen eure Nutzer sofort ein pop-up Prompt, das sie dazu bewegen soll, eure App zu installieren. Das ist aus zwei Gründen problematisch:

  1. Das Prompt kommt vom Browser, passt also überhaupt nicht zur Gestaltung eurer App
  2. Das Prompt kommt meistens innerhalb weniger Sekunden nachdem euer Nutzer die App zum ersten Mal aufgerufen hat und womöglich noch gar nicht weiß, ob er/sie die App überhaupt mag

Wenn ihr also mit diesem Prompt nicht richtig umgeht ist die Chance hoch, dass euer Nutzer es einfach wegklickt, oder ablehnt und somit die Möglichkeit verliert, eure App zu installieren, falls er oder sie eure App dann überhaupt noch verwenden möchte.

Deshalb bietet es sich an, im created()-Lifecycle-Hook eurer App.vue das Event abzufangen und für später abzuspeichern, sodass ihr es dem Nutzer zu einem geeigneteren Zeitpunkt wieder anzeigen könnt:

export default {
  // …andere Optionen…
  created() {
    // registriere einen Event Listener für das beforeinstallprompt event
    window.addEventListener('beforeinstallprompt', (prompt) => {
      prompt.preventDefault(); // fange das Event ab und verhindere seinen Effekt
      // speichere das Event im Vuex Store, damit es zu einem späteren Zeitpunkt
      // aufgerufen werden kann, dafür MUSS natürlich im Store eine
      // entsprechende Mutation registriert sein!
      this.$store.commit('setInstallPrompt', prompt);
    });
  },
  watch: {
    // registriere eine Funktion, die immer dann ausgeführt wird, wenn die Route
    // sich verändert
    $route(to, from) {
      // hier überprüfe ich, ob die letzte Route nicht das Onboarding war und ob
      // ein installPrompt im Vuex Store hinterlegt wurde und der Nutzer noch
      // nicht gefragt wurde, ob er die App installieren möchte
      // WICHTIG: dieses Skript funktioniert natürlich NUR, wenn ihr euren Store
      // so aufbaut wie ich es getan habe!!! Seht es als Inspiration, es ist
      // KEINE KOMPLETTLÖSUNG
      if (
        from.name !== 'Onboarding'
        && this.$store.state.application.installPrompt
        && !this.$store.state.user.prompted
      ) {
        // zeige eine Notification in der App, die darauf hinweist, dass sie installiert werden kann
        this.$store.commit('addToast', {
          // wenn auf den "Install"-Button der Notification geklickt wird:
          action: async () => {
            // benutze das abgespeicherte Prompt, um das Pop-Up des Browsers anzuzeigen
            this.$store.state.application.installPrompt.prompt();
            // userChoice ist eine Promise, die angibt, wie der Nutzer auf das Pop-Up reagiert
            const choice = await this.$store.state.application.installPrompt.userChoice;
            // wenn es akzeptiert wurde:
            if (choice.outcome === 'accepted') {
              this.$store.commit('addToast', {
                message: 'Great! Done in Time should have been added to your homescreen or desktop. You can now launch it from there and close this tab.'
              });
            } else { // wenn nicht
              this.$store.commit('addToast', { message: 'Alright, we won’t bother you again.' });
            }
          },
          actionLabel: 'Install',
          message: 'You seem to be enjoying Done in Time! Would you like to install it to your device?',
          timeout: -1,
        });

        // speichere ab, dass der Nutzer gefragt wurde, ob die App installiert werden soll
        // damit er nicht ständig damit genervt wird
        await this.$store.dispatch('setUserProperty', { key: 'prompted', value: true });
      }
    }
  }
  // …andere Optionen…
};

Wie auch die Reaktion auf die Service Worker Events ist dieser Teil optional, das bedeutet, ihr müsst das Event in eurer App nicht behandeln, um den Kurs zu bestehen (aber natürlich verbessert es eure Note 😉). Falls ihr aber jemals eine professionelle App baut, solltet ihr es auf jeden Fall tun, denn alles Andere ist eine sehr schlechte UX.

Auch ohne diese Optionen und speziellen Funktionen erhaltet ihr nach dem Verpacken eurer App eine voll funktionsfähige PWA – dank der Vue CLI. Aber jetzt habt ihr zumindest einen Überblick darüber, wie ihr das automatisch generierte Manifest und den Service Worker beeinflussen könnt.

Was passiert bei npm run build?

Ich habe jetzt schon mehrmals angemerkt, dass eure App „verpackt“ werden muss, bevor ihr sie auf einen Webserver laden und somit der Welt zugänglich machen könnt. Dieses „Verpacken“ nennt man auf Englisch „build“, also „bauen“. In eurem Projekt, das ihr mit der Vue CLI erstellt habt, könnt ihr einfach npm run build in einem Terminal aufrufen und die Vue CLI kümmert sich automatisch um alles.

Dieser Prozess kann eine Weile dauern, aber am Ende erhaltet ihr einen Ordner (standardmäßig dist) in eurem Projektordner, den ihr eins zu eins so auf euren Webspace laden könnt. In diesem Ordner befindet sich eine index.html-Datei, die eure Vue-Anwendung lädt und initialisiert. Mehr braucht ihr theoretisch nicht zu wissen.

Achtung:

Wenn eure App nicht direkt unter der Domain (also z.B. https://meine-app.de) läuft, sondern in einem Unterordner (z.B. https://meine-website.de/meine-app) müsst ihr in eurer vue.config.js den richtigen Pfad unter der Eigenschaft publicPath ablegen! Mehr Informationen dazu findet ihr hier.

Falls ihr aber neugierig seid, hier ein grober Überblick, was genau passiert, wenn ihr dieses Kommando ausführt:

  • Euer JavaScript wird so transpiliert, dass es auch ältere Browser verstehen
  • Falls ihr einen CSS-Preprocessor benutzt, wird dieser in normales CSS übersetzt
  • Euer CSS wird mit den korrekten Prefixen für ältere Browser versehen (z.B. -webkit-transition, -moz-transition, etc.)
  • Statische Assets wie Bilder und Icons werden richtig verlinkt und an die relevanten Stellen kopiert
  • Alle externen Libraries, die ihr über npm installiert habt werden zusammengefasst und komprimiert
  • All euer Code wird komprimiert und in kleinere Dateien aufgeteilt, externe Libraries und Skripte werden korrekt importiert
  • Ein Web App Manifest und ein Service Worker werden generiert und verlinkt
  • Eine index.html wird generiert, die automatisch die richtigen Stylesheets und Skripte importiert
  • Alles wird in einen Ordner gepackt (normalerweise dist), den ihr einfach auf euren Server kopieren könnt, um ihn zu veröffentlichen
  • Ihr bekommt Warnungen angezeigt, wenn eure Dateien zum Beispiel zu groß sind, oder es Fehler in euren Skripten gibt

Wenn ihr einmal mehr Erfahrung gesammelt habt, könnt ihr euch diesen Prozess genauer ansehen und in bestimmte Schritte eingreifen, oder sie erweitern, um zum Beispiel Bilder auf die richtigen Größen zuzuschneiden, etc. Möglich wird das Ganze durch Technologien wie Webpack und Babel.

Achtung:

Zwar könnt ihr euren dist-Ordner problemlos auf eurem Webspace veröffentlichen, aber es gibt einige wichtige Dinge zu beachten, wenn ihr wollt, dass alles richtig funktioniert, z.B. müssen bestimmte Weiterleitungen eingerichtet werden und ihr braucht ein SSL-Zertifikat (https), damit eure PWA funktionieren kann. Diese Schritte weichen stark von Hoster zu Hoster ab, aber in diesem Abschnitt der Vue CLI Dokumentation findet ihr Anleitungen und Hinweise für sehr viele von ihnen.

Was ist CI/CD?

CI/CD ist ein recht neues Konzept in der Softwareentwicklung, in dem es grob gesagt darum geht, große Teile der Integration von neuem Code zu automatisieren und so weit es geht für die Entwickler zu vereinfachen. Es gibt keine klare Definition dieses Konzepts, oder seiner einzelnen Bestandteile „Continuous Integration“, „Continuous Delivery“ und „Continuous Deployment“, aber zahlreiche Artikel, die sich damit befassen. Ihr könnt also in die Tiefe gehen, wenn euch dieses Thema näher interessiert – zum Beispiel hier.

Mich persönlich interessiert in diesem Zusammenhang eigentlich nur die Tatsache, dass ich die Veröffentlichung meiner Apps ohne großen Aufwand (und Kosten) automatisieren kann, damit ich mich auf das konzentrieren kann, was mir Spaß macht: das Entwickeln neuer Features.

Dieser Teil von CI/CD lässt sich im „Continuous Deployment“ wiederfinden. Wie es im oben verlinkten Artikel so eloquent beschrieben wird:

In der Praxis bedeutet Continuous Deployment, dass App-Änderungen eines Entwicklers binnen weniger Minuten nach ihrer Erstellung live gehen können (vorausgesetzt, sie bestehen den automatischen Test). Dies erleichtert eine kontinuierliche Integration von User Feedback ungemein.

Wie kann ich meine App bei jedem Push neu generieren und veröffentlichen lassen?

CD funktioniert im Zusammenhang mit Git und Git-Providern sehr gut, indem eine Pipeline, also eine Reihe von Arbeitsschritten, durch einen Push zu einem bestimmten Branch ausgelöst wird. In unserem Fall sind diese Schritte einfach:

  • Führe npm run build aus
  • Veröffentliche den dist-Ordner

Das kann natürlich noch deutlich komplexer werden, indem zum Beispiel vor der Veröffentlichung automatisierte Tests ablaufen. Wie genau dieser Ablauf konfiguriert wird, hängt von dem jeweiligen Git bzw. CI Provider ab. GitLab bietet einen integrierten CI-Dienst an, den ihr aktivieren und konfigurieren könnt, indem ihr eine .gitlab-ci.yml in euer Repository pusht. Mehr erfahren könnt ihr dazu in dieser Dokumentation. Wir werden aber speziell für GitLab in einem nachfolgenden Abschnitt noch ein konkretes Beispiel anführen.

Es bietet sich auch an, einen separaten Branch für die „veröffentlichte“ Version eures Codes anzulegen, den ich meistens public oder production nenne. So könnt ihr weiterhin die aktuellste Version eures Codes in eurem master-Branch ablegen und dort arbeiten, ohne diese Änderungen, die eventuell noch unfertig sind direkt zu veröffentlichen.

Seid ihr dann bereit, eine neue Version eurer App zu veröffentlichen, merged ihr euren master-Branch in den production-Branch und pusht diesen dann zu eurem Git-Provider. Dieser Push löst die Pipeline aus und ein paar Minuten später habt ihr die neue Version veröffentlicht.

Tipp:

Vergesst nicht, mit npm version patch/minor/major die Version eurer App zu erhöhen! Gerade bevor ihr sie veröffentlicht ist der richtige Zeitpunkt dafür.

Wie ihr seht ist dieses System einfach und flexibel, sobald ihr es das erste Mal konfiguriert habt. Ihr könnt euch darauf konzentrieren, eure App zu schreiben und wenn ihr bereit seid, eine neue Version zu veröffentlichen, reichen, zwei, drei Terminal-Kommandos (oder ein einzelnes selbstgeschriebenes Skript 😉), um eure App vollautomatisch zu veröffentlichen.

Achtung:

CI-Dienstleister rechnen nach „Buildminuten“ ab, also der Zeit, die es dauert, euren Code zu „verpacken“ und zu veröffentlichen! Bei den meisten habt ihr einige Freiminuten im Monat, die ihr kostenlos nutzen könnt und für kleine Projekte wie eure App aus dem Kurs werdet ihr wohl nicht darüber hinaus kommen, aber ihr solltet dennoch im Hinterkopf behalten, dass diese Dienste natürlich nicht einfach nur kostenlos sind. Veröffentlicht eure Apps also nicht alle paar Sekunden neu, wenn ihr nicht bereit seid, den Preis dafür zu zahlen.

Was ist dieses Let’s Encrypt und warum ist das so wichtig?

Damit eure PWA auf Geräten funktionieren und über APIs auf Kameras und andere Sensoren zugreifen kann, muss sie über einen sogenannten „Secure Context“ ausgeliefert werden. Das kann nur geschehen, indem ihr über ein SSL-Zertifikat eure Identität bestätigt. In der Praxis bedeutet das, die URL eurer App muss mit https beginnen, nicht mit http.

Das bedeutet, dass die Verbindung von eurer Website oder App zum Web-Server, der die Dateien ausliefert, verschlüsselt ist und nicht abgefangen und manipuliert werden kann, und dass die Website oder App auch wirklich die ist, für die sie sich ausgibt.

In der Vergangenheit war es sehr teuer, ein solches SSL-Zertifikat zu erhalten, das zudem noch, wie eure Domain auch, jährlich erneut bezahlt werden musste. Dank der Let’s Encrypt-Initiative hat sich aber alles verändert. Nun kann jeder vollautomatisch und kostenlos ein eigenes SLL-Zertifikat erhalten.

Viele Web-Hoster bieten es euch deshalb auch an, ein „kostenloses SSL Zertifikat über Let’s Encrypt“ zu erhalten, indem ihr einfach eine Checkbox anklickt. Solltet ihr aber jemals einen eigenen Server betreiben, könnt ihr auch dort einfach den Certbot von Let’s Encrypt installieren und somit automatisch das SSL-Zertifikat eures Servers ständig erneuern lassen. 👍

Let’s Encrypt ist also eine „freie, automatisierte und offene Zertifizierungsstelle“ für SSL-Zertifikate. Alle weiteren Informationen findet ihr auf der Website von Let’s Encrypt.

Wie kann ich PWAs kostenfrei hosten?

Da PWAs eigentlich nur statische Webseiten sind, d.h. nur aus ein paar kleinen Textdateien bestehen, ist es vergleichsweise günstig für einen Hosting-Provider sie zu hosten. Deshalb gibt es inzwischen sehr viele Anbieter, die es als Teil ihrer „kundenbindenden“ Leistungen anbieten, eure statischen Webseiten kostenlos für euch zu hosten, sofern sie innerhalb bestimmter Bandbreitenlimits und anderer Einschränkungen liegen, die ihr bitte den Allgemeinen Geschäftsbedingungen der Anbieter entnehmt.

Ich stelle euch hier zwei dieser Anbieter näher vor, mit denen ich in der Vergangenheit gute Erfahrungen gemacht habe, und die ich auch heute noch verwende.

GitLab Pages

Die erste und einfachste Option ist GitLab Pages. Damit könnt ihr eine statische HTML-Website direkt von eurem Repository aus hosten und verwalten. Hier hoste ich zum Beispiel diese Kurswebsite. Dank des CI-Diensts von GitLab werden alle Änderungen, die ich in dem Repository der Seite vornehme nach einem Push automatisch veröffentlicht und wie ihr seht bekomme ich sogar eine kostenlose https://gitlab.io-Domain (auch wenn ich natürlich problemlos auch meine eigene Domain benutzen könnte).

GitLab Pages wird für euer Repository aktiviert, indem ihr ihm eine gültige .gitlab-ci.yml hinzufügt, die einen pages-Job enthält. Ihr müsst hier wie immer nicht alles genau verstehen, was passiert, aber es ist eigentlich recht einfach. Diese Datei aktiviert den CI-Dienst GitLabs und verschiebt nach dem „verpacken“ eurer App den dist-Ordner in einen public-Ordner, den GitLab dann dazu verwendet, um die Seite zu hosten.

In der Dokumentation von Vue CLI gibt es einen eigenen Bereich, der euch den Code für diese Datei aufzeigt (ich habe sie hier für unsere Zwecke vereinfacht und neu kommentiert):

# .gitlab-ci.yml für euer Projekt-Root-Verzeichnis

pages: # der Name des CI-Jobs (muss pages sein)
  image: node:latest # benutze das neuste Node.js
  stage: deploy
  script:
    - npm ci # wie npm install, nur für CI Umgebungen, siehe https://docs.npmjs.com/cli/ci.html
    - npm run build # App verpacken
    - mv public public-vue # GitLab Pages braucht einen public-Folder, aber der wird auch von Vue verwendet, deshalb wird er temporär verschoben
    - mv dist public # benenne den dist Ordner in public um
  artifacts:
    paths:
      - public # sorge dafür, dass der public-Ordner nicht gelöscht wird, wenn das Skript vorbei ist, damit GitLab pages ihn veröffentlichen kann
  only:
    - production # führe dieses Skript nur für den production-Branch aus

Habt ihr die Datei in euer Repository gesteckt und etwas auf den Branch production gepusht, sollte GitLab nun das Skript ausführen und eure Seite veröffentlichen. Die Domain, die ihr dabei standardmäßig erhaltet ist https://euer-gitlab-nutzername.gitlab.io/name-des-repositories. Wenn ihr also nicht vor habt eine eigene Domain für eure App zu verwenden, müsst ihr dafür sorgen, dass ihr den publicPath innerhalb eurer vue.config.js-Datei dementsprechend anpasst:

module.exports = {
  // …andere Optionen…
  publicPath: process.env.NODE_ENV === 'production' // wenn die App für die Veröffentlichung verpackt wird
    ? '/' + process.env.CI_PROJECT_NAME + '/' // setze den Wert auf /name-des-repositories/
    : '/' // ansonsten (also während der Entwicklung) lasse ihn auf '/'
  // …andere Optionen…
}

Bitte beachtet auch, dass es aktuell keine Möglichkeit gibt, jegliche Anfragen an eine so gehostete Datei auf die index.html-Datei umzuleiten, ihr könnt also nicht den History-Modus des Vue Router benutzen, wenn ihr auf GitLab Pages hosten möchtet!

Netlify

Netlify ist der Betreiber eines globalen Content-Delivery-Networks (kurz CDN), der es euch ebenfalls erlaubt kostenlos statische Websites zu hosten und euch sogar eine kostenlose https://meine-app.netlify.app-Domain gibt. Um dort eure App zu hosten, braucht ihr einen Account, den ihr euch kostenlos anlegen könnt.

Habt ihr das getan, könnt ihr dort ein neues Projekt anlegen und mit eurem GitLab Repository verbinden, was automatisch einen CI-Dienst einrichtet (ihr braucht also keine.gitlab-ci.yml). Dann müsst ihr nur noch die richtigen Befehle in das Formular eintragen:

  • Branch to Deploy: production
  • Build Command: npm run build
  • Publish Directory: dist

Habt ihr das getan, wird eure App ebenfalls automatisch veröffentlicht, wann immer ihr einen Push im genannten Branch unternehmt. Ihr könnt dann den Namen eurer App, die Domain, und andere Einstellungen innerhalb der Netlify-Oberfläche verändern.

Falls ihr eure App lieber von Hand veröffentlichen möchtet, könnt ihr bei Netlify außerdem auch einfach einen Ordner mit euren Dateien (in unserem Fall den dist-Ordner) in die Oberfläche ziehen.

Wenn ihr den History-Mode des Vue Router benutzt, müsst ihr außerdem noch eine _redirects-Datei im public-Ordner eurer Vue-App ablegen, die jede Anfrage auf die index.html umleitet:

# Netlify Settings for PWA
/* /index.html 200

Achtung: Wenn ihr eine _redirects-Datei anlegt, müsst ihr diese von eurem generierten Service Worker ignorieren lassen, ansonsten kann dieser nicht korrekt generiert werden. Fügt dazu folgende Zeile in das workboxOptions-Objekt in eurer vue.config.js-Datei ein:

// vue.config.js
module.exports = {
  // …andere Optionen…
  pwa: {
    // …andere Optionen…
    workboxOptions: {
      // …andere Optionen…
      exclude: ['_redirects'], // ignoriere _redirects, um Fehler im Service Worker zu verhindern
      // …andere Optionen…
    },
  },
  // …andere Optionen…
}

Warum die Weiterleitung?

Wenn ihr den History-Mode verwendet, sieht es für den Browser so aus als wäre unter dem Pfad meine-app.de/todos eine index.html-Datei, die aber in dieser Form nicht existiert (es gibt ja nur die index.html unter meine-app.de/). Damit eure App also richtig starten und dann über den Vue-Router auf die angegebene URL wechseln kann, müsst ihr alle Anfragen zunächst auf die index.html umleiten.

Welchen Provider soll ich benutzen?

Wie gesagt, es gibt Unmengen an Providern, bei denen ihr eure Apps hosten könnt, denn es muss ja einfach nur ein Web-Hoster sein. Ihr könnt also gerne auch den selben Hosting-Provider benutzen, auf dem ihr aktuell eure Portfolio-Webseiten hostet.

Die Beiden, die ich euch hier vorgestellt habe sind für eure Nutzen kostenlos, und besonders GitLab Pages hat den Vorteil, dass ihr alles, was eure App angeht an einem zentralen Ort habt: eurem Git-Repository. Allerdings gibt es die unangenehme Einschränkung mit den fehlenden Redirects, falls ihr den History-Mode verwenden möchtet.

Da ich in meinen Projekten immer den History-Mode benutze, verwende ich für meine Apps also lieber Netlify, aber statische Websites, die zum Beispiel über Gridsome generiert werden, wie die Kurswebsite, hoste ich auf GitLab Pages.

Nehmt einfach den Provider, der für euch am bequemsten ist – und behaltet im Hinterkopf, dass ihr euch auch immer zu einem späteren Zeitpunkt noch umentscheiden könnt.

Praxis / Nächste Sessions

Ihr seid jetzt mitten in der Entwicklung eurer Apps, deshalb wird der heutige Praxisteil der sein, mit dem wir auch die noch verbleibenden Sessions komplett füllen werden: ihr entwickelt eure Apps weiter.

Wie gehabt stehe ich euch bis 17:15 für Fragen und Hilfestellungen im Video-Call zur Verfügung, zu anderen Zeiten einfach per Mail oder Telegram.

Ansonsten bleibt mir nichts anderes übrig als zu sagen: frohes Schaffen! Ich hoffe ihr konntet alle etwas neues Lernen und wisst jetzt, wie man an die Entwicklung einer PWA herangehen kann. Alles, was wir besprochen haben wird euch auch weiterhin in diesen Skripten zur Verfügung stehen, aber bedenkt immer, dass die JavaScript-Welt nicht still steht und sich jeden Tag weiterentwickelt. Es kann also gut sein, dass in einem halben Jahr die Landschaft ganz anders aussieht und die Welt mit anderen Libraries, Frameworks und Technologien arbeitet.

Trotzdem bleiben die Prinzipien die selben: lest die Dokumentationen und haltet die Dinge so einfach wie möglich, dann sind euch keine Grenzen gesetzt. 😊

Ich bin sehr gespannt darauf, eure fertigen Apps zu sehen und zu testen! 🎉