Full Stack JavaScript Framework

von Manuel am 12.02.2019 um 18:30 Uhr

Willkommen zurück! Heute möchte ich euch eines meiner Lieblingsprojekte der letzten Wochen und Monate vorstellen. Nachdem ich mich ausführlich mit Node.js, Nuxt.js und anderen Themen wie TypeScript beschäftigt habe, ist ein Stack entstanden, der euch schnell mit euren Projekten starten lässt. Neben den Grundbestandteilen wie Node.js und Fastify für die Serverseite und Nuxt.js für das Frontend, findet ihr unter anderem auch folgende Funktionalitäten:

Ihr findet den Stack auf GitHub und könnt ihn jederzeit für eigene Projekte nutzen. Das Ganze steht unter der MIT Lizenz. Nachdem ihr die Schritte aus der Readme Datei durchgeführt und damit den Server gestartet habt, werft als nächstes am besten einen Blick in die Usage.md Datei. Dort zeige ich ausführlich, wie die einzelnen Komponenten korrekt verwendet werden. Besonders bei den Komponenten mit Tree Shaking ist die richtige Verwendung wichtig.

Tree Shaking kurz und knapp

Falls ihr euch jetzt fragt, was dieses Tree Shaking ist, von dem ich jetzt schon einige Male gesprochen habe, dann lest die nächsten Zeilen. Bei jedem Node.js Projekt wird am Ende ein Build Prozess gestartet. Dieser fasst euren Code und alle eure Abhängigkeiten in einem Paket zusammen, um es auf einem Server veröffentlichen zu können. Dabei ist die Größe des fertigen Builds ein entscheidender Faktor für die Geschwindigkeit eurer Seite. Unter Tree Shaking versteht man die Reduzierung einer Abhängigkeit auf das Nötigste.

Nehmen wir als Beispiel FontAwesome 5. Es ist eine sehr umfangreiche Sammlung an verschiedensten Icons für eure Oberfläche. Durch die unzähligen Icons ist dieses Paket aber auch ziemlich groß. Wenn ihr in eurem Projekt von den tausenden von Icons aber nur zehn benötigt, wäre es kontraproduktiv, wenn trotzdem alle anderen Icons auch im finalen Produkt enthalten wären. Hier kommt Tree Shaking ins Spiel. Es wird automatisch von WebPack im Hintergrund erledigt, wenn ihr die einzelnen Icons wie in der Usage.md Datei beschrieben verwendet. Dabei werden nur die Icons in den Build einbezogen, die ihr eingebunden habt.

Details einzelner Komponenten

Obwohl in der Usage.md bereits einige hilfreiche Informationen zu den verwendeten Komponenten zu finden sind, will ich in den folgenden Abschnitten nochmal ausführlicher auf einzelne davon eingehen. Zum einen, um euch die Verwendung deutlicher zu machen, zum anderen aber auch, um euch Probleme zu erzählen, mit denen ich während der Zusammenstellung des Stacks zu kämpfen hatte.

TypeScript

Das Ziel bei der Zusammenstellung war es, alles über TypeScript zu entwickeln. Dazu zählten nicht nur eigene Dateien von Client und Server, sondern auch die Konfigurationen. TypeScript kommt in immer mehr Projekten zum Einsatz. Nachdem Angular mit Version 2 den Umstieg gewagt hat, wird Vue.js mit Version 3 auch komplett auf TypeScript setzen. Damit wird natürlich die Umsetzung eigener Projekte mit dieser Sprache auch vereinfacht, da man nicht mehr soviel Vorarbeit leisten muss, um das Framework an sich vorzubereiten. Wenn ich Projekte mit TypeScript umsetze, dann verwende ich immer Visual Studio Code. Die Unterstützung ist einfach sehr gut, weil Microsoft sowohl die Sprache als auch die Umgebung entwickelt und somit alles aus einer Hand kommt.

Die Entwicklung mit TypeScript bietet dabei unter anderem folgende Vorteile:

Natürlich gibt es bei der Entwicklung auch einige Umstellungen und Sachen zu beachten, auf die ich während der Stackentwicklung gestoßen bin.

Decorator

Bei der Entwicklung von Vue Komponenten mittels JavaScript, sieht der Skript Teil üblicherweise in etwa so aus:

module.exports = {
  data: function () {
    return {
      greeting: 'Hello'
    }
  }
}

Im Beispiel seht ihr den anonymen Export eines JavaScript Objekts, der dann die nötigen Eigenschaften für eine Komponente beinhaltet. Wenn ihr die gleiche Komponente mittels TypeScript umsetzen möchtet, sieht das in etwa so aus:

@Component
class MyComp extends Vue {
  greeting = 'Hello'
}

Ich möchte die Aufmerksamkeit auf @Component richten. Dieses Sprachkonstrukt wird Decorator genannt. Es dekoriert also eine Klasse, ein Attribut oder eine Methode. Während der Kompilierung eurer Dateien, wird dann vom Compiler der nötige Code hinzugefügt, um es als normales JavaScript ausführen zu können. Denn der Browser versteht nun mal kein TypeScript.

Wenn ihr mit Nuxt.js und Vue.js Projekte in Verbindung mit TypeScript umsetzen möchtet, dann müsst ihr einige dieser Decorators kennen. Ich habe dazu die wichtigsten in der Usage.md meines Stacks verlinkt.

Abgesehen davon finde ich, die Komponente wirkt aufgeräumter als zuvor. Zudem können die Komponenten mit gewohnter objektorientierter Programmierung entwickelt werden.

Zwei Modulsysteme

Eine Problematik, die mich zu Beginn einige Zeit beschäftigt hat, waren die verschiedenen Modulsysteme von Node.js und Nuxt.js bzw. von Server und Client Seite. Während Node.js für die Module auf CommonJS setzt, kommt im Frontend die EcmaScript 6 Version von Modulen zum Einsatz. Leider sind diese beiden Systeme nicht kompatibel und als ich mit der TypeScript Integration in den Stack begonnen habe, musste ich mich beim Kompilieren für eines der beiden entscheiden. Es hat sich aber schnell herausgestellt, dass das so nicht funktionieren kann.

Zum Glück bietet TypeScript die Möglichkeit, auch in Unterordnern eigene tsconfig Dateien anzulegen, die dann nach unten in der Ordnerstruktur vererbt werden. Letzten Endes gibt es also im Stack drei tsconfig Dateien. Eine Datei im server Ordner, die das CommonJS Modulsystem verwendet. Die zweite Datei im client Ordner, die das ES6 System verwendet. Die dritte und letzte Datei befindet sich im Hauptverzeichnis, da auch dort Konfigurationsdateien liegen, welche eine Übersetzung benötigen.

Vuex Stores

Was wäre ein größeres Vue.js Projekt ohne Vuex? Richtig, nicht viel, da es die Verwaltung des Anwendungszustands extrem vereinfacht.

Natürlich gibt es auch in Nuxt.js, welches in meinem Stack Verwendung findet, die Möglichkeit, Vuex Stores zu verwenden. Wie in der Nuxt Dokumentation zu diesem Thema beschrieben, gibt es die Möglichkeit, den Status auf mehrere Module zu verteilen und somit die Zuständigkeiten besser zu trennen.

Im Folgenden möchte ich auf zwei Feinheiten eingehen, die in Verbindung mit Nuxt.js und mehreren Store Modulen wichtig sind.

Persistenter Zustand

Ich habe in meinen Stack ein Plugin integriert, das den Zustand einer Anwendung über das Neuladen der Seite hinaus erhält. Da es oft nicht sinnvoll ist, den kompletten Store zu erhalten, könnt ihr wichtige Werte wie die Benutzerauthentifizierung in der entsprechenden Konfigurationsdatei plugins/vuex-persistent.ts registrieren. Hier ein Beispiel, mit je einem Wert aus dem zentralen und einem modulspezifischen Store. Ihr könnt nämlich auch beide Konzepte mischen.

paths: [
    'locale.current', // root state with embedded object value
    'list/count'      // module "list" state
]

Am Beispiel vom zentralen Store, seht ihr auch, dass auch Unterwerte möglich sind.

Die Funktion “nuxtServerInit”

Wenn ihr euch bereits ein wenig mit Nuxt.js beschäftigt habt, dann kennt ihr die Funktion nuxtServerInit sicherlich. Diese Funktion wird von Nuxt beim serverseitigen Rendern eures Projekts aufgerufen und dient der Initialisierung eures Zustands. In Verbindung mit Store Modulen, habe ich eine Lösung erarbeitet, wie ihr auch innerhalb dieser Module eine solche Funktion implementieren könnt. Ich finde es einfach sauberer, wenn jedes Modul auch für die eigene Initialisierung zuständig ist. Deshalb will ich euch kurz zeigen, wie ihr das auch umsetzen könnt.

Der Startpunkt für die Initialisierung ist dabei die Funktion nuxtServerInit innerhalb des zentralen Stores, die von Nuxt.js automatisch bei jedem Laden der Seite aufgerufen wird. Der darin befindliche Code, könnte in etwa so aussehen:

// to simulate async action in the root store
const p = new Promise((resolve, reject) => {
    resolve();
});

// use this for chaining init functions of store modules
return Promise.all([
    p,
    dispatch('list/nuxtServerInit', 20)
])

In diesem Zusammenhang ist es wichtig, zu wissen, dass wenn die Funktion ein Promise zurückgibt, Nuxt.js auf den Abschluss wartet, bevor die Seite erzeugt wird. In diesem Beispiel seht ihr gleich zwei Stellen, die ein Promise verwenden. Zum einen verwende ich ein eigens erstelltes Promise, um eine asynchrone Anfrage an einen Server zu simulieren, zum anderen verwende ich Promise.all um mehrere Promises zu einem neuen zusammenzufassen, das abgeschlossen ist, wenn alle anderen fertig sind. Häufig werden asynchrone Anfragen mit Axios durchgeführt, das in meinem Stack als Nuxt.js Modul registriert ist und das automatisch ein Promise zurückgibt.

Wie ihr in der vorletzten Zeile des obigen Beispiels seht, wird eine Aktion des Modul-Stores mit dem Namen list angestoßen. Dabei wird zu Demozwecken auch die Zahl 20 als Parameter übergeben. Die Aktion im zugehörigen Store, der dann unter store/list.ts definiert wäre, könnte zum Beispiel so aussehen:

export const actions = {
    nuxtServerInit(context, test) {
        return new Promise((resolve, reject) => {
            context.commit('test', test);
            resolve();
        });
    }
}

Beachtet, der Name der Aktion ist frei wählbar. Um aber klar zustellen, wozu sie verwendet wird, macht es Sinn, den gleichen Namen wie im zentralen Store zu verwenden. Die Variable test beinhaltet dabei die Zahl 20, die übergeben wurde. Auch hier ist wichtig, die Funktion muss ein Promise zurückgeben, um in der Hauptfunktion zu wissen, wann eine asynchrone Aktion abgeschlossen ist.

ElementUI

Ich habe mich bei meinem Stack dazu entschieden, ElementUI für das Frontend zu verwenden. Es bietet viele Standard HTML-Elemente als Vue.js Komponenten und damit erweiterten Möglichkeiten in Verbindung mit eigenen Komponenten an. Für genauere Definitionen der angebotenen Elemente verweise ich auf die Dokumentation, die wirklich sehr umfangreich ist.

Ich möchte an dieser Stelle aber kurz auf die richtige Verwendung innerhalb des Stacks eingehen, um Tree Shaking verwenden zu können.

Im Ordner plugins befindet sich eine Datei namens element-ui.ts. Dort müssen Komponenten registriert werden, die ihr in euren Projekten verwenden möchtet. Hier ein Beispiel:

import Vue from 'vue';
import {
    Button, Select
} from 'element-ui';

Vue.component(Button.name, Button);
Vue.component(Select.name, Select);

Dieser Code registriert einen Button und eine Selectbox zur globalen Verwendung innerhalb eures Projekts. Im fertigen Paket sind dann auch nur diese beiden Bestandteile enthalten und somit wird die Größe erheblich reduziert.

Solltet ihr bestimmte Elemente nur in einer einzelnen Komponente benötigen, könnt ihr diese auch direkt dort importieren. Das ist speziell mit Blick auf Code Splitting eine Erwähnung wert, das von Nuxt.js automatisch vorgenommen wird.

i18n und neue Sprachen

Der aktuelle Stack beinhaltet in der minimalen Oberfläche bereits eine Selectbox ohne Styling, um zwischen den vorhandenen Sprachen zu wechseln. Dabei wird beim Laden der Seite immer nur die Sprache geladen, die auch ausgewählt ist. Wird die Sprache umgestellt, wird die entsprechende Datei nachgeladen. Die aktuelle Sprache wird auch per Cookie und im LocalStorage des Browsers vorgehalten, um ohne sichtbares Flackern die eingestellte Sprache zu laden.

Um eine neue Sprache hinzuzufügen, reicht es, die entsprechende JSON Datei unter client/static/lang anzulegen und in zwei Dateien zu registrieren. Die erste Datei ist client/store/index.ts:

export const state = () => ({
    locale: {
        all: {
            de: "Deutsch",
            en: "English",
        },
        current: "",
        default: "de",
        // if you add new locales here, make sure you add it to nuxt.config.ts in wepack ContextReplacementPlugin config

    },
});

Dort wird die neue Sprache in das locale Objekt unter der Eigenschaft all eingefügt. An dieser Stelle kann auch die Standardsprache definiert werden, über die Eigenschaft default.

Die zweite Stelle befindet sich in der Registrierung von Nuxt.js Plugins, in der Datei nuxt.config.ts:

new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /en|de/)

Dort können neue Sprachen einfach über | gefolgt vom Kürzel eingefügt werden. Für die möglichen Sprachen, könnt ihr das GitHub Repository des Moment Projekts prüfen. Wichtig ist, die Kürzel an dieser und der oben genannten Stelle müssen übereinstimmen. Die Anpassung in dieser Datei ist wichtig, um auch von Moment.js nur die Sprachen zu laden, die benötigt werden. Leider ist es aktuell nicht möglich, die Sprache von Moment.js zu wechseln ohne die Seite neu zu laden.

FontAwesome 5

Über FontAwesome möchte ich nicht zu viele Worte verlieren. Im Grunde, handelt es sich um eine Bibliothek, die in der kostenlosen Variante über 1.000 Icons bietet. Um Tree Shaking nutzen zu können, ist eine spezielle Verwendung innerhalb der *.vue Dateien nötig.

Zuerst müsst ihr die gewünschten Icons einzeln importieren:

import { faAddressBook } from "@fortawesome/free-solid-svg-icons";

export default {
    computed: {
        fa() {
            // here you have to return all imported icons
            return {
                faAddressBook
            };
        }
    }
}

Danach könnt ihr die Icons mit vollem Funktionsumfang wie folgt nutzen:

<fa :icon="fa.faAddressBook" size="6x"></fa>

Beachtet, dass der Name innerhalb der computed Funktion und der Tag Name nur zufällig beide fa heißen. Wenn ihr möchtet, könnt ihr den Tag Namen in der Datei nuxt.config.ts anpassen:

"nuxt-fontawesome": {
    component: "fa",
    imports: [],
}

Hier seht ihr auch eine weitere wichtige Komponente für das Thema Tree Shaking. Die imports Eigenschaft ist standardmäßig leer. Das sorgt dafür, dass im Standard kein Icon geladen wird.

Fastify und API Endpunkte

Dieser Teil des Stacks kann optional dazu verwendet werden, eine API auf Basis von Fastify zu entwickeln. Manchmal ist es nicht nötig, da man nur das Frontend braucht und als Backend andere Dienste wie Firebase von Google oder ein Headless CMS wie Directus einsetzt. Aber auch in diesem Fall, kann es unter Umständen nötig sein, einen eigenen Server zu entwickeln. Immer dann, wenn ihr Passwörter bzw. API Zugriffsschlüssel verwalten müsst, sollten diese Daten nicht an die Clients geschickt werden. Stattdessen, legt ihr sie auf dem Server ab, macht einen Endpunkt in Fastify und schickt von dort dann die API Anfragen inkl. Passwort bzw. API Schlüssel.

Unter server/api/routes.ts seht ihr wie eine Route definiert ist. Wenn ihr eine umfangreiche API plant, dann solltet ihr die Routen in entsprechenden Unterdateien definieren. Diese müsst ihr in der Hauptdatei registrieren. Dazu habe ich ein auskommentiertes Beispiel eingefügt.

Hier ein relativ einfaches Beispiel, wie so eine zusätzliche Datei aussehen kann. Dabei handelt es sich um eine Login Methode, die ich für ein API Projekt benötigt habe:

const AuthController = require("../../controller/AuthController");

module.exports = async (fastify, opts) => {
    fastify.post("/login", {
        config: {
            jwt: fastify.jwt,
        },
        schema: {
            body: {
                properties: {
                    email: { type: "string", format: "email" },
                    password: { type: "string" },
                },
                required: ["email", "password"],
                type: "object",
            },
            description: "Login a user",
            response: {
                200: {
                    description: "Successfull login",
                    properties: {
                        token: { type: "string" },
                    },
                    type: "object",
                },
                400: {
                    description: "Invalid credentials",
                    type: "string",
                },
            },
            summary: "Login a user",
            tags: ["Auth"],
        },
    }, AuthController.login);
};

Der Vorteil an dieser sehr ausführlichen Routendefinition ist, dass ihr euch daraus unter anderem die entsprechende Dokumentation für die Endpunkte erstellen lassen könnt. Wer hier Interesse an weiteren Informationen hat, kann mich jederzeit per Kontaktformular, E-Mail oder über den neuen Slack Channel in der Fußzeile der Seite erreichen.

Testing

Das Testen eures Codes ist ein wichtiger Bestandteil eines jeden Projekts. In meinem Stack sind Testframeworks für Frontend und Backend integriert, inklusive Überprüfung der Testabdeckung. Für die Verwendung werft ihr am besten einen Blick in die Usage.md. Es gibt drei Kommandos, die für die Ausführung der Tests verwendet werden können:

# run client tests
npm run test-client

# run server tests
npm run test-server

# run both test suites
npm run test

Durch die Möglichkeit, Client und Server Tests getrennt auszuführen, müsst ihr nicht immer auf die Ausführung aller Tests warten.

Bonus: WebSockets und CI/CD

Zum Abschluss der Vorstellung meines Stacks, möchte ich noch kurz zwei Bestandteile erwähnen, die je nach Projekt sehr interessant sein können.

WebSockets

Wenn ihr ein Projekt macht, das mit Echtzeitdaten arbeitet, werdet ihr um WebSockets nicht herum kommen. In der Usage.md Datei findet ihr ein Beispiel, wie ihr WebSockets mit meinem Stack verwenden könnt. Damit könnt ihr beispielsweise Chats oder Nachrichtencenter Funktionalität mit Echtzeit Updates realisieren.

CI/CD

Die Themen Continous Integration und Continues Delivery sind gerade sehr aktuell. Auch ihr werdet euch mit fortschreitender Entwicklung eurer Projekte damit befassen müssen. Als Startpunkt, habe ich in der Usage.md Datei ein Beispiel veröffentlicht, das bei mir bereits erfolgreich in einem Projekt eingesetzt wurde.

Ich habe zu diesem Zeitpunkt noch GitLab verwendet und auf einen Dokku Server deployed. Bei jedem Git Push, werden dabei zuvor die Client- und Server-Tests ausgeführt und nach erfolgreichem Abschluss wird das Deployment gestartet. Ihr könnt das Ganze übersichtlich in der Oberfläche von GitLab verfolgen und es gibt auch Badges, die ihr in die Readme einbetten könnt, damit jeder, der euer Projekt anschaut, sofort sieht, was Sache ist.

Fazit

Ich hoffe, ich konnte euch einige Einblicke in meine Gedanken geben und euch dazu ermutigen mit dem Stack zu experimentieren. Auch wenn es manchmal ein paar Probleme gab, hat mir dieses Projekt sehr viel Spaß gemacht und immer wieder neue Herausforderungen beschert. Auch wenn ihr nicht den kompletten Stack verwenden wollt, gibt er euch doch viele Beispiele, wie bestimmte Pakete in das große Ganze eingefügt werden können. Ich werde versuchen die verwendeten Pakete in regelmäßigen Abständen zu aktualisieren.

Ihr könnt auch jederzeit selbst die Pakete überprüfen und aktualisieren lassen, falls ihr das in eurem Projekt wünscht. Führt dazu einfach folgendes Kommando aus:

npm run upgrade

Damit sucht ihr nach Aktualisierungen und könnt sie bequem auswählen, nachdem ihr überprüft habt, ob es Änderungen gibt, die eure Code beeinflussen.

Solltet ihr Fragen oder Anregungen haben, könnt ihr gerne auch meinem neuen Slack Channel beitreten, den ich direkt nach dem Serverumzug von Next Direction eingerichtet habe. Ihr findet den Link dazu in der Fußzeile.

Nun will ich euch aber nicht länger aufhalten. Ich hoffe, wir sehen uns im nächsten Beitrag.