© SkillUp/Shutterstock.com
TypeScript von A-Z - Teil 3

TypeScript am Client und am Backend


Wenn man sich für TypeScript entschieden hat, liegt die Frage nahe, warum man es nicht auch am Backend verwenden sollte. Dadurch ergeben sich vielfältige Synergien, ein einfacheres Application-Lifecycle-Management sowie eine breitere Know-how-Nutzung.

In den beiden ersten Teilen dieser Artikelserie standen TypeScript-Sprachfeatures im Vordergrund, die die Programmierung einfacher und fehlerresistenter gestalten. Im dritten und letzten Teil geht es um eine Nutzung von TypeScript am Client und am Server und darum, wie man dadurch Synergien, wie z. B. gemeinsame Libraries oder Domänenmodelle, nutzt.

Schnellkurs in serverseitigem TypeScript und Node.js

Einen groben Überblick über unsere Zielarchitektur finden Sie in Abbildung 1. Um TypeScript bzw. JavaScript ebenfalls am Backend oder browserless ausführen zu können, verwenden wir die Laufzeitumgebung Node. js [1]. basiert auf der Google V8 Engine und erweitert sie mit entsprechenden Libraries für serverseitige Programmierung. Node.js bietet ein Paket-/Modulsystem, den Paketmanager npm und eine Open Source Registry gleichen Namens [2]. TypeScript unterstützt das Node.js-Paket-/Modulsystem hervorragend, sodass man die TypeScript-Modul-Konzepte ("import"), die wir uns in den ersten beiden Artikeln angesehen haben, eins zu eins wiederverwenden kann. Ein Node.js-Paket entspricht der CommonJS-Konvention [3] und besteht aus einer Sammlung von JavaScript-Dateien. Den Einsprungpunkt in ein Paket bildet die Datei index.js, die bei Bedarf andere JavaScript-Dateien einbinden kann. Die Verzeichnisstruktur unserer Serveranwendung ist in Abbildung 2 beschrieben.

mahringer_typescript_teil3_1.tif_fmt1.jpgAbb. 1: Gesamtüberblick
mahringer_typescript_teil3_2.tif_fmt1.jpgAbb. 2: Strukturüberblick in Visual Studio Code

Folgende Elemente finden sich in npm Packages:

  • node_modules enthält die Node.js-Pakete, die unsere App oder unser Paket verwendet, z. B. Datenbanktreiber, Filesystemfunktionen usw.

  • package.json beschreibt die Abhängigkeiten unserer App von anderen Node.js-Paketen. Der Node Package Manager sorgt dafür, dass die benötigten Module in der richtigen Version im Verzeichnis node_modules installiert werden. In Listing 12 sehen Sie z. B. die Datei package.json unseres Backends. In der Sektion dependencies werden die Laufzeitabhängigkeiten beschrieben und in devDependencies die Abhängigkeiten, die nur zur Entwicklungszeit benötigt werden. Die Schreibweise @types/node bezeichnet ein „scoped Module“ und bedeutet in diesem Fall, dass die sogenannten TypeScript-Declaration-Files für Node.js benötigt werden. Sie enthalten die TypeScript-Typinformationen, anhand derer die IDE z. B. IntelliSense anbieten kann. Mittels des Kommandos npm install werden alle in package.json definierten Abhängigkeiten in node_modules installiert (standardmäßig von der npm Registry).

  • tsconfig.json enthält Einstellungen des TypeScript Transpilers, z. B. wohin die transpilierten JS-Dateien kopiert werden sollen, welcher ECMAScript-Standard und welches Modulsystem verwendet werden.

Kooperatives Multitasking

Node.js hat den Ruf, sehr effizient zu sein. Diese Effizienz kommt daher, dass es, wie bei clientseitigem JavaScript, nur einen Hauptthread in der Applikation gibt. Im Hintergrund, z. B. für IO-Aufgaben, verwendet Node. js natürlich mehrere Threads, aber die Applikation hat nur einen einzigen. Dadurch erspart sich das System den Aufwand laufender Thread-Switches. Kein Vorteil ohne Nachteil: Man muss sich im Fall von langlaufenden, nicht-asynchronen Aufgaben (z. B. langen Berechnungen, Parsen von Datensätzen eines Data Stores usw.) selbst darum kümmern, dass diese Tasks in kleinere asynchrone Tasks aufgeteilt werden. Aber dank des async/await-Konzepts ist das einfach.

Ein schlanker HTTP-Server

Für das Backend verwenden wir Express, einen sehr schlanken HTTP-Server. Anstatt, wie im letzten Beispiel, die Daten am Client durch einen Mock Data Loader zu generieren, werden wir diesmal am Backend dafür einen REST-Endpunkt bereitstellen.

Idee des gemeinsamen Moduls

Einer der Vorteile der Verwendung von TypeScript am Client und am Backend ist die Wiederverwendung von Libraries und des Domain Models. In unserem Fall ist das Domain Model sehr schlank, es besteht aus dem Interface ICustomer und der Klasse Customer. Wir verschieben sie nun vom Client in ein neues Paket, das wir am Client und am Server importieren werden. Das neue Modul hält sich an die npm-Package-Konvention: Es gibt dafür ein package.json inklusive der Indexdateien index.js und index.ts. Letztere dient TypeScript dazu, die TypeScript-Module innerhalb des Packages auflösen zu können.

Verwendung eines Node.js Packages in der Client-App?

Der aufmerksame Leser mag sich jetzt fragen, wie das funktionieren soll. Wir verpacken die gemeinsamen Funktionalitäten in ein gemeinsam verwendetes Node. js Package. Aber wie soll die App, die im Browser läuft, dieses verwenden können? Der Schlüssel dazu liegt bei webpack, einem Bundling-Tool, das clientseitige Artefakte (JavaScript-/TypeScript-Files, HTML-Files, CSS-Files usw.) in Bundles zusammenfasst. Der Hauptgrund ist das effizientere Laden der Artefakte durch den Server (auch bei HTTP 2.0). Ein netter Nebeneffekt ergibt sich dadurch, dass webpack die Node.js-Modulkonvention versteht und korrekt auflöst. Wie wir in Listing 4, 13 und 14 sehen, verwenden wir am Client und am Back-end dieselben Imports für unser gemeinsam verwendetes Modul.

Die Struktur der App im Gesamtüberblick

Abbildungen 3, 4 und 5 zeigen die Struktur der drei Komponenten unserer App in VS Code: Client, Backend und Common. Alle drei enthalten ein npm package. json, in dem ihre Abhängigkeiten definiert sind.

mahringer_typescript_teil3_3.tif_fmt1.jpgAbb. 3: Das am Client und am Backend genutzte Package
mahringer_typescript_teil3_4.tif_fmt1.jpgAbb. 4: Das Frontend
mahringer_typescript_teil3_5.tif_fmt1.jpgAbb. 5: Das Backend

Vorbereitung und Installation der Abhängigkeiten des Clients

Der Client erhält im Vergleich zum letzten Artikel zusätzliche Abhängigkeiten: erstens die von Common Package und zweitens die von webpack. Listing 1 zeigt das veränderte package.json.

Listing 1: package.json des Clients

{ "name": "tssampleclient", "version": "0.0.3", "description": "TypeScript Sample", "main": "not used for client app", "scripts": { "compileAndWatch": "tsc -w -p ./src", "devSrv": "webpack serve", "webPack": "webpack" }, "repository": { "type": "git", "url": "git-url" }, "keywords": [ "ts01" ], "author": "thomas mahringer", "license": "ISC", "dependencies": { "tssamplecommon": "file:../common" }, "devDependencies": { "typescript": "^4.1.3", "ts-loader": "^8.0.18", "webpack": "^5.26.0", "webpack-cli": "^4.5.0", "webpack-dev-server": "^3.11.2" } }

Listing 2: webpack.config.js

var path = require("path"); module.exports = { resolve: { extensions: ['.ts', '.tsx', '.js'] }, mode: "development", entry: "./src/app.ts", output: { filename: "bundle.js", path: path.resolve(__dirname, "dist"), publicPath: "/dist/" }, module: { rules: [ { test: /\.tsx?$/, loader: "ts-loader" } ] }, devServer: { index: "index.html", stats: { assets: false, hash: false, chunks: false, errors: true, errorDetails: true, }, overlay: true } };

Als Erstes fällt auf, dass die Abhängigkeit vom Common Package (tssamplecommon) als local dependency ("tssamplecommon": "file:../common") angegeben wird. Dadurch ersparen wir uns während der Entwicklung die Mühe, das Package immer wieder neu zu paketieren und auf eine Registry hochzuladen. Ein Aufruf von npm install führt dazu, dass

  • erstens webpack...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang