Vue-Komponenten als Custom Elements exportieren: defineCustomElement() in der Praxis
AI generated
{ }
Vue 3 · Web Components
Vue-Komponenten als Web Components exportieren
defineCustomElement(), Shadow-DOM-Kapselung und die Grenzen gegenüber normalen Vue-Komponenten

Mit defineCustomElement() lässt sich jede Vue-Komponente in ein echtes, standardkonformes Custom Element verwandeln, das sich wie ein natives HTML-Element in beliebigen Umgebungen einsetzen lässt, auch ohne dass die umgebende Anwendung selbst Vue verwendet. Dieser Artikel zeigt die API im Detail, wie Shadow-DOM-Kapselung Styling-Konflikte verhindert und wo die Einschränkungen gegenüber einer normalen Vue-Komponente liegen.

15 Min. Lesezeit Vue 3 Web Components

1. Was Custom Elements als Web-Standard eigentlich sind

Custom Elements sind Teil der Web-Components-Spezifikation und erlauben es, eigene HTML-Tags zu definieren, die sich anschließend genau wie eingebaute Elemente wie div oder button im Markup verwenden lassen. Ein Custom Element wird über customElements.define('mein-tag', KlassenDefinition) registriert, wobei die Klasse von HTMLElement erben muss und der Tag-Name laut Spezifikation zwingend einen Bindestrich enthalten muss, um Kollisionen mit zukünftigen nativen HTML-Elementen auszuschließen.

Sobald ein Custom Element registriert ist, kümmert sich der Browser selbst um dessen Lebenszyklus, ruft also automatisch Callback-Methoden wie connectedCallback auf, sobald das Element ins DOM eingefügt wird, und disconnectedCallback, sobald es wieder entfernt wird. Vue nutzt genau diese Mechanik, um eine ganz normale Vue-Komponente hinter einer solchen Klasse zu verstecken, ohne dass der Rest der Anwendung, die das Element einbindet, jemals etwas von Vue mitbekommt.

2. Die defineCustomElement() API im Detail

defineCustomElement nimmt eine gewöhnliche Vue-Komponente, entweder als Options-API-Objekt oder als Ergebnis von defineComponent, entgegen und erzeugt daraus eine Klasse, die von HTMLElement erbt und intern eine vollständige Vue-Instanz verwaltet. Diese zurückgegebene Klasse wird anschließend ganz normal über customElements.define bei einem Tag-Namen registriert, danach kann das Element überall im Dokument per Markup oder per document.createElement verwendet werden.

Der folgende Ausschnitt zeigt eine kleine Produkt-Badge-Komponente, die als eigenständiges Custom Element registriert wird. Die Datei-Endung .ce.vue ist eine reine Namenskonvention, die signalisiert, dass diese Komponente für den Custom-Element-Export gedacht ist, technisch notwendig ist sie nicht, hilft aber dabei, im Projekt zwischen normalen Vue-Komponenten und exportierten Elementen zu unterscheiden.


// ProductBadge.ce.vue kompiliert und importiert:
import { defineCustomElement } from 'vue'
import ProductBadge from './ProductBadge.ce.vue'

const ProductBadgeElement = defineCustomElement(ProductBadge)

customElements.define('product-badge', ProductBadgeElement)

3. Shadow-DOM-Kapselung und Styling-Isolation

Jede mit defineCustomElement erzeugte Instanz rendert standardmäßig in einen eigenen Shadow-Root, ein vom Browser bereitgestelltes, gekapseltes DOM-Fragment, dessen Inhalte von außen weder per CSS-Selektor noch versehentlich per globalem Stylesheet erreicht werden können. Styles, die innerhalb der SFC im style-Block der Komponente definiert werden, wandert Vue automatisch in genau diesen Shadow-Root hinein, statt sie wie bei einer normalen Komponente global ins Dokument einzufügen.

Diese Isolation löst eines der größten praktischen Probleme beim Einbetten von Komponenten in fremde Seiten: CSS-Klassennamen-Kollisionen mit dem Host-System sind praktisch ausgeschlossen, weil weder das Custom Element von außenliegenden globalen Styles beeinflusst wird noch die eigenen Styles der Komponente nach außen dringen. Eine gezielte Ausnahme bilden CSS-Custom-Properties, die per Definition die Shadow-Grenze durchdringen und sich damit hervorragend eignen, um dem Host-System begrenzte, kontrollierte Theming-Möglichkeiten zu geben.

4. Anwendungsfall: Einbettung in eine Nicht-Vue-Anwendung oder ein Legacy-System

Der klassische Anwendungsfall für Custom Elements ist die Situation, in der eine moderne Vue-Komponente in einer Umgebung eingebettet werden muss, die selbst nicht auf Vue basiert, etwa ein älteres jQuery-basiertes Backend, ein von einem anderen Framework verwaltetes Frontend, oder ein reines Server-gerendertes PHP-System wie Magento mit einem klassischen Luma-Theme. Statt die komplette Zielanwendung auf Vue umzustellen, reicht es, das kompilierte JavaScript-Bundle des Custom Elements einzubinden und den Tag an der gewünschten Stelle im bestehenden Markup zu platzieren.

Aus Sicht der einbindenden Anwendung verhält sich das Element wie jedes andere HTML-Element auch: es kann per Standard-DOM-API angesprochen, per Attribut konfiguriert und in beliebige bestehende Layouts eingefügt werden. Diese Kapselung macht Custom Elements besonders attraktiv für Design-System-Bausteine, die von mehreren, technisch unterschiedlichen Projekten gleichzeitig genutzt werden sollen, ohne dass jedes Projekt dieselbe Frontend-Technologie einsetzen muss.

5. Props und Events über die Element-Grenze hinweg

Einfache, primitive Props wie Strings oder Zahlen lassen sich bei einem Custom Element ganz normal als HTML-Attribut setzen, etwa product-badge label="Neu", Vue kümmert sich automatisch um die Umwandlung zwischen Attribut-String und dem tatsächlich in der Komponente erwarteten Typ. Komplexere Props wie Arrays oder Objekte lassen sich dagegen nicht sinnvoll als String-Attribut abbilden und müssen stattdessen als DOM-Property direkt per JavaScript gesetzt werden, also element.items = [...] statt eines Attributs.

Events, die eine Vue-Komponente intern per emit auslöst, werden bei einem Custom Element automatisch in native CustomEvent-Objekte übersetzt und auf dem Host-Element ausgelöst. Der umgebende, nicht-Vue-Code kann diese ganz normal per addEventListener abonnieren, ohne irgendetwas über Vue oder dessen internes Event-System wissen zu müssen, was die Integration in beliebige bestehende Codebasen deutlich vereinfacht.

6. Die Einschränkung bei Provide/Inject über Element-Grenzen

Eine der wichtigsten Einschränkungen gegenüber einer normalen Vue-Komponente betrifft Provide und Inject: jedes per defineCustomElement erzeugte Element verwaltet standardmäßig seine eigene, isolierte Vue-App-Instanz, wodurch über provide bereitgestellte Werte aus der umgebenden Anwendung nicht automatisch bei verschachtelten Custom Elements ankommen, selbst wenn diese im DOM ineinander verschachtelt sind. Jedes Custom Element ist in dieser Hinsicht eine eigene, geschlossene Vue-Welt.

Für Fälle, in denen mehrere eigene Custom Elements innerhalb derselben Anwendung tatsächlich denselben Provide/Inject-Kontext teilen sollen, bietet defineCustomElement einen optionalen zweiten Parameter, über den sich eine gemeinsame App-Konfiguration inklusive Plugins und globalem Provide für mehrere Elemente gemeinsam definieren lässt. Diese Lösung funktioniert allerdings nur innerhalb einer einzigen, kontrolliert gebauten Vue-Anwendung und nicht über die Grenze zu vollständig fremdem, nicht-Vue-Code hinweg.

7. Asynchrone Custom Elements für Code-Splitting

Für größere oder seltener genutzte Komponenten lässt sich defineCustomElement mit defineAsyncComponent kombinieren, sodass der eigentliche Komponenten-Code erst nachgeladen wird, sobald das Custom Element tatsächlich zum ersten Mal ins DOM eingefügt wird. Das Host-Element selbst, also die minimale Klassen-Definition, wird sofort synchron registriert, während der schwergewichtige Rest per dynamischem Import erst bei Bedarf nachgeladen wird.

Dieses Muster eignet sich besonders gut für eine Bibliothek mit mehreren Custom Elements, die gemeinsam als ein einziges kleines Basis-Skript ausgeliefert wird: nur die Elemente, die tatsächlich auf einer Seite verwendet werden, laden ihren vollen Code nach, während nicht genutzte Elemente das Ladevolumen der Seite nicht unnötig aufblähen.

8. Build-Tooling für eine Custom-Element-Bibliothek

Für den Bau einer eigenen Bibliothek aus mehreren Custom Elements bietet sich der Library-Modus von Vite an, konfiguriert über build.lib mit einem Entry-Point, der alle gewünschten Elemente registriert. Wichtig ist dabei, cssCodeSplit auf false zu setzen beziehungsweise sicherzustellen, dass Vue die Styles jeder Komponente direkt inline in deren Shadow-Root einbettet, statt eine separate, global eingebundene CSS-Datei zu erwarten, die in einer fremden Zielumgebung möglicherweise gar nicht geladen wird.

Bei der Namensgebung der Tags lohnt sich ein konsistentes Präfix für die gesamte Bibliothek, etwa ms-product-badge oder ms-price-tag, um Kollisionen mit Custom Elements aus anderen Bibliotheken oder zukünftigen nativen HTML-Elementen von vornherein auszuschließen. Die Web-Components-Spezifikation selbst erzwingt lediglich den Bindestrich, ein durchdachtes eigenes Präfix bleibt trotzdem empfehlenswert.

9. Einschränkungen gegenüber normalen Vue-Komponenten und Fazit

Neben der bereits erwähnten Provide/Inject-Grenze gibt es weitere praktische Einschränkungen: die Vue-DevTools bieten für Custom Elements deutlich weniger Einblick als für normale Komponenten innerhalb einer regulären Vue-App, Teleport-Ziele außerhalb des eigenen Shadow-Roots verhalten sich teils unerwartet, und serverseitiges Rendern von Custom Elements ist technisch aufwendiger als klassisches Komponenten-SSR, weil der Shadow-DOM-Mechanismus ursprünglich für den Client-seitigen Browser konzipiert wurde.

Unter dem Strich lohnt sich defineCustomElement genau dann, wenn eine Komponente tatsächlich framework-übergreifend oder in einer Legacy-Umgebung eingesetzt werden muss, während für reinen internen Gebrauch innerhalb einer ohnehin bestehenden Vue-Anwendung normale Komponenten weiterhin die einfachere und leistungsfähigere Wahl bleiben, unter anderem wegen der vollen Provide/Inject-Unterstützung und der besseren DevTools-Integration.

Aspekt Normale Vue-Komponente Custom Element (defineCustomElement)
Verwendung Nur innerhalb einer Vue-App In jeder HTML-Umgebung, auch ohne Vue
Styling-Kapselung Globales CSS wirkt auf die Komponente Shadow DOM isoliert Styles, CSS-Custom-Properties dringen durch
Props Direkt reaktiv per Vue-Bindings Attribute (String) oder DOM-Property für komplexe Werte
Provide/Inject Funktioniert über den ganzen Baum Nur innerhalb der eigenen App-Instanz, nicht über Element-Grenzen
Bundle-Größe Teilt sich die Vue-Runtime mit der App Vue-Runtime meist inline, jedes Element trägt mehr Gewicht

Mironsoft

Vue-Architektur, Composition API und Nuxt-Performance

Vue-Anwendungen, die mit jedem Feature nicht komplizierter werden?

Wir prüfen bestehende Vue- und Nuxt-Projekte auf unstrukturierte Composables, ungenutzte Reaktivität und aufgeblähte Bundles und bauen daraus eine Architektur, die neue Features aufnimmt, ohne die Codebasis unübersichtlicher zu machen.

Architektur-Review

Composables, State-Management und Komponentenstruktur auf Wartbarkeit prüfen.

Performance-Audit

Reaktivitäts-Overhead, Bundle-Größe und Nuxt-Rendering-Strategie systematisch optimieren.

Nuxt-Integration

SSR/SSG-Setup und API-Anbindung robust und typsicher aufbauen.

10. Zusammenfassung

Vue Custom Elements: Das Wichtigste auf einen Blick

API

defineCustomElement() aus Vue verwandelt eine SFC in eine registrierbare Element-Klasse.

Isolation

Rendering in einen eigenen Shadow-Root, Styles bleiben vollständig gekapselt.

Einsatz

Einbettung in Legacy-Systeme oder Anwendungen ohne Vue.

Grenze

Provide/Inject funktioniert nicht automatisch über Element-Grenzen hinweg.

11. FAQ: Vue Custom Elements: Das Wichtigste auf einen Blick

1Was macht defineCustomElement genau?
defineCustomElement wandelt eine gewöhnliche Vue-Komponente in eine Klasse um, die von HTMLElement erbt und intern eine vollständige Vue-Instanz verwaltet, diese Klasse wird anschließend per customElements.define unter einem Tag-Namen registriert.
2Muss der Tag-Name eines Custom Elements einen Bindestrich enthalten?
Ja, das schreibt die Web-Components-Spezifikation zwingend vor, um Kollisionen mit zukünftigen nativen HTML-Elementen auszuschließen, ein Name ohne Bindestrich wird vom Browser bei der Registrierung abgelehnt.
3Warum landen die Styles einer Komponente automatisch im Shadow DOM?
Vue erkennt beim Kompilieren einer für defineCustomElement vorgesehenen SFC automatisch die style-Blöcke und bettet sie direkt in den Shadow-Root der Komponenten-Instanz ein, statt sie global ins Dokument einzufügen.
4Kann ich komplexe Objekte als Prop an ein Custom Element übergeben?
Nicht sinnvoll als HTML-Attribut, da Attribute immer Strings sind, komplexe Werte wie Arrays oder Objekte werden stattdessen als DOM-Property direkt per JavaScript gesetzt, also element.items = [...].
5Wie kommen Events aus der Vue-Komponente beim umgebenden Code an?
Vue übersetzt intern per emit ausgelöste Events automatisch in native CustomEvent-Objekte, die auf dem Host-Element ausgelöst werden und sich ganz normal per addEventListener abonnieren lassen.
6Warum funktioniert provide/inject nicht zwischen zwei Custom Elements?
Jedes per defineCustomElement erzeugte Element verwaltet standardmäßig seine eigene, isolierte Vue-App-Instanz, wodurch bereitgestellte Werte aus der umgebenden Anwendung nicht automatisch bei verschachtelten Custom Elements ankommen.
7Gibt es eine Lösung für geteilten Provide/Inject-Kontext?
Ja, defineCustomElement akzeptiert einen optionalen zweiten Parameter mit einer gemeinsamen App-Konfiguration, über die sich mehrere eigene Elemente denselben Provide-Kontext teilen können, das funktioniert aber nur innerhalb derselben, kontrolliert gebauten Vue-Anwendung.
8Kann ich Custom Elements auch asynchron nachladen?
Ja, in Kombination mit defineAsyncComponent wird nur die minimale Element-Definition sofort registriert, während der eigentliche Komponenten-Code erst per dynamischem Import geladen wird, sobald das Element tatsächlich im DOM erscheint.
9Funktionieren Vue-DevTools bei Custom Elements genauso gut wie bei normalen Komponenten?
Nein, die Vue-DevTools bieten für Custom Elements deutlich weniger Einblick, da die Kapselung über Shadow DOM und die eigene App-Instanz die Introspektion von außen erschwert.
10Wann lohnt sich defineCustomElement gegenüber einer normalen Vue-Komponente?
Vor allem dann, wenn die Komponente tatsächlich framework-übergreifend oder in einer Legacy-Umgebung ohne Vue eingesetzt werden muss, für rein internen Gebrauch innerhalb einer bestehenden Vue-Anwendung bleibt eine normale Komponente die einfachere Wahl.