Richtlinien für Vue
TODO:
Weitere Regeln einfügen.
Weiterhin sind die Beispiele auch noch nur für Vue 2 geeignet. Composition-Api wird überhaupt nicht erwähnt.
Einleitung
Allen Kapiteln wurde eine eindeutige Nummerierung, der Richtliniennummer, hinzugefügt, um eine eindeutige Identifikation zu ermöglichen. Jede Richtliniennummer besteht aus dem Buchstaben V(Vue) gefolgt von einer Nummer, die den Abschnitt identifiziert. Damit kann eine Regel eindeutig identifiziert werden, z.B. für ein Code-Review.
INFO
Dieses Dokument bezieht sich auf Vue 3.
Alle Beispiele sind mit 2 Leerzeichen eingerückt, da dies in Markdown die beste Darstellung bietet.
V1 Allgemeine Regeln
Es gelten die Allgemeinen Regeln für Sprachen.
V2 Abstraktionsschichten
Zugriffe auf unterliegende Schichten (Vergleich mit GL3 Abstraktionsschichten) sollen in JavaScript vermieden werden.
V3 Vue-Komponenten
Vue-Komponenten sollen einige Regeln folgen:
V3 Einfachheit
Vue-Komponenten sollen so einfach wie möglich gehalten werden (KISS-Prinzip). Auf diese Weise wird die Wartbarkeit, Lesbarkeit und Testbarkeit verbessert.
V3 Aufgabenorientierung
Eine Vue-Komponente soll nur eine Aufgabe erfüllen (Seperation of Concern). Damit wird die Testbarkeit, Wartbarkeit und Wiederverwendbarkeit verbessert.
V3 Wiederverwendbarkeit
Vue-Komponenten können erweitert werden, indem das Kompositions-Entwurfsmuster (Composite) verwendet wird. Alternativ werden Vue-Komponenten durch Verschachtelung von Sub-Komponenten, von Slots, Prop-Drilling, durch das Verwenden von Mixins oder mit Dependency Injection erweitert.
V3 Komplexe und Geschäfts-Logik auslagern
Komplexe Logik soll in Methoden, Computed Properties oder Services ausgelagert werden. Generell soll Geschäftslogik nicht in Vue-Komponenten liegen, sondern in Services (Klassen), die durch Dependency Injection in die Komponenten injiziert werden.
V3 Trennen von UI-Logik und Geschäfts-Logik
Methoden und Events in Vue-Komponenten sollen sich nur um die Logik der UI-Komponente kümmern. Wenn Geschäftslogik in einer Methode benötigt wird, soll diese in einer Service-Klasse ausgelagert werden, so kann die Logik wiederverwendet, ersetzt oder getestet werden.
V3 Kopplung reduzieren
Vue-Komponenten sollen so wenig wie möglich von anderen Komponenten abhängig sein. Damit wird die Wiederverwendbarkeit und Testbarkeit verbessert.
V3 Tests parallel zur Entwicklung
Tests sollen direkt für Vue-Komponenten erstellt werden, damit die Vue-Komponente parallel zur Entwicklung getestet wird und entsprechend für die Tests angepasst wird.
Komponenten, die ohne Tests entwickelt werden, neigen dazu komplex und kompliziert zu werden, denn das Entwickeln von Tests zwingt dazu, die Komponenten einfach und klar zu halten.
V3 Keine direkten DOM-Zugriffe
Direkte DOM-Zugriffe sollen vermieden werden. Vue bietet viele Möglichkeiten, um auf das DOM zuzugreifen, ohne direkt darauf zuzugreifen. Direkte DOM-Zugriffe machen die Komponenten schwer testbar und wartbar.
V4 Nur einfache Vue-Expressions
Vue-Expression im HTML-Template sollen nur für einfachste Ausdrücke verwendet werden. Komplexe oder längere Ausdrücke sollen in Methoden oder Computed Properties ausgelagert werden.
V4 Problem
Oft werden in Vue-Expressions in Direktiven wie v-if
, v-for
, v-bind
oder v-on
zuerst einfache Ausdrücke verwendet, die dann mit der Zeit und Anforderungen zu immer größere und komplexere Ausdrücke werden. Dies führt zu unübersichtlichen, schwer testbaren und wartbaren Templates. Zudem ist es oftmals nicht ersichtlich, was der Ausdruck genau bewirkt.
<template>
<div>
<div v-if="user && user.name && user.name.length > 0">
{{ user.name == null ? 'No Name' : user.name }}
</div>
</div>
V4 Lösung
Komplexe Ausdrücke sollen in Methoden oder Computed Properties ausgelagert werden, wenn sie aus mehr als einer Zeile bestehen oder mehr als eine einfache Operation enthalten. Computed Properties sind hierbei vorzuziehen, wenn der Wert gecached werden soll.
<template>
<div>
<div v-if="isUserNameValid">
{{ userNameOrNoName }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
user: {
name: 'Max'
}
}
},
computed: {
isUserNameValid() {
return this.user && this.user.name && this.user.name.length > 0
},
userNameOrNoName() {
return this.user.name == null ? 'No Name' : this.user.name
}
}
}
WARNING
Die zu berechnenden Terme innerhalb eines Computed Properties sollen nach den Regeln in diesem Guide definiert werden.
V5 Verwenden von Klassen statt dynamischer Styles
Dynamische Styles sollten durch CSS-Klassen mit :class
ersetzt werden, statt direkt das Style-Attribut mit :style
oder sogar style
.
V5 Problem
Wenn das Style-Attribut direkt gesetzt wird, ist es schwierig, die Styles zu ändern oder zu erweitern. Zudem ist es schwer zu erkennen, welche Styles aufgrund welcher Bedingungen gesetzt werden und diese zu ändern.
<template>
<div>
<div :style="{ color: isSelected ? 'red' : 'blue' }">
{{ user.name }}
</div>
</div>
V5 Lösung
Klassen sollen verwendet werden, um dynamische Styles zu setzen. Vue und darauf aufbauende UI-Frameworks wie Vuetify bieten viele Möglichkeiten, Klassen dynamisch zu setzen.
Vue unterstützt das Setzen von Klassen mit v-bind:class
oder :class
.
<template>
<div>
<div :class="{ selected: isSelected, 'text-red': isRed }">
{{ user.name }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
isSelected: true,
isRed: false
}
}
}
</script>
<div class="selected">User Name</div>
<template>
<div>
<div :class="mainClasses">
{{ user.name }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
isSelected: true,
isRed: false
}
},
computed: {
mainClasses() {
return {
selected: this.isSelected,
'text-red': this.isRed
}
}
}
}
</script>
<div class="selected">User Name</div>
<template>
<div>
<div :class="[isSelected, isRed]">
{{ user.name }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
isSelected: 'selected',
isRed: 'text-red'
}
}
}
<div class="selected text-red">User Name</div>
V6 Reihenfolgen der Direktiven in Vue-Elementen
Es soll eine bestimmte Reihenfolge der Direktiven in Vue-Elementen eingehalten werden.
v-if
undv-show
sollen an erster Stelle im Element verwendet werden.v-for
soll als nächstes verwendet werden.v-bind
oder:
soll danach verwendet werden.v-on
oder@
soll zuletzt verwendet werden.v-model
soll nachv-bind
oder:
verwendet werden.v-slot
soll nachv-model
verwendet werden.v-html
soll nachv-slot
verwendet werden.v-pre
soll als letztes verwendet werden.
V7 Kommentare im HTML-Template
Kommentare im HTML-Template sollen vermieden werden, insbesondere große Kommentarblöcke (Guter Code statt Kommentare)
V7 Problem
Es ist möglich, dass Kommentare im Produktivcode landen, wenn sie nicht entfernt werden.
V7 Lösung
Kommentare sollen durch gut benannte Methoden oder Computed Properties ersetzt werden.
V8 Vue-Props nicht verändern
Vue-Props sollen nicht verändert werden.
V8 Problem
Wenn Vue-Props verändert werden, kann dies zu unerwartetem Verhalten führen.
export default {
props: {
user: {
type: Object,
required: true
}
},
methods: {
changeUser() {
this.user.name = 'Max'
}
}
}
V8 Lösung
Vue-Props sollen als read-only betrachtet werden. Dies kanna auf verschiedene Weisen erreicht werden:
- Vue-Props generell nicht verändern.
- Vue-Props in lokale Daten kopieren und diese verändern.
- Vue-Props in Computed Properties verwenden, die angepasste oder veränderte Werte zurückgeben.
- Statt Schleifen können Deklarative Methoden aus
Array
wiemap
,filter
,reduce
verwendet werden, die keine Seiteneffekte haben.
computed: {
userName() {
return this.user.name + '!'
}
}
V8 Weitere Informationen
- Vue 3
- Vue 2
V9 Keine direkten Objektzugriffe im Template
Verwende keine direkten Objektzugriffe im Template, sondern lass Computed-Eigenschaften die gewünschten Werte zurückgeben, z. B. nicht v-for(data in myProps.User.userList)
.
V10 Watcher und Listener müssen entfernt werden
Jeder Watcher und Listener muss entfernt werden, wenn die Vue-Komponente zerstört wird.
V10 Problem
Wenn Watcher und Listener nicht entfernt werden, können unerwartete Seiteneffekte auftreten. Dies können sein:
- Speicherlecks
- Unerwartetes Ausführen von Code, obwohl die Komponente nicht mehr existiert
V10 Lösung
Watcher und Listener sollen in der onUnmounted
-Hook entfernt werden.
Dependency Injection für setTimeout
und setInterval
setTimeout
und setInterval
sollen mit inject
als Dependency Injection injiziert werden. Siehe dazu Browserspezifische Funktionen.
import { watch, onMounted, onUnmounted } from 'vue'
let watcher
let stopTimeout
let stopInterval
onMounted(() => {
watcher = watch(() => userName, (newValue, oldValue) => {
console.log('Name changed')
})
stopTimeout = setTimeout(() => {
console.log('Timeout')
}, 1000)
stopInterval = setInterval(() => {
console.log('Interval')
}, 1000)
})
onUnmounted(() => {
watcher()
clearTimeout(stopTimeout)
clearInterval(stopInterval)
})
V11 null, undefined Werte
null
oder undefined
Werte sollen immer gepüft, abgefangen oder vermieden werden.
V11 Problem
Viele Fehler treten auf, weil auf null
oder undefined
Werte zugegriffen wird, die zu einem TypeError
führen. Insbesondere der Zugriff innerhalb eines Vue-Templates auf null
oder undefined
Werte führt zu Fehlern, die schwer zu finden sind.
export default {
data() {
return {
user: null
}
}
}
<template>
<div>
<div v-if="user.name">
{{ user.name }}
</div>
</div>
</template>
V11 Lösung
null
oder undefined
Werte sollen immer geprüft und abgefangen werden.
Folgende Möglichkeiten gibt es:
v-if
oderv-show
verwenden, um zu prüfen, ob ein Wertnull
oderundefined
ist.||
-Operator verwenden, um einen Standardwert zu setzen.??
-Operator verwenden, um einen Standardwert zu setzen, wenn es auch den Wert0
oder''
(leerer String) gibt.Optional Chaining
verwenden, um auf verschachtelte Objekte zuzugreifen.null
oderundefined
Werte in Computed Properties prüfen und abfangen.- Spezielle Objekte, die einen
Null Object Pattern
verwenden, umnull
oderundefined
Werte zu vermeiden.
<template>
<div>
<div v-if="user.name">
{{ user.name }}
</div>
</div>
</template>
<script setup>
import { ref, readonly, onMounted } from 'vue'
const NullUser = readonly(reactive({
name: 'None'
}))
const user = ref(NullUser)
onMounted(() => {
// default user
user.value = NullUser
})
</script>
Das Beispiel zeigt, wie ein Null Object Pattern
verwendet werden kann, um null
oder undefined
Werte zu vermeiden. Dazu wird ein spezielles Objekt NullUser
erstellt, das als Standardwert für user
verwendet wird. Das NullUser-Objekt ist ein readonly
-Objekt, das nicht verändert werden kann und auch nicht mit value verändert werden kann.
V12 Inject/Provide verwenden
Vue-Komponenten sollen Inject
und Provide
verwenden, um Abhängigkeiten zu injizieren. Statt import
von Abhängigkeiten in Komponenten sollen diese durch Inject
und Provide
injiziert werden, so dass im besten Fall kein import
in der Komponente vorkommt.
V12 Problem
Vue-Komponenten sind oft abhängig von anderen Komponenten oder Services. Diese Abhängigkeiten sollen nicht direkt in den Komponenten erstellt werden, sondern durch Inject
und Provide
injiziert werden.
import { onMounted, onUnmounted } from 'vue'
import UserService from './UserService'
import { otherMethod } from './OtherService'
onMounted(() => {
const userService = new UserService()
otherMethod(userService)
})
V12 Lösung
Abhängigkeiten sollen durch Inject
und Provide
injiziert werden. Dadurch wird die Wiederverwendbarkeit, Testbarkeit und Wartbarkeit verbessert. Eine Komponente kann so einfach verändert werden, indem eine andere Abhängigkeit injiziert wird.
import { onMounted, inject } from 'vue'
const userService = inject('userService')
const otherMethod = inject('otherService')
onMounted(() => {
otherMethod(userService)
})
Der Komponente wird durch provide
die Abhängigkeit injiziert.
import { provide, ref } from 'vue'
const userService = new UserService()
provide('userService', userService)
provide('otherService', otherMethod)
V13 Browserspezifische Funktionen
Oft werden spezielle Funktionen verwendet, um den Browser zu steuren. Dazu gehören:
setTimeout
undsetInterval
scrollTo
undscrollBy
resize
undscroll
und ähnliche Events
V13 Problem
Diese Funktionen führen speziellen Code in der Laufzeitumgebung (Node, Browser) aus und können in Tests nicht oder nur schwer getestet werden. Um Komponenten testbar zu machen, sollen diese Funktionen vermieden und Alternativen wie Reaktivität, Events und Dependency Injection verwendet werden.
Im Folgenden wird auf setTimeout
und setInterval
als Beispiel eingegangen.
V13 setTimeout und setInterval
setTimeout
und setInterval
sollen in Vue-Komponenten vermieden werden. Die Gründe sind:
- Schwierige Tests Wenn Komponenten getestet werden, führen
setTimeout
undsetInterval
dazu, dass Tests schwierig zu schreiben und zu verstehen sind, weil Code asynchron ausgeführt wird. - Asynchronität
setTimeout
undsetInterval
führen zu asynchronem Code, der schwer zu verstehen und zu warten ist. Andere Komponenten, die eine solche Komponente verwenden, müssen wiederum asynchronen Code verwenden, um auf die asynchronen Timer zu reagieren. - Lebensdauer
setTimeout
undsetInterval
führen zu Problemen, wenn die Komponente zerstört wird, aber die Timer noch laufen. So können bereits abgeschlossene Tests fehlschlagen, weil die Timer noch laufen. Siehe Watcher und Listener müssen entfernt werden.
V13 Alternativen
Statt setTimeout
soll die Reaktivität von Vue verwendet werden.
V13 Dependecy Injection
Sollte ein Timer benötigt werden, weil beispielsweise eine verwendete Komponente dies bereits macht, muss der Timer durch Dependency Injection injiziert werden, um die Asynchronität zu vermeiden.
Vue 3 Composition API bietet die Möglichkeit, Timer zu injizieren.
// Komponente
import { inject } from 'vue'
const setTimeoutDI = inject('setTimeout')
// Parent Komponente
import { provide } from 'vue'
provide('setTimeout', setTimeout)
Tests können dadurch von setTimeout
und setInterval
entkoppelt, und die Asynchronität vermieden werden. Im folgenden Test wird setTimeout
durch eine Funktion ersetzt, die sofort ausgeführt wird. Die Komponente kann so getestet werden, ohne auf die asynchrone Ausführung von setTimeout
zu warten.
const setTimeoutDI = (callback) => callback()
it('should call setTimeout', () => {
const setTimeout = jest.fn()
const wrapper = mount(MyComponent, {
global: {
provide: {
setTimeout: (callback) => callback()
}
}
})
// ... alle Methoden der Komponenten, die setTimeout verwenden, aufrufen
// sind nicht mehr asynchron
expect(wrapper.vm.method()).toHaveBeenCalled()
})
V13 Globale Timeout und Intervall ersetzen für Tests
In Tests für Komponenten, die weitere Komponenten einbinden, die setTimeout
und setInterval
verwenden, jedoch nicht verändert werden können, können die globalen Funktionen ersetzt werden.
describe('MyComponent', () => {
let setTimeoutOriginal
let setIntervalOriginal
before(() => {
setTimeoutOriginal = global.setTimeout
setIntervalOriginal = global.setInterval
global.setTimeout = (callback) => callback()
global.setInterval = (callback) => callback()
})
after(() => {
global.setTimeout = setTimeoutOriginal
global.setInterval = setIntervalOriginal
})
it('should call setTimeout', () => {
// MyComponent verwendet intern eine weitere Komponente, die setTimeout verwendet
const wrapper = mount(MyComponent)
// ... alle Methoden der Komponenten, die setTimeout verwenden, aufrufen
// sind nicht mehr asynchron
expect(wrapper.vm.method()).toHaveBeenCalled()
})
})