Bei der Erstellung einer komponentenbasierte Frontend-Infrastruktur ist einer der größten Schmerzpunkte, auf die ich persönlich gestoßen bin, die Erstellung von Komponenten, die sowohl wiederverwendbar als auch responsiv sind, wenn es verschachtelte Komponenten innerhalb von Komponenten gibt.
Nehmen wir zum Beispiel die folgende „Call to Action“-Komponente (<CTA />)

Auf kleineren Geräten soll sie so aussehen

Dies ist mit einfachen Media Queries einfach genug. Wenn wir Flexbox verwenden, kann eine Media Query die Flex-Richtung ändern und den Button über die gesamte Breite anzeigen. Aber wir stoßen auf ein Problem, wenn wir andere Komponenten darin verschachteln. Sagen wir zum Beispiel, wir verwenden eine Komponente für den Button und diese hat bereits eine Prop, die sie vollwertig macht. Wir duplizieren tatsächlich das Styling des Buttons, wenn wir eine Media Query auf die Elternkomponente anwenden. Der verschachtelte Button ist bereits in der Lage, damit umzugehen!
Dies ist ein kleines Beispiel und wäre kein allzu großes Problem, aber in anderen Szenarien könnte dies zu viel dupliziertem Code führen, um das Styling zu replizieren. Was, wenn wir in Zukunft etwas an der Art und Weise ändern wollten, wie vollwertige Buttons gestylt werden? Wir müssten alle diese verschiedenen Stellen durchgehen und sie ändern. Wir sollten in der Lage sein, es in der Button-Komponente zu ändern und es überall aktualisiert zu haben.
Wäre es nicht schön, wenn wir von Media Queries weggehen und mehr Kontrolle über das Styling haben könnten? Wir sollten die vorhandenen Props einer Komponente nutzen und unterschiedliche Werte basierend auf der Bildschirmbreite übergeben können.
Nun, ich habe einen Weg gefunden, das zu tun, und werde Ihnen zeigen, wie ich es gemacht habe.
Ich bin mir bewusst, dass Container Queries viele dieser Probleme lösen können, aber sie stecken noch in den Anfängen und lösen nicht das Problem des Übergens einer Vielzahl von Props basierend auf der Bildschirmbreite.
Verfolgen der Fensterbreite
Zuerst müssen wir die aktuelle Breite der Seite verfolgen und einen Breakpoint festlegen. Dies kann mit jedem Frontend-Framework erfolgen, aber ich verwende hier ein Vue Composable, um die Idee zu demonstrieren
// composables/useBreakpoints.js
import { readonly, ref } from "vue";
const bps = ref({ xs: 0, sm: 1, md: 2, lg: 3, xl: 4 })
const currentBreakpoint = ref(bps.xl);
export default () => {
const updateBreakpoint = () => {
const windowWidth = window.innerWidth;
if(windowWidth >= 1200) {
currentBreakpoint.value = bps.xl
} else if(windowWidth >= 992) {
currentBreakpoint.value = bps.lg
} else if(windowWidth >= 768) {
currentBreakpoint.value = bps.md
} else if(windowWidth >= 576) {
currentBreakpoint.value = bps.sm
} else {
currentBreakpoint.value = bps.xs
}
}
return {
currentBreakpoint: readonly(currentBreakpoint),
bps: readonly(bps),
updateBreakpoint,
};
};
Der Grund, warum wir Zahlen für das currentBreakpoint-Objekt verwenden, wird später klar werden.
Jetzt können wir auf Fenstergrößenänderungsereignisse hören und den aktuellen Breakpoint mit dem Composable in der Hauptdatei App.vue aktualisieren
// App.vue
<script>
import useBreakpoints from "@/composables/useBreakpoints";
import { onMounted, onUnmounted } from 'vue'
export default {
name: 'App',
setup() {
const { updateBreakpoint } = useBreakpoints()
onMounted(() => {
updateBreakpoint();
window.addEventListener('resize', updateBreakpoint)
})
onUnmounted(() => {
window.removeEventListener('resize', updateBreakpoint)
})
}
}
</script>
Wir wollen das wahrscheinlich debouncen, aber ich halte es der Kürze halber einfach.
Styling von Komponenten
Wir können die <CTA /> Komponente aktualisieren, um eine neue Prop für die Art und Weise zu akzeptieren, wie sie gestylt werden soll
// CTA.vue
props: {
displayMode: {
type: String,
default: "default"
}
}
Die Benennung hier ist völlig willkürlich. Sie können beliebige Namen für jeden der Komponentenmodi verwenden.
Wir können diese Prop dann verwenden, um den Modus basierend auf dem aktuellen Breakpoint zu ändern
<CTA :display-mode="currentBreakpoint > bps.md ? 'default' : 'compact'" />
Sie können jetzt sehen, warum wir eine Zahl verwenden, um den aktuellen Breakpoint darzustellen – damit der richtige Modus auf alle Breakpoints unter oder über einer bestimmten Zahl angewendet werden kann.
Wir können dies dann in der CTA-Komponente verwenden, um sie entsprechend dem übergebenen Modus zu stylen
// components/CTA.vue
<template>
<div class="cta" :class="displayMode">
<div class="cta-content">
<h5>title</h5>
<p>description</p>
</div>
<Btn :block="displayMode === 'compact'">Continue</Btn>
</div>
</template>
<script>
import Btn from "@/components/ui/Btn";
export default {
name: "CTA",
components: { Btn },
props: {
displayMode: {
type: String,
default: "default"
},
}
}
</script>
<style scoped lang="scss">
.cta {
display: flex;
align-items: center;
.cta-content {
margin-right: 2rem;
}
&.compact {
flex-direction: column;
.cta-content {
margin-right: 0;
margin-bottom: 2rem;
}
}
}
</style>
Bereits haben wir die Notwendigkeit von Media Queries beseitigt! Sie können dies in Aktion auf einer Demo-Seite sehen, die ich erstellt habe.
Zugegebenermaßen mag dies wie ein langer Prozess für etwas so Einfaches erscheinen. Aber wenn dieser Ansatz auf mehrere Komponenten angewendet wird, kann er die Konsistenz und Stabilität der Benutzeroberfläche massiv verbessern und gleichzeitig die Gesamtmenge an Code reduzieren, die wir schreiben müssen. Diese Art der Verwendung von JavaScript und CSS-Klassen zur Steuerung des responsiven Stylings hat noch einen weiteren Vorteil…
Erweiterbare Funktionalität für verschachtelte Komponenten
Es gab Szenarien, in denen ich zu einem früheren Breakpoint für eine Komponente zurückkehren musste. Zum Beispiel, wenn sie 50% des Bildschirms einnimmt, möchte ich, dass sie im kleinen Modus angezeigt wird. Aber ab einer bestimmten Bildschirmgröße wird sie vollwertig. Mit anderen Worten, der Modus sollte sich beim Größenänderungsereignis in die eine oder andere Richtung ändern.

Ich war auch in Situationen, in denen die gleiche Komponente in verschiedenen Modi auf verschiedenen Seiten verwendet wird. Dies ist nichts, was Frameworks wie Bootstrap und Tailwind tun können, und die Verwendung von Media Queries, um dies zu erreichen, wäre ein Albtraum. (Sie können diese Frameworks weiterhin mit dieser Technik verwenden, nur ohne die Notwendigkeit der responsiven Klassen, die sie bereitstellen.)
Wir *könnten* eine Media Query verwenden, die nur für mittelgroße Bildschirme gilt, aber das löst nicht das Problem der variablen Props basierend auf der Bildschirmbreite. Glücklicherweise kann der von uns behandelte Ansatz das lösen. Wir können den vorherigen Code modifizieren, um einen benutzerdefinierten Modus pro Breakpoint zu ermöglichen, indem wir ihn über ein Array übergeben, wobei das erste Element des Arrays die kleinste Bildschirmgröße ist.
<CTA :custom-mode="['compact', 'default', 'compact']" />
Zuerst aktualisieren wir die Props, die die <CTA /> Komponente akzeptieren kann
props: {
displayMode: {
type: String,
default: "default"
},
customMode: {
type: [Boolean, Array],
default: false
},
}
Dann können wir Folgendes hinzufügen, um den richtigen Modus zu generieren
import { computed } from "vue";
import useBreakpoints from "@/composables/useBreakpoints";
// ...
setup(props) {
const { currentBreakpoint } = useBreakpoints()
const mode = computed(() => {
if(props.customMode) {
return props.customMode[currentBreakpoint.value] ?? props.displayMode
}
return props.displayMode
})
return { mode }
},
Dies nimmt den Modus aus dem Array basierend auf dem aktuellen Breakpoint und greift standardmäßig auf den displayMode zurück, wenn keiner gefunden wird. Dann können wir mode verwenden, um die Komponente zu stylen.
Extraktion für Wiederverwendbarkeit
Viele dieser Methoden können in zusätzliche Composables und Mixins extrahiert werden, die mit anderen Komponenten wiederverwendet werden können.
Extrahieren des berechneten Modus
Die Logik zur Rückgabe des korrekten Modus kann in ein Composable extrahiert werden
// composables/useResponsive.js
import { computed } from "vue";
import useBreakpoints from "@/composables/useBreakpoints";
export const useResponsive = (props) => {
const { currentBreakpoint } = useBreakpoints()
const mode = computed(() => {
if(props.customMode) {
return props.customMode[currentBreakpoint.value] ?? props.displayMode
}
return props.displayMode
})
return { mode }
}
Extrahieren von Props
In Vue 2 konnten wir Props mit Mixins wiederholen, aber es gibt bemerkenswerte Nachteile. Vue 3 ermöglicht es uns, diese mit anderen Props mithilfe desselben Composables zusammenzuführen. Es gibt einen kleinen Vorbehalt, da IDEs Props für die Autovervollständigung mit dieser Methode anscheinend nicht erkennen können. Wenn dies zu störend ist, können Sie stattdessen ein Mixin verwenden.
Optional können wir auch eine benutzerdefinierte Validierung übergeben, um sicherzustellen, dass wir nur die Modi verwenden, die für jede Komponente verfügbar sind, wobei der erste übergebene Wert der Validator ist.
// composables/useResponsive.js
// ...
export const withResponsiveProps = (validation, props) => {
return {
displayMode: {
type: String,
default: validation[0],
validator: function (value) {
return validation.indexOf(value) !== -1
}
},
customMode: {
type: [Boolean, Array],
default: false,
validator: function (value) {
return value ? value.every(mode => validation.includes(mode)) : true
}
},
...props
}
}
Lassen Sie uns nun die Logik auslagern und diese stattdessen importieren
// components/CTA.vue
import Btn from "@/components/ui/Btn";
import { useResponsive, withResponsiveProps } from "@/composables/useResponsive";
export default {
name: "CTA",
components: { Btn },
props: withResponsiveProps(['default 'compact'], {
extraPropExample: {
type: String,
},
}),
setup(props) {
const { mode } = useResponsive(props)
return { mode }
}
}
Fazit
Das Erstellen eines Designsystems von wiederverwendbaren und responsiven Komponenten ist eine Herausforderung und anfällig für Inkonsistenzen. Außerdem haben wir gesehen, wie leicht es ist, sich mit einer Menge dupliziertem Code wiederzufinden. Es gibt eine feine Balance, wenn es darum geht, Komponenten zu erstellen, die nicht nur in vielen Kontexten funktionieren, sondern auch gut mit anderen Komponenten zusammenspielen, wenn sie kombiniert werden.
Ich bin sicher, Sie sind schon einmal auf eine solche Situation gestoßen. Die Verwendung dieser Methoden kann das Problem reduzieren und hoffentlich die Benutzeroberfläche stabiler, wiederverwendbarer, wartbarer und einfacher zu bedienen machen.
Ich habe Schwierigkeiten, den Wert dieses Ansatzes zu begreifen und möchte mehr über Ihren Denkprozess erfahren, warum JavaScript eine bessere Wahl als CSS-Media-Queries ist.
Wenn ich diesen Ansatz betrachte, fühlt es sich wie ein Rückschritt an… Sie befürworten eine Methode, die vor der Einführung von
@media-Breakpoints die De-facto-Methode zur Erzielung von responsivem Design war. Das bedeutet im Wesentlichen, dass Sie eine JavaScript-Lösung fördern, die inzwischen ein Jahrzehnt alt ist.Gibt es Leistungsprobleme, die Ihrer Meinung nach JavaScript zu einer besseren Lösung machen? Die Beispiele in Ihrem Artikel geben keinen Aufschluss darüber, warum
@mediafür Ihre Bedürfnisse unzureichend war. Geht es darum, eine JavaScript-first-Herangehensweise für Benutzer anzubieten, die vielleicht weniger komfortabel mit CSS sind?Modernes CSS kann bei den meisten dieser Probleme helfen. Container Queries kommen bald und bis dahin haben wir ein solides Polyfill dafür.
Clamp() ist ebenfalls extrem nützlich. Ebenso minmax(), :where(), :is() und :has() (derzeit nur in Safari, wird aber bald von Chrome und Firefox übernommen).
Persönlich würde ich diese Methoden zusammen mit einigen intelligenten/knappen Media Queries bei Bedarf lieber verwenden, anstatt mehr JavaScript (insbesondere für Stile) hinzuzufügen.
Ich liebe das, ich denke, das fragliche Beispiel ist nicht das beste. Aber ich verstehe den Kern und sehe, dass dies in Fällen, in denen Designer Ihnen drastisch unterschiedliche Designs für jeden Breakpoint geben und die Props für jede Komponente sich alle vollständig ändern müssen, äußerst nützlich sein kann. Oder wenn sich die Benutzeroberfläche auf kleineren Bildschirmen in Seiten/Tabs aufteilt.
Sauberes DRY CSS für den Sieg :)
Ich verstehe es nicht. Sicherlich sollte JavaScript die Breite des *enthaltenden* Elements messen, nicht die Breite des gesamten Viewports?
Wenn dies Container Queries polyfillen würde, wäre es sinnvoll. Aber im Moment ist es ein Polyfill für Media Queries … die wir bereits haben.
Was fehlt mir?
Guter Artikel. Ich denke, dieser Ansatz wird für Leute am sinnvollsten sein, die mit großen Anwendungen mit Hunderten von Varianten derselben Komponente zu tun hatten. Ja – bei kleineren Projekten ist es vielleicht einfacher, Media Queries zu nutzen, aber wenn Komponenten immer wieder auf unterschiedliche Weise innerhalb anderer Komponenten wiederverwendet werden, scheint dies meiner Meinung nach ein guter Ansatz zu sein und verwandelt eine Menge unordentlicher Media Queries in schönen, sauberen und wiederverwendbaren Code.
Eine weitere Möglichkeit, etwas Ähnliches zu erreichen, ist die Verwendung von CSS-Variablen in den Komponenten, um den „displayMode“ festzulegen, und die Verwendung von Media Queries auf der obersten Komponente, um diese in den untergeordneten Elementen festzulegen…
Ich brauche das in Angular2, kann mir bitte jemand helfen?
Ich schlage vor, die ResizeObserver API dafür zu verwenden – zumindest bis zur vollständigen Unterstützung von Container Queries. Der Hauptvorteil ist, dass Sie den direkten übergeordneten Container anstelle des Fensters abhören können. :)
Ich hatte mehr erwartet, als ich „in einem Design System“ hörte. Ein Design System ist für mich weiter gefasst als nur die Vue/React/Angular-Implementierung. Das ist nur ein Ausschnitt aus dem System. Ein Muster ist kein Design System.
Gleichzeitig ist mir selbst als Muster nicht klar, welche spezifischen Vorteile der Wechsel von CSS zu JS für diese Dinge bringt, abgesehen vielleicht von einer kleinen Reduzierung des CSS-Outputs, der wahrscheinlich durch Gzipping kompensiert würde.
Ich schließe mich anderen an, verwenden Sie intrinsische CSS-Größen und Container Queries
Verwenden Sie cq-prolyfill, um Container Queries für alle CSS-Eigenschaften zu erhalten: Höhe, Farbe, Textausrichtung usw., nicht nur für die Containerbreite. Und kann mit Data-Attributen arbeiten, wenn Sie keinen Präprozessor verwenden möchten. [https://ausi.github.io/cq-prolyfill/demo/]
Verwenden Sie das neueste Polyfill für neuere Browser CSS-Tricks: Container Query Polyfill, das einfach funktioniert
CQfill wurde im CSS-Tricks-Artikel erwähnt, aber ich fand CQ-prolyfill eine viel bessere Lösung und es funktioniert für alle alten Browser (IE9+, Safari7+).
Ist das nicht einfacher, indem man Grid mit Grid-Bereichen verwendet, die für verschiedene Media Queries zugeordnet sind?
Ich frage nur.
Es scheint keine gute Idee zu sein, CSS-Aufgaben mit JS zu lösen. Ich denke, das könnte Leistungsprobleme und seltsame Bugs/Verhalten in Zukunft verursachen.
Sie haben vielleicht ein zu einfaches Beispiel verwendet, wodurch Ihre Methode überentwickelt erscheint.
Modernes CSS allein kann dies ohne Media Queries ermöglichen. Warum brauchen Sie JS?
Ich finde nicht viel Nützlichkeit in dieser Methode. Sie ist für mich kompliziert und unordentlich. Ich würde eher Container Queries verwenden, die bereits ein solides Polyfill haben.
Dies erscheint mir eher komplex und eine reine JS-Lösung für etwas, das in CSS gemacht werden könnte. Was ich tun würde (und in einem riesigen Angular-Projekt gerade tue), ist die Verwendung von CSS Custom Properties (der Name sagt alles: das sind Props für CSS) in der untergeordneten Komponente
<Button />und diese in der übergeordneten Komponente<CTA />zu definieren. Dies ist auch eine der wenigen Möglichkeiten, Web Components von außerhalb ihres Shadow DOM zu stylen.Ich stimme dem oben Gesagten zu, dass ich für diesen Anwendungsfall lieber CSS, Media Queries und Container Queries verwenden würde.
Etwas anderes, das man berücksichtigen sollte, wenn man einen JS-first-Ansatz wie diesen für etwas verwendet, das statisch gerendert wird, ist, dass, wenn die Fensterbreite bekannt sein muss, die Stile für das serverseitige Rendering nicht berechnet werden, so dass Sie Layoutverschiebungen oder Blitze von sich ändernden Stilen sehen könnten, wenn die Fensterbreite nicht in das von Ihnen festgelegte Standard-Styling für die Komponente fällt.