Vue.js 3.2: Code sparen in Single File Components

Seite 2: SFCs im Wandel der Zeit

Inhaltsverzeichnis

Single File Components mit der Dateiendung *.vue vereinen in sich den Template-, den Script- und den Style-Teil für eine Komponente. Eine simple Notiz-Komponente kann so aussehen:

// MyNote.vue
<template>
  <form @submit.prevent="saveNote">
    <label> Notiz für {{ user }} <MyTextarea v-model="note"></MyTextarea> </label> 
    <button type="submit">Notiz speichern</button>
  </form>
</template>

<script>
import MyTextarea from './MyTextarea.vue'

export default {
  components: { MyTextarea },
  props: ['user'],
  emits: ['saved'],
  data() {
    return { 
      note: '',
   } 
  },
  methods: {
    saveNote() { 
      // API-Aufruf zum Abspeichern der Notiz 
      this.$emit('saved')
  }
}
</script>

Die Komponente MyNote.vue besteht aus einem Formular, das eine benutzerdefinierte Komponente MyTextarea.vue für den Textinhalt importiert. Das Prop user stellt im Formular den Usernamen dar. Der Textinhalt wird im data-Teil unter note gespeichert. Schließlich gibt es noch eine Methode saveNote, die beim Abschicken des Formulars zum Einsatz kommt. Sie emittiert das Event saved.

Diese SFC-Syntax gilt seit Version 1 des Frameworks – lediglich die Option emits kam in Version 3 hinzu – und lässt sich weiterhin problemlos verwenden. Mit der Composition API aus Version 3 kann der <script>-Teil nun auch wie folgt aussehen:

// MyNote.vue
<script>
import { ref } from 'vue'
import MyTextarea from './MyTextarea.vue'

export default {
  components: { MyTextarea },
  props: ['user'], 
  emits: ['saved'], 
  setup(props, { emit }) {
    const note = ref('')
    function saveNote() {
      // API-Aufruf zum Abspeichern der Notiz 
      emit('saved')
    } 

    return { note, saveNote } 
  }
}
</script>

Damit ist auf den ersten Blick nicht viel gewonnen, denn data() und methods werden nun vermischt, was eigentlich einen Verstoß gegen die Separation of Concerns darstellt. Jedoch lassen sich nun die Inhalte der setup-Funktionen in eigene Funktionen – die namensgebenden Composables der Composition API – auslagern. Bei diesen Funktionen hat sich die Community auf das Präfix use vor dem eigentlichen Funktionsnamen geeinigt. In der Notizen-Komponente wäre also das Refactoring des setup-Inhalts in eine useNote()-Funktion denkbar. Der Code wird nun nach der Domäne getrennt und nicht nach data() und methods. Ein Composable hat also jetzt nur eine bestimmte Aufgabe, in diesem Fall das Speichern einer Notiz.

In Vue.js 3.2 sieht die Komponente mit <script setup> wie folgt aus:

// MyNote.vue
<script setup>
import { ref } from 'vue'
import MyTextarea from './MyTextarea.vue'

const props = defineProps(['user'])
const emit = defineEmits(['saved'])

const note = ref('')

function saveNote() {
  // API-Aufruf zum Abspeichern der Notiz emit('saved')
}
</script>

Auf den ersten Blick fallen einige Unterschiede zwischen <script> und dem neueren <script setup> auf: Erstens existiert export default { ... } nicht mehr, stattdessen lebt alles im Top-Level-Kontext. Zweitens sind die Props über eine nicht importierte defineProps-Funktion definiert. defineProps ist in diesem Fall ein spezielles Compiler-Makro des Vue.js-SFC-Compilers. Beim Umwandeln der SFC wird die defineProps-Funktion zu normalem JavaScript umgewandelt, das die Props für die Komponente definiert. Da dies zur Compile-Zeit geschieht, ist es nicht notwendig, diese Funktion zu importieren, da es sie zur Laufzeit gar nicht mehr gibt. Genauso verhält es sich bei der Funktion defineEmits, die die emit-Funktionen zurückgibt.

Es ist nicht nötig, die importierte Komponente MyTextarea in components aufzulisten. Das Importieren genügt, damit sie im <template>-Bereich verfügbar ist. Das Ref note und die Funktion saveNote sind direkt im Top-Level-Kontext verfügbar und es ist nicht erforderlich, sie zurückzugeben.

Im Allgemeinen sind alle Variablen, Funktionen und Imports im Top-Level direkt im <template>-Bereich als Bindings verwendbar. Das bedeutet jedoch, dass Komponenten mit <script setup> standardmäßig geschlossen sind. Von außen ist es nicht möglich, via Template Refs auf ihre Bindings zuzugreifen.

<template>
  <MyNote ref="note_ref" user="Benutzer"></MyNote>
  <button @click="saveNoteViaRef">Speichern von außen</button>
</template>

<script>
import MyNote.vue from './MyNote.vue'

export default {
  components: { MyNote },
  methods: {
    saveNoteViaRef() {
      // Nicht möglich, da nicht von MyNote exposed
      this.refs.note_ref.save()
    }
  }
}
</script>

Um dennoch explizit definieren zu können, welche Bindings nach außen sichtbar sein sollen, stellen die Vue.js-Entwickler das zusätzliche Compiler-Makro defineExpose bereit. Damit das obige Beispiel funktioniert, muss die MyNote.vue-Komponente so aussehen:

// MyNote.vue
<script setup>
// ...

function saveNote() {
 // API-Aufruf zum Abspeichern der Notiz emit('saved')}

// Hiermit wird saveNote von außen sichtbar
defineExpose({ saveNote })
</script>

<script setup> lässt sich in einer SFC verwenden, die bereits ein <script> nutzt. Manchmal ist das sogar notwendig, wie etwa beim Verwenden von Plug-ins, die einen bestimmten Schlüssel im Option-Objekt der Komponente erwarten. Auch ist <script> nötig, um einen einmaligen Seiteneffekt zu erzeugen:

<script>
runSideEffectOnce()

export default {
  name: 'CustomComponentName',
  inheritAttrs: false,
}
</script>

<script setup>
import { ref } from 'vue'

const name = ref('')
</script>