triplecore-logotriplecore
  Zurück zur Blog-Übersicht

Integration einer Vue 3 Component Library

Trotz einer enormen Vielfalt an modernen Web-Technologien basiert ein großer Teil des Internets auf dem LAMP Stack (Linux, Apache, MySQL, PHP). Auch jQuery ist nach wie vor weiter verbreitet als manch ein Gespräch zwischen Frontend-Entwicklern vermuten lässt. Viele Unternehmen möchten schrittweise auf moderne Technologien umsteigen, um weiterhin neue Features releasen zu können.

Stellen wir uns vor...

... wir betreiben einen digitalen Book Store. Dieser basiert technisch auf PHP, clientseitig wird jQuery eingebunden. Unseren Book Store haben wir ca. 2013 erstellt. Seitdem haben wir unsere Anwendung nicht komplett verwahrlosen lassen, sondern haben PHP, jQuery und unsere weiteren Dependencies regelmäßig aktualisiert.
Aber natürlich gibt es die ein oder andere Stelle in unserem Code, die irgendwie historisch gewachsen ist.

Unsere langjährigen Mitarbeiter finden sich gut im Code unseres Book Stores zurecht. Im letzten Jahr haben wir eine neue Kollegin eingestellt, die noch immer ein wenig kämpft, wenn sie Anpassungen in älteren Code-Stellen vornehmen muss, aber es wird besser. Vor Kurzem haben wir einen neuen Mitarbeiter eingestellt, der uns aber leider in der Probezeit schon wieder verlassen hat. Obwohl wir die Einarbeitung sehr wichtig finden, ist es doch immer ärgerlich, wie viel Zeit dabei drauf geht.

Wir würden auch gerne einen moderneren Ansatz in unserer Software-Entwicklung verfolgen, sind aber besorgt, welchen Migrationsaufwand das mit sich bringt. Es ist uns wichtig, dass wir unseren Kunden regelmäßige Updates zur Verfügung stellen. Geht das überhaupt, wenn wir gleichzeitig ein großes Refactoring umsetzen?

So oder so ähnlich könnte die Geschichte vieler Unternehmen erzählt werden, die zwar die Vorteile vom Einsatz moderner Technologien erkannt haben, aber noch nicht so recht wissen, wie sie den Umbau ihres Produkts konkret gestalten können.

In diesem Artikel stellen wir ein mögliches Vorgehen vor, mit dem Vue-Komponenten in eine bestehende Anwendung integriert werden können. Es werden dabei Kenntnisse in Vue 3 vorausgesetzt. Vue bietet sich in diesem Kontext an, weil es sehr leichtgewichtig ist und die Möglichkeit bietet, mehrere autarke Apps auf einer Seite zu laden. Wir möchten, dass sich diese Apps eine Vue-Instanz teilen, um die Last für den Client zu verringern. Mit Hilfe eines geteilten Stores ist es zudem möglich diese Apps miteinander zu verbinden. Dieses Vorgehen orientiert sich am Konzept von Micro-Frontends.

Entwicklung der Component Library mit Vue

Zwar wäre es mit geringem Aufwand möglich, die Vue-Komponenten direkt im bestehenden Projekt zu entwickeln und zu laden, aber wir möchten diese trotzdem in ein separates Repository auslagern. Das bringt einmalig ein wenig mehr Konfigurationsaufwand mit sich, aber der psychologische Effekt
von einem frischen Start mit lauter guten Vorsätzen für sauberen Code und der Möglichkeit, den Best-Practices zu folgen, ist nicht zu unterschätzen. Also legen wir mit dem Vue-CLI ein neues Projekt an.

1 vue create $PROJECT_NAME

Im folgenden Setup-Prozess entscheiden wir uns dafür, Vue 3, Vuex und TypeScript zu verwenden. Nun erstellen wir zwei Komponenten, die wir später als eigenständige Anwendungen in ein externes Projekt einbinden wollen. Die beiden Komponenten sollen über einen Vuex Store Daten austauschen können. In unserem Anwendungsfall ist es einfacher, die Options API zu verwenden, weil wir so den Store besser auslagern können – dazu später mehr.

1<template>
2 <div>
3 <h2>First Example</h2>
4 <h3>Anzahl der Einträge im Store:</h3>
5 <p>{{ items.length }}</p>
6 <button @click="add">Add Item to store</button>
7 </div>
8</template>
9
10<script lang="ts">
11import { defineComponent } from "vue";
12
13export default defineComponent({
14 name: "FirstExample",
15 computed: {
16 items(): string[] {
17 return this.$store.state.items;
18 }
19 },
20 methods: {
21 add() {
22 this.$store.commit("ADD", "from first app - " + new Date().getTime());
23 }
24 }
25});
26</script>
27
28<style scoped>
29div {
30 padding: 1rem 2rem;
31 font-family: sans-serif;
32 box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
33 background: #7f2a68;
34 color: white;
35}
36</style>

Die beiden Komponenten können dann in der main.ts jeweils für einen unterschiedlichen Container gemountet werden.

Soweit funktioniert die Anwendung in unserem Vue-Projekt nun. Die main.ts bindet zwei Apps ein und sorgt dafür, dass beide mit einem Store kommunizieren können. Wir könnten unsere Anwendung nun bauen und in einem anderen Projekt einbinden. Beide Apps wären in der Ziel-Anwendung zu sehen. Wir möchten den Konsumenten unserer Apps (also z.B. dem Book Store basierend auf PHP) aber die Möglichkeit geben, einzelne Apps einzubinden und andere nicht. Eine Möglichkeit das zu tun, bietet das Vue CLI mit dem Build Target lib. Hiermit können wir die Einstiegspunkte unserer Apps, also unsere beiden Komponenten, als einzelne Bundles erstellen, die der Konsument getrennt laden und mounten kann. Zusätzlich können wir noch einen Adapter für das Erstellen und mounten unserer Apps zur Verfügung stellen, damit das konsumierende Projekt keine direkte Abhängigkeit zu Vuex haben muss. Vue selbst muss als Dependency an das Ziel-Projekt übergeben werden bzw. dort geladen werden.

Der Adapter kann wie folgt implementiert und zum Testen in der main.ts verwendet werden.

1import { createApp, Component } from "vue";
2import store from "./store";
3
4export default {
5 createApp: (component: Component) => {
6 const app = createApp(component);
7 app.use(store);
8 return app;
9 }
10};

Bundles erstellen

Wir möchten nun unsere beiden Komponenten als einzelne Bundles bauen. Dafür können wir die Build Target Optionen des Vue-CLIs verwenden. Leider müssen wir für den Export, strikte TypeScript-Checks ausstellen, da TypeScript den Type der Komponenten nicht richtig auflösen kann. Wahrscheinlich gibt es hierfür eine sauberere Lösung. Wir freuen uns über Feedback dazu!
Die Bespiel-Komponenten können mit folgendem Befehl als Libraries gebaut werden.

1 vue-cli-service build --target lib --no-clean --name firstExample --mode production src/components/FirstExample.vue
2 vue-cli-service build --target lib --no-clean --name secondExample --mode production src/components/SecondExample.vue

Wenn dieser Befehl im Terminal ausgeführt wird und cli-service-global nicht zuvor global installiert wurde, kann npx helfen. Der Befehl lautet dann z.B. npx vue-cli-service build --target lib --name firstExample src/components/FirstExample.vue.

Bei Libraries wird Vue standardmäßig nicht mitgebundlet, das ist für die beiden Komponenten genau das was wir wollen. Es wäre schön, wenn nur unser Adapter Vue integrieren würde, und so unser Konsument keine Abhängigkeit von Vue benötigen würde. Leider ist das zum aktuellen Zeitpunkt noch nicht möglich. Den Adapter bauen wir also analog zu unseren Komponenten mit diesem Befehl:

1 vue-cli-service build --target lib --no-clean --name adapter --mode production src/lib-adapter.ts
Achtung 

Achtet darauf, dass ihr mindestens Version 3.0.4 von Vue verwendet. In früheren Versionen gibt es einen Bug beim Bauen der Libraries, der dazu geführt hat, dass es mehrere Vue-Instanzen auf der Seite gab, wodurch die Komponenten nicht gerendert werden konnten. Dann sind folgende Fehler in der Konsole aufgetaucht:

resolveComponent can only be used in render() or setup()
oder auch
Invalid VNode type: Symbol(Fragment) (symbol)

Das Build-Script erzeugt jede Lib in den Formaten common.js, umd.js und umd.min.js. Zusätzlich werden vorhandene Styles in
separate Files geschrieben. Wir können nun die erzeugten Dateien aus dem dist Ordner verwenden, um sie in unserem externen Projekt einzubinden. Diese können natürlich auch über eine Package Registry verwaltet und konsumiert werden, aber das ist nicht Bestandteil dieses Artikels.

Die Libraries verwenden

Bevor die so gebauten Libraries über npm im Ziel-Projekt hinzugefügt werden, können sie in Form eines Proof of Concept manuell in das Projekt kopiert werden. Wenn kein module bundler im Ziel-Projekt genutzt wird, können die erzeugten umd.min.js-Dateien per Script-Tag eingebunden werden. Zusätzlich muss Vue geladen werden.

Ein einfaches Beispiel für das Einbinden der Libraries über ein Script-Tag sieht so aus:

1<!DOCTYPE html>
2<html lang="en">
3 <head>
4 <meta charset="utf-8">
5 <link rel="stylesheet" href="/lib/firstExample.css">
6 <link rel="stylesheet" href="/lib/secondExample.css">
7 </head>
8 <body>
9 <div id="first-app"></div>
10 <div id="second-app"></div>
11
12 <script src="https://unpkg.com/vue@3.0.4"></script>
13 <script src="/lib/adapter.umd.min.js"></script>
14 <script src="/lib/firstExample.umd.min.js"></script>
15 <script src="/lib/secondExample.umd.min.js"></script>
16 <script>
17 const adapter = window.adapter.default;
18 adapter.createApp(secondExample).mount("#second-app");
19 adapter.createApp(firstExample).mount("#first-app");
20 </script>
21 </body>
22</html>

Styles und Script werden einzeln geladen. Das Zusammensetzen der Apps funktioniert genau wie in der main.ts in unserem Vue-Projekt.

Viel Erfolg mit eurem Migrations-Projekt! 👍

eye candy
Kristina Mergenthaler - 14.12.2020