Vue mit Typescript, Babel und Webpack

Ausgangslage

Javascript ist schwierig. Nicht weil man nicht weiss, wie etwas grundsätzlich zu tun wäre, aber bei der Vielzahl von Tools, Frameworks und Plugins fällt es schwer, das richtige Setup zu wählen, um sein Problem zu lösen. Und schlussendlich muss es möglichst einfach sein, denn ich möchte das Ganze auch in einem Jahr verstehen und anpassen können. Dies wird dann schwierig, wenn man eine fertige Lösung aus dem Internet als Ausgangslage verwendet. Aus diesem Grund baue ich hier alles von Grund auf selber auf.

Ich möchte eine Applikation bauen, die:

  1. Typescript verwendet, damit die Typsicherheit überprüft werden kann.
  2. Ich möchte überprüfen, dass sich mein Code an Standards und Regeln hält betreffend Variablennamen, Leerzeilen, gültige Javascript-Konstrukte und vieles mehr. Diese Prüfung wird "linting" genannt.
  3. Ich möchte Vue.js einsetzen, und dort möchte ich, dass der HTML-Code, der SCSS-Code und der Typescript Code je in einzelnen Files liegt. So kann ich die Module einfacher überblicken und habe Ordnung im Source Code.
  4. Natürlich möchte ich Klassen und Properties, Arrow-Functions und all die weitere schöne neue Funktionalität von Javascript verwenden, ohne die alten Browser komplett von meiner Applikation auszuschliessen.
  5. Ich möchte automatisiert testen und auch die Test-Coverage ausgeben.
  6. Für das Debugging und das Testing möchte ich SourceMaps verwenden können, damit ich sehe, wie mein Code im Browser verwendet wird.

Herausforderungen

Typescript -> Javascript -> Javascript für alle

Die Anforderung zur Verwendung von SourceMaps und die Unterstützung von älteren Browsern impliziert, dass mein Code nach dem Tippen des Quellcodes einen weiteren Prozess durchlaufen muss, der meine Applikation für den geplanten Einsatzzweck aufbereitet. Während mir beim Entwickeln die Grösse des Outputs ziemlich egal ist, so möchte ich in der Produktivumgebung kleinen, schnellen Code, der die User Experience maximiert. Beim Entwickeln brauche ich die SourceMaps und meinen Code möglichst nahe an der Form, wie ich ihn geschrieben habe. In der Produktivumgebung sollen Kommentare entfernt, Variablen umbenannt (damit sie wenig Speicher benötigen), überflüssiger Code entfernt (Tree Shaking) und nach Möglichkeit weitere Optimierungen durchgeführt werden.

Mein Code muss von Typescript nach Javascript umgewandelt werden, denn Browser verstehen nur Javascript. Zudem soll der Javascript Code so aussehen, dass beispielsweise auch der IE 11 ihn versteht. Nach dem Umwandeln von Typescript nach Javascript muss also ein weiterer Schritt dafür sorgen, dass mein Code die richtige Form für ältere Browser erhält.

CSS? SCSS!!

Ich möchte nicht von Hand CSS-Code schreiben, sondern auf den viel mächtigeren SCSS-Standard setzen, damit ich insbesondere Variablen verwenden( die im Bedarfsfall einfach und zentral geändert werden können) oder meine Konstrukte verschachteln kann.

Selbstverständlich möchte ich auch für mein umgewandeltes SCSS SourceMaps zur Verfügung haben, um bei Darstellungsfehlern möglichst straight forward den Grund ausfindig machen zu können.

Ansatz

Nun müsste also mein Code wie folgt verarbeitet werden:

  1. Vue-Komponenten müssen zusammengebaut werden:
    1. HTML aus dem Template übernehmen
    2. SCSS nach CSS konvertieren. Je nach Build-Ziel, für die Produktion minified und ohne unnötigen Ballast und für die Entwicklung mit SourceMaps.
    3. Typescript muss nach Javascript übersetzt werden. Danach muss das resultierende Javascript für ältere Browser aufbereitet werden. Auch hier: Für die Produktion schlank, minified und uglyfied, für die Entwicklung mit SourceMaps.
  2. Reine Typescript-Files funktionieren wie im Schritt "Typescript" bei den Vue Komponenten (oben).
  3. Die Index.html muss die generierten Scripts referenzieren.

Tools

Für jedes Problem gibt es Libraries

Für das Bundling ist die Wahl auf Webpack gefallen. Webpack, momentan in der Version 4, ist seit einigen Jahren der Quasi-Standard für diese Aufgabe. Er verwendet sogenannte Loaders, um meinen Source Code in die fertige Applikation zu verwandeln, die am Schluss aus 1-3 Javascript Files und einem index.html besteht.

Für Typescript besteht der Typescript-Compiler "tsc", der die Umwandlung von TypeScript nach Javascript vornimmt.

Für die Umwandlung von Javascript für ältere Browser empfiehlt sich die Verwendung von Babel.

Für die Umwandlung von SCSS nach CSS kommt ein Webpack-Loader zum Einsatz.

Als Tool für das Linting wird ESLint empfohlen.

Für das Testing habe ich bisher als Testrunner Karma verwendet.

Alles easy, oder?

Schwierigkeiten

Vue-Komponenten bauen

Für meine Vue-Komponenten muss ich einen Weg finden, um das TypeScript, das HTML und das SCSS untereinander zu verlinken - und dies in einer Art, die Webpack versteht und wieder mit Loaders kombinieren kann. Es gibt dafür z.B. den vue-template-loader den ich schon verwendet habe.

Reihenfolge von Webpack Loaders

Bei Webpack funkioniert alles über die sogenannten "loaders". Ich muss also konfigurieren, dass alle Schritte sequentiell durchgeführt werden:

  1. getrennte Loaders der einzelnen Files für die Vue-Komponente (bei Vue besteht eine Komponente aus dem Code, dem HTML und dem verwendeten CSS), Umwandeln (Typescript, SCSS) wie oben beschrieben.
  2. Umwandeln des Resultats in Javascript-Code, der auch für ältere Browser funktioniert.

Die Loaders sind aber einfach ein Array in der Webpack-Konfiguration. Loaders arbeiten mit bestimmten Filetypen (.ts, .scss, .html, usw.). Von Reihenfolgen und Prozessschritten steht da nichts. Man muss selber steuern, welche Loaders wann zum Einsatz kommen und für welche Files sie gestartet werden sollen. Dies tut man, indem man für die Loaders untereinander steuert, welcher an welcher Stelle im Build-Prozess seine Arbeit verrichtet (siehe "enforce": "pre").

Babel

Babel muss nach der Umwandlung von Typescript nach Javascript zum Einsatz kommen - nicht vorher und nicht später.

Dieser Loader muss so konfiguriert werden, damit mein gewünschtes Set an Browsern unterstützt wird. Welche Browser will ich unterstützen und welche Auswirkungen hat dies auf die Grösse meiner fertigen Applikation?

Lösung

Ich habe eine funktionierende Lösung mit den so gebauten Komponenten im Einsatz. Diese Lösung basiert auf dem vue-webpack-template in einer Version von vor ca. 3 Jahren. Sie ist kompliziert:

  1. In der Form, wie ich sie verwende, gibt es nicht weniger als 16 Files, die etwas mit Webpack und dessen Konfiguration zu tun haben.
  2. Die verschiedenen Aspekte wie SourceMaps, Uglyfy und Minify, Input- und Output-Directories, modes wie "development" und "production" sind quer über diese Files verteilt.
  3. Es ist nicht ohne genaustes Studium der Konfiguration möglich festzustellen, wann jetzt genau der TypeScript Kompiler und wann Babel zum Einsatz kommt. Wo stelle ich nun z.B. genau ein, für welche Browser mein Build funktionieren soll?

Ich kann diese Lösung nur schwer pflegen, auch weil ich die Funktionsweise nicht von Grund auf selber gebaut und verstanden habe. Es hat gereicht, alles durch "trial and error" für meine Bedürfnisse anzupassen.

Neuer Ansatz

Nun bin ich über einen interessanten Blog-Post gestolpert. Die Aussage darin ist: Babel und TypeScript passen wunderbar zusammen. Die Idee ist, nur noch den Babel-Compiler zu verwenden und damit die mühsame Konfiguration mit den Reihenfolgen loszuwerden. Das Type-Checking bietet Babel jedoch nicht, dafür wird nach wie vor tsc (TypeScript Compiler) verwendet. Das ist genau das, was ich will. Also habe ich es ausprobiert (Alle Commits und was sie ändern, ist unten beschrieben).

Babel mit Typescript und Webpack

Der erste Schritt war, Babel mit Typescript und Webpack einzurichten. Vue habe ich mit Absicht erst mal aussen vor gelassen, um den Build von Grund auf selber aufbauen zu können.

Erkenntnisse

  1. Man kann mit Babel TypeScript bauen (gemäss Dokumentation ab Babel Version 7)
  2. Das Type-Checking von TypeScript kann man trotzdem verwenden:
    Eintrag in der "scripts"-Section von package.json: "build:types": "tsc --emitDeclarationOnly". Somit kann man die Types prüfen, wann man will - ein weiterer Vorteil.
  3. Man sieht die Transformierung von TypeScript nach Javascript im Dateisystem. Dies war vorher immer im Prozess der Umwandlung durch Webpack verborgen.
  4. Mit Babel kann man eine Browserlist verwenden, die vorgibt, für welche Browser Babel das Javascript bauen soll. Man kann alle zu unterstützenden Browser einzeln auflisten. Ich habe aber nach einer weiteren Vereinfachung gesucht und wurde fündig: Man kann über einen Browserlist-String die Zielsysteme bestimmen. Der Eintrag
    "browserslist": "> 0.25%, not dead"

    in der package.json sorgt dafür, dass alle Browser mit einer Verbreitung von mehr als 0.25%, die noch supported sind, unterstützt werden. Das Tool Browserlist war sehr hilfreich.

Commit

https://github.com/Mcafee123/vue-starter/commit/d74ae8983ab69f06ebad89957a00a313de6232d7

webpack-dev-server

Dann wurde der Webpack Dev Server hinzugefügt, um die Resultate direkt im Browser anschauen zu können.

Erkenntnisse

  1. Das Plugin wird unter "plugins" angehängt und benötigt auch den html-loader.

Commits

https://github.com/Mcafee123/vue-starter/commit/fb237393498897dcb47d919f004c5006465e41db

https://github.com/Mcafee123/vue-starter/commit/9b01dcdcb8fec74ed18d77f785036109c6675609

Vue-Komponente mit HTML

Jetzt kommt der erste Vue Component. Dieser wird hinzugefügt, mit "Component" aus vue-property-decorator. Referenziert wird nur ein HTML-Template. Dies wird mittels dem "WithRender"-Decorator erreicht.

Erkenntnisse

  1. Es werden vue, vue-property-decorator, vue-template-compiler und vue-template-loader benötigt.
  2. Babel muss mit Decorators umgehen können, deshalb wird auch @babel/plugin-proposal-decorators hinzugefügt und in .babelrc referenziert (an erster Stelle). In tsconfig.json müssen experimentalDecorators eingeschaltet werden.
  3. Um Referenzen einfacher zu Vue-Komponenten und *.ts-Files hinzufügen zu können, müssen die tsconfig.json und die webpack.config.js die richtigen root-Pfade kennen.
    In tsconfig.json: "baseUrl": "./" und "paths": "*": [_alle_pfade_relativ_vom_project_root_].
    In webpack.config.js: resolve: modules: [_alle_pfade_mit_path.resolve_aufgelöst]
  4. Es wird der vue-template-loader bei den rules eingebunden. Die index.html darf dieser Loader nicht mitnehmen.
  5. Damit
    import WithRender from './App.html'

    in der Vue-Komponente keinen Fehler in Webstorm meldet, muss im root ein typings.d.ts eingefügt und ein module '*.html' definiert werden.

  6. Mit allen obigen Einstellungen lässt sich alles kompilieren, es ist aber noch nicht im Browser lauffähig. Die Templates sind das Problem; es erscheint ein Laufzeitfehler:
    vue.runtime.esm.js:620 [Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.
    Damit Webpack die Templates korrekt erkennt, muss in webpack.config.js folgendes eingefügt werden: resolve: alias: vue: 'vue/dist/vue.js'

Commit

https://github.com/Mcafee123/vue-starter/commit/2c14981c953ee5459a1caa6da2cc1968a0e4abad

[Update 24.4.2019]

In diesem Commit hatte sich ein Fehler eingeschlichen: Mit dem Entfernen von @babel/preset-env wurde die main.ts nicht mehr richtig transpiliert. Alle ".ts-Files, die nicht eine Vue-Komponente waren, wurden so nicht mehr richtig verarbeitet. https://github.com/Mcafee123/vue-starter/commit/8a5d7d9e5e3ba828e976bc4d1dc3a142ebed6fdc enthält die Korrektur. Zudem war die .babelrc nicht auf Github vorhanden. Die bessere Lösung ist somit, eine babel.config.js zu erstellen und zu committen: https://github.com/Mcafee123/vue-starter/commit/44bb76104b2eeed314e5f9d1ceeb4a71512442a5.

Vue-Komponente zusätzlich mit SCSS

Unser Vue-Component referenziert das SCSS File wie folgt:

import WithRender from './App.html?style=./App.scss'

Erkenntnisse

  1. Die Referenz muss von Babel ins js-File übernommen werden. Dann wird das SCSS von Webpack nach CSS transformiert und gebundelt.
  2. Um SCSS auf diesem Weg als gültige Referenz in den Vue-Component einfügen zu können, wird ein ähnliches Konstrukt wie für die HTML Templates verwendet: In typings.d.ts muss ein module für *.scss eingefügt werden.
  3. Damit Webpack das SCSS nach CSS transformieren und bundeln kann, werden folgende Packages benötigt: css-loader, node-sass, sass-loader, style-loader. Momentan wirft npm bei der Installation von node-sass folgenden schweren Fehler aufgrund einer Vulnerability:
    Das Problem ist also irgendwo bei node-gyp und der tar-Package anzusiedeln: https://github.com/sass/node-sass/issues/2625. Ich gehe davon aus, dass das Problem bald behoben sein wird.

  4. Die SCSS-Datei muss Code enthalten, sonst wird der Umwandlungs-Mechanismus von Webpack nicht angeworfen (da es nichts zu bundeln gibt).

Commit

https://github.com/Mcafee123/vue-starter/commit/8e1e6d5fa3019715fb3fb2bb48cd9f973eb331ae

Weitere Einstellungen

Das CSS war nicht "scoped" und wurde als style-Tag ins HTML eingefügt - so war nur "globales" CSS möglich, das für alle Komponenten gilt.

Was ich möchte ist jedoch, dass das CSS nur für die einzelne Komponente gilt, und dass ich ein globales SCSS File einfügen kann.

  1. der vue-template-loader erstellt scoped-CSS, wenn man in seinen options scoped: true setzt. Dann muss aber auch beim SCSS loader enforce: 'post' eingestellt werden!
  2. zum Extrahieren des CSS in ein eigenes File wird das Plugin mini-css-extract-plugin verwendet. Es muss als Webpack-Plugin registriert und anstelle des Loaders "style-loader" in der SCSS-Rule verwendet werden.

Commit

https://github.com/Mcafee123/vue-starter/commit/c9054ca61363641935c6c97351f5beb113d1b139

Konfiguration soll per Default einen produktiven Build durchführen, wenn man --env.development angibt, dann einen Debug-Build
  1. Die Konfiguration wurde auf zwei Files aufgeteilt: webpack.base.js und webpack.config.js. Per Default verwendet Webpack webpack.config.js, die intern webpack.base.js aufruft. Je nach dem, ob --env.development gesetzt ist oder nicht, wird unterschiedlich aufbereitet (dev oder prod).

Commit

https://github.com/Mcafee123/vue-starter/commit/5fa83364fa1d0c908b6c36b5165020e8df4daf45

Fazit

Die Umstellung des Build-Prozesses und der Aufbau von Grund auf haben mich rund zwei Arbeitstage gekostet. Es war schwierig, die gewünschte Funktionalität in den diversen Packages zu finden und das Zusammenspiel aufzusetzen.

Beurteilung Lösung

Mit der Lösung bin ich einigermassen zufrieden, bin aber sicher, dass sie noch weiter verbessert werden muss.

Positiv

  1. Einfache Konfiguration (zwei Files, klare Handhabung).
  2. TypeScript und Vue mit Class-Components und 3 verschiedenen Files funktioniert.
  3. Babel funktioniert und ich kann das neuste Javascript verwenden.
  4. Auch das SCSS kann extrahiert und minified und uglyfied werden.

Negativ

  1. Die SourceMaps funktionieren nicht. Ich kann meinen TypeScript Code nicht mehr anschauen und dahinter den Javascript-Code debuggen.
    [Update 23.4.2019]
    SourceMaps funktionieren! Es hatte ein kleiner Teil gefehlt: In der webpack.config.js muss ein devtool: 'source-map' eingetragen werden. Zumindest sehe ich jetzt den TypeScript Code im Source-Fenster der Chrome Developer Tools.
    Commit
    https://github.com/Mcafee123/vue-starter/commit/69082a216939cce9fe2388e27d5741a1e91ea4ba
  2. Da ich die SCSS Files scoped auf die jeweilige Komponente bauen will, musste ich trotzdem auf "enforce": "post" zurückgreifen und ich kombiniere für das SCSS eine chain von 3 Loaders.

Neutral

  1. Auf das Linting habe ich verzichtet. Erstens habe ich im Code-Editor (WebStorm) entsprechende Funktionalität und zweitens schreibt Babel meinen Code für die Applikation sowieso neu.
  2. Das Testing wurde noch nicht in Angriff genommen.
  3. Assets wie Bilder, Videos und Schriftarten werden noch nicht unterstützt.