Beliebte Suchanfragen
|
//

Spring und Vue - Ein Setup für kleine Projekte (Teil 1)

10.1.2025 | 7 Minuten Lesezeit

Mal eben für eine Spring-Boot Anwendung eine Weboberfläche mit Vue.js schreiben, sollte ein Google-lösbares Problem sein. Dachten wir. War dann doch nicht so. Mit der Kombination der richtigen Puzzleteile funktioniert es trotzdem.

Anforderungen

Ein RESTful-Service mit Spring-Boot ist schnell geschrieben. Mit Spring sind auch ein paar dynamische HTML-Seiten schnell ausgeliefert (siehe Serving Web Content with Spring MVC). Wir wollen aber ein modernes Frontend mit Vue.js bauen (man hätte auch Angular oder React nehmen können, das ist Geschmacksache). Im Rest des Blog-Posts gehen wir auf unsere Lösung ein. Grundlagen in Vue.js und Spring-Boot setzen wir voraus.
Weitere Anforderungen, für die unsere Lösung passen soll:

  • Eine überschaubare Anzahl von Seiten (< 100)
  • Überschaubare Skalierbarkeit notwendig (kein CDN etc.).
  • Einfaches Deployment: Ein direkt startbares jar mit kompletter Anwendung, kein Node.js, Reverse Proxy etc. auf dem Produktionsserver notwendig.

So wie Vue es vorsieht, soll es eine Single-Page Anwendung sein, ohne Reload bei jedem Klick. Trotzdem wollen wir in der Adresszeile „richtige“ URLs wie /users/4711 sehen, inklusive funktionierender Bookmarks. Vue und Spring kommunizieren über REST.
Sowohl bei der Entwicklung als auch später im Betrieb muss die Same Origin Policy eingehalten werden, ohne extra Header für CORS.
Und (hier kommt der spezielle Wunsch der Entwickler): Es soll in der Entwicklungsumgebung „schnell“ sein. Änderungen im Backend oder Frontend sollen im Browser ohne große Verzögerungen und ohne einen langwierigen Build wirksam werden. Für die Produktion soll der Build eine jar-Datei erzeugen, die sich per java -jar starten lässt.
Klingt wie die Quadratur des Kreises, ist aber lösbar.

Projektaufbau

Sowohl Spring/Maven/Gradle als auch Vue geben für ihren Teil einen Projektaufbau vor. Als übergreifendes Build-Tool nutzen wir Maven (mit Gradle wäre es auch ähnlich), daher gibt Spring die äußere Projektstruktur vor. Darin liegt unter src/vueapp das Vue-Projekt.
Die Spring Boot Anwendung lassen wir mit dem Spring-Initializr erzeugen (start.spring.io),
im src Verzeichnis folgt mit npm create vue@latest das Erstellen der Vue Anwendung im Verzeichnis src/vueapp. Dabei sollten TypeScript-Unterstützung und Vue-Router ausgewählt werden. Wir haben zusätzlich auch noch ESLint, Prettier und die Vue-DevTools ausgewählt. Mit den zwei erzeugten Vue-Routen und dazugehörenden Komponenten haben wir auch sofort einen Test für die Navigation und funktionierende Bookmarks.
Der Maven-Build ist für die Orchestrierung der einzelnen Schritte verantwortlich, sodass am Ende ein jar mit Vue und Spring Artefakten entsteht. Im ersten Schritt wird per Maven-Exec-Plugin der Vue-Build mit npm run build ausgeführt. Die Ergebnisse landen in src/vueapp/dist. Es handelt sich dabei um die komplette Vue-Anwendung, die so später von Spring Boot statisch ausgeliefert werden soll. Das maven-resources-plugin kopiert dazu das Verzeichnis src/vueapp/dist nach target/classes/static. Alles, was dort liegt, liefert Spring Boot aus. Der Rest des Build-Prozesses ist ein Spring Standard Build. Bis auf eine Ausnahme, den OpenAPI-Generator, aber darauf gehen wir im zweiten Teil ein.

Spring konfigurieren

Mit der automatisch erzeugten Vue Anwendung können wir nun die Spring Anwendung so konfigurieren, dass die statischen Dateien der Vue Anwendung korrekt ausgeliefert werden. Ohne weitere Konfiguration liefert Spring alle Dateien im static Verzeichnis aus. So wird zum Beispiel die Datei static/index.html unter localhost:8080/index.html ausgeliefert. Erzeugt man nun eine jar Datei und führt diese aus, kann man localhost:8080/index.html aufrufen und bekommt die Vue-Anwendung angezeigt. Jetzt kann über den angezeigten Link zur /about Seite navigiert werden, welche dann auch angezeigt wird. Lädt man die Seite jedoch neu, bekommt man allerdings eine Fehlermeldung. Bevor wir uns ansehen, wie Spring konfiguriert werden kann, dass diese beiden Dinge funktionieren, sollten wir uns kurz ansehen, wie eine Vue Anwendung strukturiert ist. Der Einstiegspunkt ist immer eine index.html Datei, die die Grundstruktur der Webseite enthält. Sie lädt zwei Dateien nach: ein Stylesheet und eine JS-Datei. Neben der index.html wird außerdem ein assets Verzeichnis erstellt, das alle Stylesheets und JS-Dateien enthält. Egal welche URL aufgerufen wird, es wird immer die index.html angezeigt und das dort geladene JavaScript lädt dann die zur URL passende Komponente und zeigt sie an.
Steigt ein Nutzer über einen Bookmark auf einer anderen Seite in die Anwendung ein (und es gibt dazu keine statische Ressource oder einen Spring-Controller), so muss immer der Inhalt der index.html ausgeliefert werden.
Standardmäßig prüft Spring zuerst die selbstgeschriebenen Controller und wenn keiner davon zuständig ist, folgt das static Verzeichnis. Dazu nutzt Spring standardmäßig den PathResourceResolver, welcher immer der letzte Resolver in der ResolverChain sein muss, da er nicht an den nächsten Resolver delegiert, wenn keine Datei gefunden wurde. Wenn wir den PathResourceResolver nach dem VueResourceResolver konfigurieren würden, müssten wir im VueResourceResolver prüfen, ob der Pfad zu anderen statischen Dateien aufgelöst werden kann, womit wir den halben PathResourceResolver nachbauen würden. Stattdessen erweitern wir den PathResourceResolver sodass dieser doch delegiert und registrieren diesen vor dem VueResourceResolver.

Ein Resource Resolver löst die URLs zu einem Resource Objekt auf, welches dann von Spring genutzt wird, um die darin enthaltene Datei als Response zurückzugeben. Der standardmäßig enthaltene Resolver von Spring ist der PathResourceResolver, der versucht die URL, bzw. den Pfad aus dieser im Dateisystem oder auf dem Classpath zu finden. Unser neuer VueResourceResolver gibt unabhängig vom Pfad immer dieselbe Ressource zurück.

Damit der VueResourceResolver funktioniert, müssen wir Spring noch beibringen, diesen überhaupt aufzurufen. Dazu legen wir die MvcConfig Klasse an; in der addResourceHandlers Methode wird der VueResourceResolver für alle möglichen Pfade konfiguriert.

1@Override
2public void addResourceHandlers(ResourceHandlerRegistry registry) {
3    registry.addResourceHandler("*", "**")
4            .addResourceLocations("classpath:/static/")
5            .setUseLastModified(true)
6            .setCachePeriod(3600)
7            .resourceChain(true)
8            .addResolver(new ChainedPathResourceResolver())
9            .addResolver(new VueResourceResolver(indexController));
10}

Leider funktioniert dieser Weg nicht, um auch für den Pfad / die index.html anzuzeigen, da für leere Pfade keine Resource Resolver aufgerufen werden. (siehe ResourceHandlerUtils.shouldIgnoreInputPath) Um dieses Problem zu lösen, erstellen wir einen neuen Controller mit einem GetMapping für /. In diesen verschieben wir das Laden der index.html und delegieren an diesen aus dem VueResourceResolver.

1@Value("classpath:/static/index.html")
2private Resource indexResource;
3
4@GetMapping(value = "/", produces = MediaType.TEXT_HTML_VALUE)
5@ResponseBody
6public Resource loadIndexHtml() {
7    return indexResource;
8}

Vue konfigurieren

Während der Entwicklung wollen wir die Vue Anwendung mit npm run dev starten und gleichzeitig die Spring Anwendung in der IDE starten. Gleichzeitig wollen wir, dass Änderungen im Code direkt sichtbar sind, ohne einen Build ausführen zu müssen.
Der mit npm run dev gestartete integrierte Server bringt einige nützliche Features für die Entwicklung mit, wie z.B. ein Autoreload, das bei Änderungen in der IDE automatisch die Webseite im Browser neu lädt, entweder teilweise oder vollständig, je nach Art der Änderung.
Wenn wir jetzt aus Vue unser Spring Backend aufrufen, kommt direkt eine Fehlermeldung, dass der Cross-Origin-Request blockiert wurde, weil er gegen die Same-Origin-Policy verstößt.
Doch was ist diese Same-Origin-Policy überhaupt? Diese Policy ist ein Sicherheitsmechanismus der Browser, das einschränkt, wie z.B. ein Skript, mit Ressourcen von anderen Origins interagieren kann. Dieser Mechanismus verhindert so zum Beispiel, dass ein bösartiges Skript auf ein Webmail Programm zugreifen kann, in dem du angemeldet bist, um so deine E-Mails zu lesen. Zwei URLs haben dann die gleiche Origin, wenn das Protokoll, der Port und die Domain (auch die Subdomain) gleich sind. Um trotzdem mit einer anderen Origin zu interagieren, muss diese das explizit erlauben. Das funktioniert allerdings nur bei einer anderen Domain oder Port, das Protokoll muss trotzdem gleich sein. Dazu setzt sie CORS Header in denen alle Domains (inklusive Port) vermerkt sind, die diese aufrufen dürfen.
Es gibt also zwei Möglichkeiten, um das Problem zu lösen:

  1. Spring CORS konfigurieren, so dass unsere Vue-Anwendung Cross-Origin-Anfragen stellen darf.
  2. Den bei Vue bzw. Vite eingebauten Proxy benutzen. Dabei ruft die Vue-Anwendung Spring nicht direkt auf, sondern den integrierten Server, der dann Requests an die Spring-Anwendung weiterleitet.

Für dieses Projekt haben wir uns für die zweite Variante entschieden, da sie einfach umsetzbar ist und der Proxy in der Produktion nicht aktiv ist.

1export default defineConfig({
2    [...]
3    server: {
4        proxy: {
5            '/api': {
6                target: 'http://localhost:8080',
7                changeOrigin: true,
8                secure: false,
9                ws: true,
10            }
11        }
12    }
13}

Und jetzt?

Nun haben wir eine Spring Anwendung die unsere Vue Anwendung ausliefert. Aber was jetzt? Im nächsten Teil dieses Artikels schauen wir uns an, wie man Spring und Vue verbindet, sodass Daten zwischen beiden Komponenten ausgetauscht werden können. Dazu werden wir eine OpenAPI Spezifikation anlegen, aus der wir automatisch Teile des Backends und Frontends generieren, um ein definiertes Interface zwischen beiden Teilen zu haben.
Das gesamte Projekt inklusive der Inhalte des zweiten Teils könnt ihr in unserem GitHub Repository finden.

|

Beitrag teilen

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.