Ich habe kürzlich einen Artikel geschrieben, der erklärt, wie Sie einen Countdown-Timer mit HTML, CSS und JavaScript erstellen können. Nun sehen wir uns an, wie wir daraus eine wiederverwendbare Komponente machen können, indem wir sie in Vue überführen und dabei grundlegende Features des Frameworks nutzen.
Warum das Ganze? Nun, es gibt einige Gründe, aber zwei stechen besonders hervor.
- UI mit dem Timer-Status synchron halten: Wenn Sie sich den Code aus dem ersten Beitrag ansehen, lebt alles in der timerInterval-Funktion, insbesondere die Zustandsverwaltung. Jedes Mal, wenn sie ausgeführt wird (jede Sekunde), müssen wir manuell das richtige Element in unserem document finden — sei es das Zeit- label oder der verbleibende Zeitpfad oder was auch immer — und entweder seinen Wert oder ein Attribut ändern. Vue bietet eine HTML-basierte Vorlagensyntax, mit der Sie die gerenderte DOM-Struktur deklarativ an die Daten der zugrunde liegenden Vue-Instanz binden können. Das nimmt die gesamte Last des Suchens und Aktualisierens der richtigen UI-Elemente auf sich, sodass wir uns rein auf die Eigenschaften der Komponenteninstanz verlassen können.
- Eine hochgradig wiederverwendbare Komponente haben: Das Originalbeispiel funktioniert gut, wenn nur ein Timer auf unserem Dokument vorhanden ist, aber stellen Sie sich vor, Sie möchten einen weiteren hinzufügen. Hoppla! Wir verlassen uns auf die ID des Elements, um unsere Aktionen auszuführen, und die Verwendung derselben ID bei mehreren Instanzen würde sie daran hindern, unabhängig voneinander zu funktionieren. Das bedeutet, dass wir für jeden Timer unterschiedliche IDs zuweisen müssten. Wenn wir eine Vue-Komponente erstellen, ist ihre gesamte Logik gekapselt und mit dieser spezifischen Komponenteninstanz verbunden. Wir können leicht 10, 20, 1.000 Timer in einem einzigen Dokument erstellen, ohne eine einzige Zeile in der Komponente selbst zu ändern!
Hier ist derselbe Timer, den wir im letzten Beitrag zusammen erstellt haben, aber in Vue.
Vorlage und Stile
Aus der Vue-Dokumentation
Vue verwendet eine HTML-basierte Vorlagensyntax, mit der Sie die gerenderte DOM-Struktur deklarativ an die zugrunde liegenden Daten der Vue-Instanz binden können. Alle Vue.js-Vorlagen sind gültiges HTML, das von spezifikationskonformen Browsern und HTML-Parsern verarbeitet werden kann.
Lassen Sie uns unsere Komponente erstellen, indem wir eine neue Datei namens BaseTimer.vue öffnen. Hier ist die grundlegende Struktur, die wir dafür benötigen.
// Our template markup will go here
<template>
// ...
</template>
// Our functional scripts will go here
<script>
// ...
</script>
// Our styling will go here
<style>
// ...
</style>
In diesem Schritt konzentrieren wir uns auf die Abschnitte <template> und <style>. Verschieben wir unsere Timer-Vorlage in den <template>-Abschnitt und all unsere CSS in den <style>-Abschnitt. Die Markup besteht hauptsächlich aus SVG, und wir können exakt denselben Code verwenden, den wir aus dem ersten Artikel verwendet haben.
<template>
// The wrapper for the timer
<div class="base-timer">
// This all comes from the first article
<svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<g class="base-timer__circle">
<circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
<path
id="base-timer-path-remaining"
stroke-dasharray="283"
class="base-timer__path-remaining ${remainingPathColor}"
d="
M 50, 50
m -45, 0
a 45,45 0 1,0 90,0
a 45,45 0 1,0 -90,0
"
></path>
</g>
</svg>
// The label showing the remaining time
<span
id="base-timer-label"
class="base-timer__label"
>
${formatTime(timeLeft)}
</span>
</div>
</template>
// "scoped" means these styles will not leak out to other elements on the page
<style scoped>
.base-timer {
position: relative;
width: 100px;
height: 100px;
}
</style>
Werfen wir einen Blick auf die Vorlage, die wir gerade kopiert haben, um zu identifizieren, wo wir unser Framework verwenden können. Es gibt einige Teile, die dafür verantwortlich sind, dass unser Timer die Zeit herunterzählt und die verbleibende Zeit anzeigt.
stroke-dasharray: Ein Wert, der dem SVG<path>-Element übergeben wird, das für die Speicherung der verbleibenden Zeit verantwortlich ist.remainingPathColor: Eine CSS-Klasse, die dafür verantwortlich ist, die Farbe des kreisförmigen Rings des Timers zu ändern, um visuell anzuzeigen, dass die Zeit abläuft.formatTime(timeLeft): Ein Wert, der dafür verantwortlich ist, anzuzeigen, wie viel Zeit im Timer verbleibt.
Wir können unseren Timer steuern, indem wir diese Werte manipulieren.
Konstanten und Variablen
Okay, gehen wir zu unserem <script>-Abschnitt und sehen wir, was Vue unsout of the box bietet, um uns das Leben leichter zu machen. Eine Sache, die es uns erlaubt, ist die Definition unserer Konstanten im Voraus, was sie auf die Komponente beschränkt.
Im letzten Beitrag haben wir etwas Zeit damit verbracht, den stroke-dasharray-Wert anzupassen, um sicherzustellen, dass die Animation der oberen Ebene des Timers (des Rings, der sich animiert und seine Farbe ändert, während die Zeit vergeht) perfekt mit seiner unteren Ebene (dem grauen Ring, der die vergangene Zeit anzeigt) übereinstimmt. Wir haben auch "Schwellenwerte" definiert, ab wann die obere Ebene ihre Farbe ändern sollte (orange bei 10 verbleibenden Sekunden und rot bei fünf Sekunden). Wir haben auch Konstanten für diese Farben erstellt.
Wir können all diese direkt in den <script>-Abschnitt verschieben.
<script>
// A value we had to play with a bit to get right
const FULL_DASH_ARRAY = 283;
// When the timer should change from green to orange
const WARNING_THRESHOLD = 10;
// When the timer should change from orange to red
const ALERT_THRESHOLD = 5;
// The actual colors to use at the info, warning and alert threshholds
const COLOR_CODES = {
info: {
color: "green"
},
warning: {
color: "orange",
threshold: WARNING_THRESHOLD
},
alert: {
color: "red",
threshold: ALERT_THRESHOLD
}
};
// The timer's starting point
const TIME_LIMIT = 20;
</script>
Nun werfen wir einen Blick auf unsere Variablen.
let timePassed = 0;
let timeLeft = TIME_LIMIT;
let timerInterval = null;
let remainingPathColor = COLOR_CODES.info.color;
Wir können hier zwei verschiedene Arten von Variablen identifizieren.
- Variablen, bei denen die Werte direkt in unseren Methoden neu zugewiesen werden.
timerInterval: Ändert sich, wenn wir den Timer starten oder stoppen.timePassed: Ändert sich jede Sekunde, während der Timer läuft.
- Variablen, bei denen sich die Werte ändern, wenn sich andere Variablen ändern.
timeLeft: Ändert sich, wenn sich der Wert vontimePassedändert.remainingPathColor: Ändert sich, wenn der Wert vontimeLeftden festgelegten Schwellenwert überschreitet.
Es ist wichtig, diesen Unterschied zwischen diesen beiden Arten zu erkennen, da er uns die Nutzung verschiedener Framework-Funktionen ermöglicht. Gehen wir jede Art einzeln durch.
Variablen, bei denen Werte direkt neu zugewiesen werden
Denken wir darüber nach, was passieren soll, wenn wir den Wert timePassed ändern. Wir wollen berechnen, wie viel Zeit noch übrig ist, prüfen, ob wir die Farbe des oberen Rings ändern sollen, und eine Neuberechnung eines Teils unserer Ansicht mit neuen Werten auslösen.
Vue verfügt über ein eigenes Reaktivitätssystem, das die Ansicht aktualisiert, um den neuen Werten bestimmter Eigenschaften zu entsprechen. Um eine Eigenschaft zum Reaktivitätssystem von Vue hinzuzufügen, müssen wir diese Eigenschaft in einem data-Objekt in unserer Komponente deklarieren. Dadurch erstellt Vue einen Getter und einen Setter für jede Eigenschaft, die Änderungen an dieser Eigenschaft verfolgen und entsprechend reagieren.
<script>
// Same as before
export default {
data() {
return {
timePassed: 0,
timerInterval: null
};
}
</script>
Es gibt zwei wichtige Dinge, die wir beachten müssen.
- Wir müssen alle reaktiven Variablen in unserem
data-Objekt im Voraus deklarieren. Das bedeutet, wenn wir wissen, dass eine Variable existieren wird, aber nicht, wie der Wert sein wird, müssen wir sie trotzdem mit einem Wert deklarieren. Wenn wir vergessen haben, sie indatazu deklarieren, wird sie nicht reaktiv sein, auch wenn sie später hinzugefügt wird. - Beim Deklarieren unseres
data-Options-Objekts müssen wir immer eine neue Objektinstanz zurückgeben (mitreturn). Das ist entscheidend, denn wenn wir diese Regel nicht befolgen, werden die deklarierten Eigenschaften zwischen allen Instanzen der Komponente geteilt.
Sie können dieses zweite Problem in Aktion sehen.
Variablen, bei denen sich Werte ändern, wenn sich andere Variablen ändern
Diese Variablen hängen vom Wert einer anderen Variablen ab. Zum Beispiel hängt timeLeft rein von timePassed ab. In unserem Originalbeispiel mit Vanilla JavaScript haben wir diesen Wert im Intervall berechnet, das für die Änderung des Wertes von timePassed verantwortlich war. Mit Vue können wir diesen Wert in eine computed-Eigenschaft extrahieren.
Eine computed-Eigenschaft ist eine Funktion, die einen Wert zurückgibt. Diese Werte sind an die Abhängigkeitswerte gebunden und werden nur bei Bedarf aktualisiert. Noch wichtiger ist, dass computed-Eigenschaften gecacht werden, was bedeutet, dass sie sich die Werte merken, von denen die computed-Eigenschaft abhängt, und den neuen Wert nur berechnen, wenn sich der abhängige Eigenschaftswert geändert hat. Wenn sich der Wert nicht ändert, wird der zuvor gecachte Wert zurückgegeben.
<script>
// Same as before
computed: {
timeLeft() {
return TIME_LIMIT - this.timePassed;
}
}
}
</script>
Die Funktion, die an die computed-Eigenschaft übergeben wird, muss eine reine Funktion sein. Sie darf keine Nebeneffekte verursachen und muss einen Wert zurückgeben. Außerdem muss der Ausgabewert ausschließlich von den in die Funktion übergebenen Werten abhängen.
Nun können wir weitere Logik in computed-Eigenschaften verschieben.
circleDasharray: Diese gibt einen Wert zurück, der zuvor in dersetCircleDasharray-Methode berechnet wurde.formattedTimeLeft: Diese gibt einen Wert aus derformatTime-Methode zurück.timeFraction: Dies ist eine Abstraktion dercalculateTimeFraction-Methode.remainingPathColor: Dies ist eine Abstraktion dersetRemainingPathColor-Methode.
<script>
// Same as before
computed: {
circleDasharray() {
return `${(this.timeFraction * FULL_DASH_ARRAY).toFixed(0)} 283`;
},
formattedTimeLeft() {
const timeLeft = this.timeLeft;
const minutes = Math.floor(timeLeft / 60);
let seconds = timeLeft % 60;
if (seconds < 10) {
seconds = `0${seconds}`;
}
return `${minutes}:${seconds}`;
},
timeLeft() {
return TIME_LIMIT - this.timePassed;
},
timeFraction() {
const rawTimeFraction = this.timeLeft / TIME_LIMIT;
return rawTimeFraction - (1 / TIME_LIMIT) * (1 - rawTimeFraction);
},
remainingPathColor() {
const { alert, warning, info } = COLOR_CODES;
if (this.timeLeft <= alert.threshold) {
return alert.color;
} else if (this.timeLeft <= warning.threshold) {
return warning.color;
} else {
return info.color;
}
}
}
</script>
Wir haben jetzt alle benötigten Werte! Aber nun müssen wir sie in unserer Vorlage verwenden.
Verwendung von Daten und berechneten Eigenschaften in der Vorlage
Hier sind wir mit unserer Vorlage stehen geblieben.
<template>
<div class="base-timer">
<svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<g class="base-timer__circle">
<circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
<path
id="base-timer-path-remaining"
stroke-dasharray="283"
class="base-timer__path-remaining ${remainingPathColor}"
d="
M 50, 50
m -45, 0
a 45,45 0 1,0 90,0
a 45,45 0 1,0 -90,0
"
></path>
</g>
</svg>
<span
id="base-timer-label"
class="base-timer__label"
>
${formatTime(timeLeft)}
</span>
</div>
</template>
Beginnen wir mit formatTime(timeLeft). Wie können wir den gerenderten Wert dynamisch an unsere formattedTimeLeft-berechnete Eigenschaft binden?
Vue verwendet eine HTML-basierte Vorlagensyntax, die es uns ermöglicht, die gerenderte DOM-Struktur deklarativ an die zugrunde liegenden Daten der Vue-Instanz zu binden. Das bedeutet, dass alle Eigenschaften im Vorlagenbereich verfügbar sind. Um eine davon darzustellen, verwenden wir Textinterpolation mit der "Mustache"-Syntax (doppelte geschweifte Klammern oder {{ }}).
<span
id="base-timer-label"
class="base-timer__label"
>
{{ formattedTimeLeft }}
</span>
Als nächstes kommt stroke-dasharray. Wir sehen, dass wir diesen Wert nicht rendern wollen. Stattdessen wollen wir den Wert des <path>-Attributs ändern. Mustache kann nicht innerhalb von HTML-Attributen verwendet werden, aber keine Sorge! Vue bietet eine weitere Möglichkeit: die v-bind-Direktive. Wir können einen Wert wie folgt an ein Attribut binden:
<path v-bind:stroke-dasharray="circleDasharray"></path>
Um die Verwendung dieser Direktive zu erleichtern, können wir auch eine Kurzform verwenden.
<path :stroke-dasharray="circleDasharray"></path>
Das Letzte ist remainingPathColor, das einer Komponente eine passende Klasse hinzufügt. Das können wir mit derselben v-bind-Direktive wie oben machen, aber den Wert dem class-Attribut eines Elements zuweisen.
<path :class="remainingPathColor"></path>
Werfen wir einen Blick auf unsere Vorlage nach den Änderungen.
<template>
<div class="base-timer">
<svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<g class="base-timer__circle">
<circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
<path
:stroke-dasharray="circleDasharray"
class="base-timer__path-remaining"
:class="remainingPathColor"
d="
M 50, 50
m -45, 0
a 45,45 0 1,0 90,0
a 45,45 0 1,0 -90,0
"
></path>
</g>
</svg>
<span class="base-timer__label">{{ formattedTimeLeft }}</span>
</div>
</template>
Wir haben unsere Vorlage fertig, wir haben alle Variablen in data oder computed verschoben, und wir haben die meisten Methoden durch die Erstellung entsprechender computed-Eigenschaften eliminiert. Uns fehlt jedoch noch ein wichtiger Teil: Wir müssen unseren Timer starten.
Methoden und Komponenten-Lifecycle-Hooks
Wenn wir uns unsere startTimer-Methode ansehen, sehen wir, dass alle Berechnungen, Attributänderungen usw. im Intervall stattfinden.
function startTimer() {
timerInterval = setInterval(() => {
timePassed = timePassed += 1;
timeLeft = TIME_LIMIT - timePassed;
document.getElementById("base-timer-label").innerHTML = formatTime(
timeLeft
);
setCircleDasharray();
setRemainingPathColor(timeLeft);
if (timeLeft === 0) {
onTimesUp();
}
}, 1000);
}
Da wir all diese Logik bereits in die computed-Eigenschaft verschoben haben, müssen wir in unserem timerInterval nur den Wert von timePassed ändern — der Rest geschieht dann magisch in den computed-Eigenschaften.
<script>
// Same as before
methods: {
startTimer() {
this.timerInterval = setInterval(() => (this.timePassed += 1), 1000);
}
}
</script>
Wir haben die Methode bereit, aber wir rufen sie immer noch nirgendwo auf. Jede Vue-Komponente verfügt über eine Reihe von Hooks, die es uns ermöglichen, spezifische Logik während einer bestimmten Phase des Komponentenlebenszyklus auszuführen. Diese werden Lifecycle-Hooks genannt. In unserem Fall möchten wir unsere Methode sofort aufrufen, wenn die Komponente geladen wird. Das macht mounted zum gewünschten Lifecycle-Hook.
<script>
// Same as before
mounted() {
this.startTimer();
},
// Same methods as before
</script>
Das ist alles, wir haben unseren Timer mit Vue in eine konsistente und wiederverwendbare Komponente verwandelt!
Nehmen wir an, wir möchten diese Komponente nun in einer anderen Komponente verwenden. Dazu sind einige Dinge erforderlich:
- Zuerst importieren wir die Komponente.
- Als Nächstes registrieren wir die Komponente.
- Schließlich instanziieren wir die Komponente in der Vorlage.
// App.vue
import BaseTimer from "./components/BaseTimer"
export default {
components: {
BaseTimer
}
};
Das ist ein Abschluss!
Dieses Beispiel zeigt, wie wir eine Komponente von Vanilla JavaScript zu einem komponenten-basierten Frontend-Framework wie Vue verschieben können.
Wir können den Timer nun als eigenständige Komponente behandeln, bei der alle Markup, Logik und Stile so enthalten sind, dass sie nicht nach außen dringen oder mit anderen Elementen in Konflikt geraten. Komponenten sind oft Kinder einer größeren Elternkomponente, die mehrere Komponenten zusammenfügt — wie ein Formular oder vielleicht eine Karte — wobei die Eigenschaften des Elternteils zugänglich und teilbar sind. Hier ist ein Beispiel der Timer-Komponente, bei der sie Anweisungen von einer Elternkomponente erhält.
Ich hoffe, ich habe Ihr Interesse für Vue und die Leistungsfähigkeit von Komponenten geweckt! Ich ermutige Sie, die Vue-Dokumentation zu besuchen, um eine detailliertere Beschreibung der in unserem Beispiel verwendeten Funktionen zu erhalten. Vue kann so viel mehr!
Ja, vielen Dank für den Artikel, aber es gibt ein Problem.
Wie verwendet man Komponenten in Vanilla JS?
Ist das möglich?