Vue in Legacy-Systeme integrieren statt alles neu zu bauen
AI generated
<v/>
{ }
Vue.js · Legacy-Migration · PHP · jQuery · Twig
Vue in Legacy-Systeme integrieren
statt alles neu zu bauen

Ein Big-Bang-Rewrite scheitert häufiger als er gelingt. Die bessere Strategie: Vue schrittweise in bestehende PHP-Monolithen, Twig-Templates und jQuery-Anwendungen einführen – Insel für Insel, ohne den laufenden Betrieb zu unterbrechen.

16 Min. Lesezeit createApp · Custom Elements · Event-Bus · State-Bridging Vue 3 · PHP · jQuery · Twig · Vite

1. Warum ein Rewrite keine Lösung ist

Die Vue Legacy-Integration beginnt mit der Erkenntnis, dass ein vollständiger Rewrite in den meisten Projekten ein erhebliches Risiko darstellt. Joel Spolsky nannte es "das Schlimmste, was ein Software-Unternehmen tun kann": alles wegwerfen und neu schreiben. Der Grund liegt darin, dass in einer bestehenden Anwendung jahrelang akkumuliertes Domain-Wissen steckt – Bugfixes, Edge Cases, Sonderlogik für bestimmte Kunden – das im neuen System erst nach kostspieligen Incidents wiederentdeckt wird. Vue Legacy-Integration bedeutet stattdessen, dieses Wissen zu bewahren und die Frontend-Schicht schrittweise zu modernisieren.

Hinzu kommt der wirtschaftliche Aspekt: Während ein Rewrite-Projekt läuft, muss das bestehende System weiter gewartet werden. Zwei Codebasen parallel pflegen kostet mehr als eine. Bugs werden doppelt gefixt, Features werden doppelt implementiert, Entwickler sind gespalten zwischen altem und neuem Code. Die Vue Legacy-Integration umgeht dieses Problem, weil die bestehende Codebasis das Fundament bleibt und Vue-Komponenten als eigenständige Einheiten hinzugefügt werden, ohne die bestehende Logik zu berühren.

Die dritte Motivation ist die Risikoverteilung. Eine Vue Legacy-Integration kann jederzeit pausiert werden, ohne dass die Anwendung in einem halbfertigen Zustand verbleibt. Jede Vue-Insel, die fertiggestellt ist, bringt einen unmittelbaren Mehrwert in der Produktion. Das Team lernt schrittweise mit dem neuen Ansatz, statt nach einer langen Rewrite-Phase auf einmal in Production gehen zu müssen. Dieser inkrementelle Ansatz ist in regulierten Branchen, bei hoher Traffic-Last und in Teams mit heterogenem Wissensstand der einzig verantwortbare Migrationspfad.

2. Die Insel-Strategie: Vue schrittweise einführen

Das Kernkonzept der Vue Legacy-Integration ist die Insel-Architektur: Einzelne, abgegrenzte Bereiche der bestehenden Anwendung werden durch Vue-Komponenten ersetzt, während der Rest der Seite unverändert bleibt. Der Server rendert weiterhin das HTML-Grundgerüst, und Vue mountet sich in definierte Container-Elemente hinein. Eine typische erste Insel ist ein komplexes Formular, eine interaktive Suche oder ein Warenkorb-Widget – Bereiche, in denen jQuery-Spaghetti-Code am schmerzhaftesten ist und in denen eine Vue-Komponente den größten unmittelbaren Mehrwert bringt.

Die Auswahl der ersten Inseln für eine Vue Legacy-Integration sollte nach drei Kriterien erfolgen: Häufigkeit der Änderungen, Komplexität des bestehenden Codes und Isolierbarkeit vom Rest der Seite. Bereiche, die selten geändert werden und gut mit dem Rest der Seite verflochten sind, sind schlechte erste Kandidaten. Bereiche, die häufig neue Features bekommen, schwer zu testen sind und klar abgegrenzt werden können, sind ideal. Ein Suchwidget, das über eine REST-API kommuniziert und keine direkte DOM-Abhängigkeit zu anderen Seiten-Elementen hat, ist ein idealer Startpunkt für Vue Legacy-Integration.

3. Mount-Patterns: createApp für mehrere Widgets

In Vue Legacy-Integration-Szenarien ist createApp die zentrale Funktion. Im Gegensatz zu Vue 2, wo es nur eine globale Vue-Instanz gab, erlaubt Vue 3, mehrere unabhängige Applikationen auf derselben Seite zu mounten. Das ist für Legacy-Integration unverzichtbar: Jede Vue-Insel ist eine eigene createApp-Instanz mit eigenem Plugin-System, eigenem Provide/Inject-Kontext und eigenem State. Plugins, die in einer Instanz registriert sind, beeinflussen andere Instanzen nicht.

Das Initialisierungsskript für eine Vue Legacy-Integration scannt beim DOMContentLoaded-Event alle Elemente mit einem definierten Datenschema-Attribut und mountet die passende Vue-Komponente. Dieses Muster erlaubt es, mehrere Instanzen derselben Komponente auf einer Seite zu platzieren, ohne dass der Server wissen muss, welches Framework die Komponente rendert. Die Daten kommen als JSON in einem data-Attribut, die Vue-Komponente liest sie beim Mount und rendert sich vollständig clientseitig.


// src/legacy-init.js
// Entry point for Vue integration into legacy PHP/Twig pages
// Include via: <script src="/dist/legacy-bundle.js"></script>
import { createApp } from 'vue'
import SearchWidget from './components/SearchWidget.vue'
import CartWidget from './components/CartWidget.vue'
import ProductGallery from './components/ProductGallery.vue'

// Registry maps data-vue-component attribute values to Vue components
const COMPONENT_REGISTRY = {
  'search-widget': SearchWidget,
  'cart-widget': CartWidget,
  'product-gallery': ProductGallery,
}

// Mount all Vue islands found on the current page
function mountVueIslands() {
  const islands = document.querySelectorAll('[data-vue-component]')

  islands.forEach((el) => {
    const componentName = el.dataset.vueComponent

    if (!COMPONENT_REGISTRY[componentName]) {
      console.warn(`[Vue Legacy] Unknown component: ${componentName}`)
      return
    }

    // Pass server-rendered JSON as props via data-props attribute
    let props = {}
    if (el.dataset.props) {
      try {
        props = JSON.parse(el.dataset.props)
      } catch (e) {
        console.error(`[Vue Legacy] Invalid props JSON for ${componentName}`, e)
      }
    }

    const app = createApp(COMPONENT_REGISTRY[componentName], props)

    // Register shared plugins only once per island
    app.use(router)
    app.mount(el)
  })
}

// Wait for DOM, then mount
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', mountVueIslands)
} else {
  mountVueIslands()
}

4. Daten aus PHP und Twig in Vue übertragen

Der Datentransfer vom Server zu Vue-Komponenten ist in einer Vue Legacy-Integration einer der ersten Probleme, die gelöst werden müssen. Der einfachste Ansatz: PHP oder Twig rendert die initialen Daten als JSON in einem data-Attribut des Mount-Elements oder in einem versteckten script-Tag mit type="application/json". Vue liest dieses JSON beim Mount, nutzt es als initialem State und holt danach per API aktuellere Daten. Das vermeidet einen zusätzlichen API-Call für die erste Darstellung und ist besonders für SEO-kritische Daten wie Produktnamen und Preise relevant.

Für komplexere Vue Legacy-Integration-Szenarien bietet sich ein globales JavaScript-Objekt an, das PHP befüllt und Vue beim Start liest. Twig rendert window.__INITIAL_STATE__ = { { initial_state | json_encode | raw } }; im head, und das Vue-Entry-Skript importiert dieses Objekt und übergibt es als Provide-Wert an alle Vue-Inseln. Das entkoppelt die Datenübergabe vom Mount-Element und erlaubt, ganze Seiten-Context-Daten wie eingeloggten User, Währung oder Locale an alle Vue-Komponenten weiterzugeben, ohne jeden Mount-Container einzeln befüllen zu müssen.

5. jQuery-Koexistenz: Events und DOM teilen

In vielen Vue Legacy-Integration-Projekten ist jQuery noch aktiv und verwaltet Teile des DOM, die neben Vue-Inseln existieren. Die goldene Regel: Vue und jQuery dürfen nie dasselbe DOM-Element verwalten. Vue übernimmt vollständige Kontrolle über sein Mount-Element und alle Kinder. jQuery darf dieses Element weder lesen noch schreiben, ohne über eine definierte Schnittstelle zu gehen. Der sicherste Weg: Vue-Islands bekommen eigene Container-Elemente, die jQuery nicht kennt und nicht anfasst.

Kommunikation zwischen jQuery und Vue-Inseln in einer Vue Legacy-Integration läuft über native Browser-Events. jQuery feuert document.dispatchEvent(new CustomEvent('cart:updated', { detail: { count: 3 } })), und Vue-Komponenten lauschen in einem onMounted-Hook auf dieses Event. Umgekehrt feuert Vue Custom Events, die jQuery-Code empfangen kann. Dieses Muster ist explizit, debuggbar im Event-Monitor der Browser-DevTools und erzeugt keine direkte Abhängigkeit zwischen den Technologien. Es ermöglicht, jQuery-Code und Vue-Inseln unabhängig voneinander zu ersetzen, ohne die Schnittstelle zu ändern.


// Communication bridge between jQuery legacy code and Vue islands
// jQuery fires events, Vue listens — and vice versa — via native CustomEvents

// --- JQUERY SIDE (legacy code, untouched) ---
// jQuery triggers an event when cart changes (existing code, no modification needed)
// $(document).trigger('cart:updated', [{ itemCount: 3, total: '49,90 €' }])
// Translated to native CustomEvent for compatibility:
$(document).on('cart:updated', function (event, data) {
  document.dispatchEvent(new CustomEvent('vue:cart-updated', { detail: data }))
})

// --- VUE SIDE (CartWidget.vue) ---
// Listen for legacy jQuery events from the DOM
import { ref, onMounted, onUnmounted } from 'vue'

export function useCartBridge() {
  const itemCount = ref(0)
  const total = ref('')

  function handleCartUpdate(event) {
    itemCount.value = event.detail.itemCount
    total.value = event.detail.total
  }

  onMounted(() => {
    document.addEventListener('vue:cart-updated', handleCartUpdate)
  })

  onUnmounted(() => {
    document.removeEventListener('vue:cart-updated', handleCartUpdate)
  })

  // Vue fires events back to jQuery when Vue-side actions happen
  function notifyLegacy(eventName, detail) {
    document.dispatchEvent(new CustomEvent(eventName, { detail, bubbles: true }))
  }

  return { itemCount, total, notifyLegacy }
}

6. Vue als Custom Elements: framework-agnostisch einsetzen

Vue 3 unterstützt nativ die Kompilierung von Komponenten zu Web Components / Custom Elements. Das ist für Vue Legacy-Integration in polyglotten Umgebungen besonders interessant: Eine als Custom Element verpackte Vue-Komponente funktioniert in jedem HTML-Kontext – in PHP-Templates, in Twig, in serverseitig gerendertem Magento-HTML – ohne dass die Hostseite etwas über Vue wissen muss. Das Custom Element verhält sich wie ein natives HTML-Element, empfängt Attribute und Properties und feuert Events.

Der Hauptvorteil von Custom Elements in der Vue Legacy-Integration ist die vollständige Entkopplung: Die Hostseite referenziert ein JavaScript-Bundle und verwendet ein HTML-Tag. Ob die Implementierung dahinter Vue, React oder Vanilla-JavaScript ist, ist für die Hostseite irrelevant. Das ermöglicht, über mehrere Jahre hinweg die Implementierung schrittweise zu ersetzen, ohne die Templates anfassen zu müssen. Custom Elements haben aber auch Einschränkungen: SSR ist komplizierter, Styling erfordert Shadow DOM oder CSS Custom Properties, und die Prop-Übergabe funktioniert nur mit JSON-serialisierbaren Werten.

7. State-Management zwischen Vue-Inseln

Mehrere Vue-Inseln auf einer Seite müssen gelegentlich State teilen – der Warenkorb-Counter im Header soll aktualisiert werden, wenn eine Produkt-Karte den Warenkorb ändert. In einer Single-Page-Application ist das trivial, weil alle Komponenten im selben Vuex- oder Pinia-Store leben. In einer Vue Legacy-Integration mit separaten createApp-Instanzen teilen sich die Inseln keinen Store. Die Lösung: Ein geteilter Pinia-Store, der außerhalb aller createApp-Instanzen als Singleton instanziiert wird.

Das Muster für geteilten State in der Vue Legacy-Integration: Eine createPinia()-Instanz wird im Entry-Skript erstellt und jeder createApp-Instanz als Plugin übergeben. Da Pinia-Stores als Singletons funktionieren, teilen sich alle Vue-Inseln, die Pinia nutzen, denselben Store-State. Änderungen in einer Insel sind sofort in allen anderen sichtbar. Dieses Muster funktioniert auch mit dem reaktiven Mitt-Event-Bus als leichtgewichtige Alternative, wenn kein vollständiger State-Store benötigt wird, sondern nur Ereignisse zwischen Inseln ausgetauscht werden sollen.

8. Build-Integration mit Vite in bestehende Asset-Pipelines

Die Build-Integration ist in vielen Vue Legacy-Integration-Projekten technisch die größte Herausforderung. Bestehende PHP-Anwendungen nutzen häufig Webpack Mix, Gulp, Grunt oder einfache Concat-Skripte für Assets. Vite als moderner Build-Tool für Vue passt nicht automatisch in diese Pipelines. Der pragmatische Ansatz: Vite läuft als separater Build-Schritt, der das fertige Bundle in das bestehende Public-Verzeichnis schreibt. Die bestehende Asset-Pipeline wird nicht verändert – sie lädt das Vite-Bundle als reguläre JavaScript-Datei.

Für die Konfiguration in einer Vue Legacy-Integration ist die Vite-Konfiguration mit build.lib-Modus oder build.rollupOptions.input auf den Legacy-Entry-Point zu setzen. Das Ergebnis ist ein einzelnes JavaScript-Bundle, das alle Vue-Komponenten enthält und sich selbst initialisiert. Der Content-Hash im Dateinamen kann für Cache-Busting genutzt werden. Das Bundle wird in einer Twig- oder PHP-Partial-Datei eingebunden, die das Build-Manifest ausliest und den aktuellen Dateinamen rendert – ein Muster, das sich in Magento, Symfony und Laravel gleichermaßen etabliert hat.

9. Migrationsstrategien im Vergleich

Für eine Vue Legacy-Integration gibt es mehrere Strategien mit unterschiedlichen Aufwand-Nutzen-Profilen. Die Wahl hängt von der Größe des Teams, der Komplexität der bestehenden Anwendung und den verfügbaren Ressourcen ab.

Strategie Aufwand Risiko Geeignet für
Insel-Integration Gering (Widget für Widget) Minimal Monolithen, laufender Betrieb
Custom Elements Mittel Minimal Multi-Framework-Umgebungen
Strangler Fig Pattern Hoch Mittel Mittelfristige Vollmigration
Micro-Frontends Sehr hoch Mittel–Hoch Große Teams, Monorepos
Big-Bang-Rewrite Extrem hoch Sehr hoch Nur wenn Totalersatz unvermeidlich

Das Strangler-Fig-Pattern ist ein bewährter Ansatz für Vue Legacy-Integration auf mittlere Sicht. Routen werden schrittweise auf eine neue Vue-SPA umgestellt, während der Legacy-Server den Rest der Routen noch bedient. Ein Reverse-Proxy (nginx oder Caddy) entscheidet, welche Requests zur neuen SPA und welche zum alten Server gehen. So kann die Migration seitenweise erfolgen, ohne dass jemals ein "Feature Freeze" nötig ist. Nach einigen Monaten sind genügend Seiten migriert, dass der Legacy-Server abgeschaltet werden kann.

Mironsoft

Vue Legacy-Integration · Migration · Frontend-Architektur

Legacy-Frontend modernisieren ohne Risiko?

Wir analysieren bestehende PHP-, Twig- und jQuery-Anwendungen und entwickeln eine schrittweise Vue-Integrationsstrategie, die den laufenden Betrieb nicht unterbricht.

Analyse

Bestehende Anwendung evaluieren, Integrationspunkte identifizieren und Migrationspfad definieren

Umsetzung

Vue-Inseln implementieren, Build-Integration einrichten und jQuery-Brücken bauen

Übergabe

Team einarbeiten, Dokumentation und Patterns für eigenständige Weiterentwicklung

10. Zusammenfassung

Die Vue Legacy-Integration ist kein Entweder-Oder zwischen altem und neuem Stack, sondern ein gesteuerter Übergang, der den laufenden Betrieb respektiert. Das Insel-Muster mit mehreren createApp-Instanzen ermöglicht, Vue-Komponenten ohne Änderung an bestehenden PHP- oder Twig-Templates hinzuzufügen. Daten fließen über JSON-Attribute und globale JavaScript-Objekte vom Server zu Vue. jQuery und Vue koexistieren über native Browser-Events, ohne direkten DOM-Zugriff zu teilen. Custom Elements sind die stärkste Entkopplungsstrategie für polyglotte Umgebungen.

Der größte Fehler in einem Vue Legacy-Integration-Projekt ist, den Scope zu früh auszuweiten und in ein De-facto-Rewrite zu driften. Klare Grenzen – welche Teile der Seite sind Vue-verantwortlich, welche nicht – und eine konsequente Event-basierte Kommunikation halten die Integration wartbar. Mit jedem ersetzten jQuery-Widget wächst das Team in Vue hinein, ohne jemals den vollständigen Betrieb zu riskieren.

Vue Legacy-Integration — Das Wichtigste auf einen Blick

Insel-Muster

Mehrere createApp-Instanzen auf einer Seite – jede Insel ist isoliert, hat eigene Plugins und eigenen Provide/Inject-Kontext.

Daten-Bridging

PHP/Twig rendert JSON in data-Attribute oder window.__INITIAL_STATE__. Vue liest beim Mount, kein zusätzlicher API-Call für initiale Daten.

jQuery-Koexistenz

Native CustomEvents als Brücke – jQuery und Vue teilen nie dasselbe DOM-Element, kommunizieren nur über Events.

Geteilter State

Pinia-Singleton außerhalb aller createApp-Instanzen – alle Vue-Inseln teilen denselben Store ohne direkte Kopplung.

11. FAQ: Vue Legacy-Integration

1Vue 3 und jQuery auf derselben Seite?
Ja, solange beide nicht dasselbe DOM-Element verwalten. Kommunikation über native CustomEvents.
2PHP-Variablen an Vue übergeben?
data-props-Attribut mit JSON oder window.__INITIAL_STATE__ im head. Kein zusätzlicher API-Call für initiale Daten.
3Strangler Fig Pattern?
Routen schrittweise auf Vue-SPA umstellen. Reverse-Proxy leitet neue Routen zur SPA, alte zum Legacy-Server. Migration ohne Feature Freeze.
4State zwischen Vue-Inseln teilen?
Pinia-Singleton außerhalb createApp erstellen und jeder App als Plugin übergeben – alle Inseln teilen denselben Store.
5Custom Elements vs. Insel-Muster?
Custom Elements für polyglotte Umgebungen – funktionieren in PHP, Java, .NET ohne Framework-Kenntnisse in der Hostseite.
6Vite in Webpack-Pipeline integrieren?
Vite als separater Build-Schritt – schreibt Bundle ins bestehende Public-Dir. Bestehende Pipeline unverändert. PHP-Helper liest Vite-Manifest.
7Integration wird zum Rewrite – vermeiden?
Strenge Scope-Grenzen setzen. Keine Legacy-Logik in Vue duplizieren. Jede Insel vollständig abschließen, bevor die nächste beginnt.
8SSR mit Vue-Inseln in PHP-Templates?
PHP rendert initiale Daten als HTML, Vue hydratisiert beim Load. Echter SSR erfordert Node.js oder Nuxt als eigenständige Schicht.
9Vue-Inseln in Legacy-Umgebung testen?
Vue-Komponenten mit Vitest isoliert testen. HTML-Fixture für Integrationstests. Playwright oder Cypress für vollständige End-to-End-Tests.
10Vue 2 oder Vue 3 für Legacy-Integration?
Vue 3 – Composition API, mehrere createApp-Instanzen, native Custom-Element-Unterstützung. Vue 2 hat End-of-Life-Status.