Gå til indhold
Jan Engelbrecht Pederseb Logo

Introduktion til Vue.js 3

Et komplet kursus fra de grundlæggende koncepter til bygning af en fuldskala Enterprise-applikation med Vue Router og Pinia.

Introduktion til Vue.js 3

Et komplet kursus fra de grundlæggende koncepter til bygning af en fuldskala Enterprise-applikation med Vue Router og Pinia.

Modul 1: Fundamentet og det moderne webproblem

Modul 2: Fra imperativ til deklarativ kode (Praktisk case)

Modul 3: Hvad er Vue.js, og hvornår bruger vi det?

Modul 4: Hvorfor Vue.js er overlegent (Fordele over Vanilla JS)

Modul 5: Reaktive core-koncepter i dybden

Modul 6: Komponentkommunikation og livscyklus

Modul 7: Single-Page Architecture med Vue Router

Modul 8: Global State Management med Pinia

Modul 9: API-integration, håndtering af asynkron data og PHP backend

Modul 10: Avancerede teknikker, genanvendelighed og optimering

Modul 11: Det store afgangsprojekt (Enterprise Case)

Modul 1: Fundamentet og det moderne webproblem

Dette modul zoomer ind på, hvorfor moderne JavaScript-frameworks som Vue.js overhovedet eksisterer. Vi kigger på det mest centrale problem i webudvikling: Hvordan holder vi det, brugeren ser på skærmen, synkroniseret med applikationens data?

1.1 Problemet i moderne webudvikling

I webudvikling opererer vi grundlæggende med to adskilte verdener:

Koncept Beskrivelse Eksempel
State (Data)Applikationens interne sandhed og hukommelse.En variabel: let isLoggedIn = true; eller en prissum: 149.50
DOM (Brugergrænsefladen)Det visuelle HTML-træ, som browseren tegner, og som brugeren interagerer med.<p>Velkommen tilbage, Peter!</p> eller <span>Pris: 149,50 kr.</span>

Det fundamentale problem: Webapplikationer er dynamiske. Data ændrer sig hele tiden – f.eks. når en vare lægges i en kurv, en besked tikker ind, eller brugeren taster i et inputfelt. Hver evig eneste gang dine data ændrer sig, skal dit HTML-skærmbillede (DOM'en) opdateres med det samme for at afspejle den nye virkelighed. Hvis synkroniseringen fejler, oplever brugeren en ødelagt eller fastfrossen applikation.

1.2 Vanilla JavaScripts begrænsninger (Den imperative fælde)

Når du bygger komplekse applikationer med ren JavaScript (Vanilla JS), tvinges du til at benytte en imperativ tilgang.
Imperativ betyder: At du skal diktere hvordan noget skal gøres, trin for trin. Du skal give browseren detaljerede mikroinstruktioner.
Hvis du f.eks. har en profilside og brugeren ændrer sit navn, skal du manuelt skrive kode til at:

  1. Finde det præcise HTML-element i DOM'en (document.getElementById).
  2. Slette det gamle navn.
  3. Indsætte det nye navn (element.innerText).

Hvorfor fører det til "spaghettikode"?
I takt med at en applikation vokser, får du hundredvis af variable og hundredvis af HTML-elementer. Koden bliver hurtigt tæt koblet:

  • En ændring i ét stykke data kræver manuelle opdateringer fem forskellige steder i din HTML.
  • Glemmer du blot ét sted, er din UI asynkron med dine data.
  • Resultatet bliver uoverskuelig, sårbar og svær-at-vedligeholde spaghettikode, hvor det er næsten umuligt at gennemskue, hvilken funktion der ændrer hvad.

1.3 Vues løsning: De to kerneprincipper

Vue.js fjerner fuldstændig behovet for manuelle mikroinstruktioner ved at vende strategien 180 grader om. Vue introducerer en deklarativ tilgang.
Deklarativ betyder: Du beskriver hvad resultatet skal være (sluttilstanden), og lader maskinrummet (Vue) finde ud af, hvordan det eksekveres.
Dette lader sig gøre gennem to revolutionerende kerneprincipper:

1. Deklarativ rendering
I stedet for at lede efter HTML-elementer med JavaScript, forbinder du dine data direkte i din HTML-struktur ved hjælp af simple skabeloner (templates). Du fortæller blot HTML'en: "Her skal variablen brugerNavn vises". Vue overtager herfra ansvaret for at bygge og opdatere det rå HTML-træ.

2. Reaktivitet & den Virtuelle DOM
Vue gør dine data reaktive. Det betyder, at Vue overvåger og sporer alle ændringer i dine variabler i baggrunden.
Når en værdi ændres, sker følgende lynhurtigt:

[Data ændres] ➔ [Vue opdaterer en lynhurtig 'Virtuel DOM' i hukommelsen] ➔ [Vue finder præcis de elementer, der er ændret] ➔ [Kun de specifikke dele af den rigtige HTML opdateres i browseren]
            

Dette sker 100% automatisk. Du ændrer blot værdien på din variabel i JavaScript, og millisekunder efter har browseren rettet det på skærmen – uden at du har skrevet en eneste linje kode med querySelector eller innerText.

Modul 2: Fra imperativ til deklarativ kode (Praktisk case)

I dette modul skal vi se teorien i praksis. Vi bygger den samme simple applikation – en interaktiv tæller (Counter) – i både rå Vanilla JavaScript og i Vue 3. Målet er at demonstrere, hvordan mængden af kode, kompleksiteten og risikoen for fejl falder drastisk, når vi skifter til Vue.
Applikationen har et enkelt formål: Den viser et tal på skærmen, og når brugeren klikker på en knap, stiger tallet med 1.

2.1 Det klassiske "Counter"-eksempel

For at forstå forskellen skal vi se på arkitekturen i de to tilgange. I Vanilla JS skal vi selv bygge broen mellem knappen, dataen og teksten. I Vue 3 forbinder vi tingene direkte i HTML-strukturen.

2.2 Vanilla JS-tilgangen (Imperativ manuel DOM-manipulation)

Her er standardmåden at løse opgaven på i traditionel JavaScript. Koden er inddelt i to filer (eller sektioner): HTML og Script.

HTML-struktur:

<div id="app">
  <p>Antal klik: <span id="counter-text">0</span></p>
  <button id="increment-btn">Klik mig</button>
</div>
            

JavaScript-logik:

// 1. Definer state (applikationens data i hukommelsen)
let count = 0;

// 2. Find DOM-elementer (Vi skal manuelt række ud i HTML'en)
const button = document.getElementById('increment-btn');
const counterText = document.getElementById('counter-text');

// 3. Tilføj en Event Listener (Lyt efter klik på knappen)
button.addEventListener('click', () => {
  // 4. Opdater applikationens state i hukommelsen
  count++;
  
  // 5. Opdater DOM'en manuelt (Tving browseren til at tegne det nye tal)
  counterText.innerText = count;
});
            

Vær forsigtig med at bruge denne kode.

Hvad sker der her? (Den imperative arbejdsproces)
Læg mærke til trin 2 og trin 5. Hvis du glemmer trin 2, kan JavaScript ikke finde ud af, hvor teksten skal stå. Hvis du glemmer trin 5, tæller variablen count fint op i baggrunden, men brugeren vil stadig se tallet "0" på skærmen. Du har det fulde ansvar for at synkronisere data og visning.

2.3 Vue.js 3-tilgangen (Composition API)

Nu bygger vi nøjagtig den samme funktionalitet i Vue 3 ved hjælp af det moderne Composition API og <script setup>-syntaksen.

Den samlede Vue-komponent:

<!-- HTML Skabelon (Template) -->
<div id="app">
  <!-- Deklarativ binding: HTML lytter direkte til variablen 'count' -->
  <p>Antal klik: {{ count }}</p>
  
  <!-- Event binding: Vi kobler klik-eventet direkte til logikken -->
  <button @click="count++">Klik mig</button>
</div>

<!-- JavaScript Logik -->
<script setup>
import { ref } from 'vue'

// 1. Definer en reaktiv variabel (State)
const count = ref(0)
</script>
            

Vær forsigtig med at bruge denne kode.

Hvad sker der her? (Den deklarative arbejdsproces)

  1. ref(0): Vi pakker tallet 0 ind i Vues reaktivitetssystem. Det fortæller Vue: "Hold øje med denne variabel!"
  2. {{ count }}: Disse "mustache"-parenteser er en deklarativ binding. Vi siger til HTML'en: "Vis værdien af count her, uanset hvad den er."
  3. @click="count++": I stedet for at lede efter knappen i JavaScript, sætter vi eventet direkte på HTML-elementet. Når der klikkes, lægges 1 til count.

Fordi count er reaktiv, opdager Vue øjeblikkeligt ændringen og opdaterer automatisk {{ count }} i HTML'en. Vi skal ikke skrive noget kode for at opdatere skærmen.

2.4 Sammenligning: Vanilla JS vs. Vue 3

Når vi stiller de to metoder op over for hinanden, bliver det tydeligt, hvorfor frameworks har vundet over Vanilla JS til dynamiske interfaces:

Parameter Vanilla JavaScript Vue.js 3
DOM-manipulationManuel (getElementById, innerText)100% Automatisk via reaktivitet
KoblingTæt koblet (Logikken skal kende HTML-id'er)Løst koblet (HTML'en trækker blot på dataen)
Boilerplate-kodeHøj (Meget kode går til at hente og binde elementer)Minimal (Fokus ligger udelukkende på data og logik)
SkalerbarhedSvær (Jo flere elementer, jo mere uoverskuelig DOM-kode)Nem (Nye elementer bindes blot til eksisterende data)
Risiko for UI-bugsStor (Nemt at glemme at opdatere et element)Nærmest nul (UI er altid et direkte spejlbillede af data)

Modul 3: Hvad er Vue.js, og hvornår bruger vi det?

Nu hvor vi har set forskellen på Vanilla JS og Vue i praksis, skal vi dykke ned i, hvad Vue.js er som platform, og hvilke specifikke arkitektoniske problemer det løser i moderne systemudvikling.

3.1 Definitionen på et progressivt framework

Når man læser den officielle dokumentation, beskrives Vue.js som et "Progressivt Framework". Det er et helt centralt designvalg, som adskiller Vue fra andre store frameworks som Angular.
Progressivt betyder: At frameworket kan skaleres trinvis alt efter projektets kompleksitet. Det dikterer ikke en fast "alt-eller-intet"-struktur fra start.
I praksis betyder det, at Vue kan adopteres på to vidt forskellige måder:

Implementering Beskrivelse Typisk brug
Det lille script (Drop-in)Du linker blot til Vue via et CDN-script (ligesom det gamle jQuery) på en eksisterende HTML-side.Tilføjelse af en lille interaktiv formular eller en kurvfunktion på en traditionel PHP- eller WordPress-side.
Det fulde økosystem (SPA)Du bygger projektet fra bunden ved hjælp af build-værktøjer (som Vite) og udnytter hele Vues økosystem.Store, komplekse webapplikationer, dashboards og SaaS-produkter (Software as a Service).

Dette gør Vue ekstremt populært i industrien, da virksomheder kan migrere gamle systemer til Vue gradvist i stedet for at skulle genforfatte millioner af linjer kode på én gang.

3.2 Primære anvendelsesscenarier

Vue.js er ikke det rette værktøj til en simpel, statisk tekstside (f.eks. en blog eller en privat hjemmeside). Vue brillerer til gengæld i de tre følgende kategorier:

1. Single-Page Applications (SPA'er)
En SPA er en webapplikation, hvor browseren kun indlæser én enkelt HTML-side én gang. Når brugeren klikker rundt i applikationen (f.eks. skifter fra "Indbakke" til "Sendt" i Gmail, eller skifter sang i Spotify), genindlæses siden ikke.
I stedet går Vue ind og sletter de gamle elementer i DOM'en og indsætter de nye lynhurtigt i baggrunden. Det giver en flydende, app-agtig brugeroplevelse uden hvide blink eller ventetid på sideloads.

2. Komplekse dashboards og realtidsinterfaces
Hvis din applikation indeholder skærmbilleder med mange datakilder, der ændrer sig uafhængigt af hinanden – f.eks. et finansielt dashboard med grafer, dynamiske tabeller og søgefiltre – er Vue i sit rette element. Når en ny dataværdi tikker ind via et API, opdaterer Vue præcis den graf eller den tabelrække, der påvirkes, uden at resten af skærmen forstyrres.

3. Genanvendelige UI-komponenter (digitale byggeklodser)
I moderne webudvikling koder man ikke længere store, sammenhængende HTML-sider. Man tænker i komponenter. En komponent er en isoleret byggeklods, der indeholder sit eget HTML-layout, sin egen CSS-styling og sin egen JavaScript-logik.

[ Applikation ]
  ├── [ Navigationsbar ]
  ├── [ Produktoversigt ]
  │     ├── [ Produktkort 1 ]
  │     ├── [ Produktkort 2 ]
  │     └── [ Produktkort 3 ]
  └── [ Footer ]
            

Når du har kodet et produktkort én gang i Vue, kan du genbruge det tusindvis af gange på tværs af hele din platform. Skal kortet rettes eller styles om, gør du det ét sted, og ændringen slår igennem overalt.

Modul 4: Hvorfor Vue.js er overlegent (Fordele over Vanilla JS)

I dette afsluttende modul for introduktionen skal vi se på, hvorfor professionelle udviklingsteams vælger Vue.js frem for rå JavaScript til større it-projekter. Vi kigger på den arkitektoniske struktur, det officielle økosystem og ikke mindst applikationssikkerhed.

4.1 Farvel til manuel DOM-manipulation

Som vi så i Modul 2, slipper du i Vue helt for at skrive metoder som document.querySelector, document.getElementById og element.innerHTML.
Når du eliminerer disse manuelle mikroinstruktioner, opnår du tre afgørende fordele i professionelle projekter:

  • Færre UI-bugs: Den hyppigste kilde til fejl i komplekse Vanilla JS-apps er, at en udvikler glemmer at opdatere et bestemt HTML-element, når data ændres. I Vue er UI altid synkroniseret med din data (State).
  • Mindre kode (Clean Code): Du slipper for hundredvis af linjer med opsætnings- og bindingskode (såkaldt boilerplate-kode). Du koder kun din rå forretningslogik.
  • Læsevenlighed: Koden bliver væsentligt nemmere at læse og overskue for nye udviklere på holdet.

4.2 Komponentbaseret arkitektur og Single-File Components (.vue)

I traditionel webudvikling er dit projekt ofte splittet op i tre store, uoverskuelige filer: én gigantisk HTML-fil, en enorm CSS-fil og en ustruktureret JS-fil. Det gør det svært at gennemskue, hvilken CSS-styling og JS-logik der hører til hvilket HTML-element.
Vue løser dette via Single-File Components (SFC) med filendelsen .vue. Her samler du alt, hvad en specifik byggeklods (komponent) skal bruge, i én enkelt fil:

<!-- MinKomponent.vue -->
<template>
  <div class="alert-box">
    <p>{{ message }}</p>
  </div>
</template>

<script setup>
const message = "Operationen lykkedes!"
</script>

<style scoped>
.alert-box {
  padding: 20px;
  background-color: #d4edda;
}
</style>
            

Vær forsigtig med at bruge denne kode.

Hvad betyder scoped styling?
Læg mærke til <style scoped>. Dette er en af Vues stærkeste features. Det sikrer, at CSS-reglerne i denne fil kun rammer denne specifikke komponent. Du risikerer dermed aldrig, at en CSS-ændring i ét modul ved et uheld ødelægger designet i et helt andet modul på tværs af systemet.

4.3 Det officielle økosystem og standardisering

Hvis du bygger en stor applikation i Vanilla JS, skal du selv opfinde den dybe tallerken hver gang, du skal løse komplekse arkitektoniske udfordringer. Vue har et fuldstændig standardiseret og officielt økosystem, som hele branchen anvender:

Værktøj Hvad løser det? Eksempel på Vanilla JS-udfordring
Vue RouterSide-navigation (Routing): Styrer hvilken komponent der skal vises, når URL'en ændres (f.eks. /profil eller /indstillinger), uden at genindlæse browseren.I Vanilla JS skal du manuelt lytte på URL-ændringer og bygge dit eget system til at skjule/vise elementer.
PiniaGlobal State Management: En central "databank" i din app, hvor data kan deles på tværs af alle komponenter (f.eks. oplysninger om den indloggede bruger eller indholdet af en indkøbskurv).I Vanilla JS skal du sende data frem og tilbage via komplekse funktioner eller gemme alt i kaotiske globale variable.

4.4 Indbygget sikkerhed: Forebyggelse af Cross-Site Scripting (XSS)

Sikkerhed er kritisk i professionelle applikationer. En af de mest udbredte sårbarheder på nettet er Cross-Site Scripting (XSS), hvor ondsindede brugere forsøger at indsprøjte skadelig JavaScript-kode i dine inputfelter (f.eks. i et kommentarfelt), som derefter afvikles i andre brugeres browsere.
Hvis du i Vanilla JS bruger element.innerHTML til at spytte brugergenereret tekst ud på skærmen, åbner du døren på vid gab for XSS-angreb.

Hvordan beskytter Vue dig?
Når du udskriver data i Vue ved hjælp af de deklarative mustache-parenteser {{ }} eller via v-text, foretager Vue automatisk HTML Escaping.
Hvis en hacker forsøger at indsende koden:
<script>stjæl_passwords()</script>
...vil Vue automatisk omdanne de rå tegn til harmløse HTML-entiteter (&lt;script&gt;...), så browseren blot viser teksten som ren tekst på skærmen i stedet for at eksekvere koden. Vue gør det altså "sikkert som standard" at bygge dynamiske interfaces.

Modul 5: Reaktive core-koncepter i dybden

I de foregående moduler har vi kradset i overfladen af Vues reaktivitet. I dette modul tager vi motorhjelmen af. Du vil lære, hvordan Vue håndterer forskellige datatyper, hvordan vi optimerer beregninger, overvåger dataændringer i baggrunden, og hvordan vi bygger komplekse tovejsdataformularer.

5.1 Reaktive datatyper: ref() vs. reactive()

Når du skal gøre data reaktivt i Vue 3 Composition API, har du to primære værktøjer: ref() og reactive(). Det er afgørende at kende forskellen for at undgå arkitektoniske fejl.

Værktøj Understøttede datatyper Tilgang i JavaScript (<script>) Tilgang i HTML (<template>)
ref()Primitive værdier (String, Number, Boolean) samt Objekter/Arrays.Kræver altid .value (f.eks. count.value++)Pakkes automatisk ud (f.eks. {{ count }})
reactive()Kun komplekse typer (Object, Array). Fejler med primitive værdier.Direkte adgang uden .value (f.eks. user.name = 'Ib')Direkte adgang (f.eks. {{ user.name }})

Det store problem med reactive()
Selvom reactive() kan virke rarere, fordi man slipper for at skrive .value, har den en stor svaghed: Hvis du forsøger at destrukturere et reactive-objekt, eller hvis du overskriver hele objektet med et nyt API-respons, mister du reaktiviteten.

// ❌ RISIKABELT: Reaktivitet kan mistes ved overskrivning
let user = reactive({ name: 'Lars', age: 30 })
user = { name: 'Bo', age: 25 } // UI opdaterer IKKE, forbindelsen er brudt!

// ✅ SIKKERT standardvalg: ref() håndterer alt
const user = ref({ name: 'Lars', age: 30 })
user.value = { name: 'Bo', age: 25 } // Fungerer perfekt! UI opdaterer med det samme.
            

Vær forsigtig med at bruge denne kode.

Professionel anbefaling: Brug ref() som din standard til absolut alt, medmindre du har en meget specifik årsag til andet.

5.2 Beregnede egenskaber (computed)

Forestil dig, at du har en webshop og skal vise en kurvsum inklusiv moms og fragt. Du kan skrive regnestykket direkte i din HTML, men hvis JavaScript-motoren skal genberegne det, hver gang der sker noget tilfældigt på skærmen, bliver din app langsom.
Her bruger vi computed(). En computed property er en reaktiv variabel, der cacher (gemmer) sit resultat. Den genberegner kun, hvis de underliggende data rent faktisk ændrer sig.

<template>
  <div>
    <p>Subtotal: {{ subtotal }} kr.</p>
    <!-- computed property kaldes uden parenteser da det er en variabel -->
    <p>Total inkl. moms: {{ totalPrice }} kr.</p> 
  </div>
</template>

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

const subtotal = ref(100)
const taxRate = 0.25

// Denne beregning caches. Sker der ændringer andre steder i UI, 
// genberegnes denne IKKE, før subtotal.value ændres.
const totalPrice = computed(() => {
  return subtotal.value * (1 + taxRate)
})
</script>
            

Vær forsigtig med at bruge denne kode.

5.3 Datamonitorering (watch og watchEffect)

Nogle gange vil du ikke bare vise en beregnet værdi. Du vil i stedet køre en side-effect (en handling), når en specifik variabel ændrer sig – f.eks. gemme data til browserens LocalStorage eller kalde et API, hver gang brugeren skifter side i en tabel.

watch vs. watchEffect

  • watch: Du skal eksplicit fortælle, hvad du vil holde øje med. Den giver dig adgang til både den nye og den gamle værdi af variablen.
  • watchEffect: Den sporer automatisk alle reaktive variabler, der bliver brugt inden i funktionen, og kører desuden én gang med det samme, når komponenten indlæses.
import { ref, watch, watchEffect } from 'vue'

const searchInput = ref('')
const userId = ref(1)

// Eksempel 1: watch (God til specifikke overvågninger og API-kald)
watch(searchInput, (newValue, oldValue) => {
  console.log(`Brugeren ændrede søgning fra ${oldValue} til ${newValue}`)
  // Udfør f.eks. et søge-API-kald her
})

// Eksempel 2: watchEffect (Kører med det samme og sporer 'userId' automatisk)
watchEffect(() => {
  console.log(`Henter automatisk data for bruger-ID: ${userId.value}`)
  // Fetch-kald vil køre hver gang userId.value ændres
})
            

Vær forsigtig med at bruge denne kode.

5.4 Tovejs-databinding (v-model)

I traditionel JavaScript skal du lytte på en input-event, hente værdien fra inputfeltet, og opdatere din variabel. Når du vil have variablen tilbage i inputfeltet, skal du sætte element.value = minVariabel.
I Vue bruger vi den magiske attribut v-model. Den opretter en tovejs-databinding.

  • Hvis brugeren taster i feltet, opdateres JavaScript-variablen øjeblikkeligt.
  • Hvis JavaScript-variablen ændres (f.eks. via et API-kald), opdateres teksten i inputfeltet øjeblikkeligt.
<template>
  <div class="form-container">
    <h3>Tilmeldingsformular</h3>
    
    <!-- Tovejs-databinding på tekstfelt -->
    <input v-model="formData.username" type="text" placeholder="Brugernavn" />
    
    <!-- Tovejs-databinding på en checkbox (bliver til en Boolean) -->
    <label>
      <input v-model="formData.acceptTerms" type="checkbox" />
      Jeg accepterer handelsbetingelserne
    </label>

    <button :disabled="!formData.acceptTerms">Send tilmelding</button>
    
    <pre>{{ formData }}</pre> <!-- Live preview af formulardata -->
  </div>
</template>

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

const formData = ref({
  username: '',
  acceptTerms: false
})
</script>
            

Vær forsigtig med at bruge denne kode.

Modul 6: Komponentkommunikation og livscyklus

Når du bygger professionelle applikationer i Vue, vil du hurtigt ende med at have mange forskellige komponenter. For at applikationen kan fungere som en samlet enhed, skal disse uafhængige byggeklodser kunne udveksle data.
Dette modul gennemgår, hvordan komponenter kommunikerer op og ned i hierarkiet, og hvordan du styrer koden baseret på, hvornår en komponent fødes og dør på skærmen.

6.1 Props og Custom Events (Uvejs-datastrøm)

I Vue flyder data top-down efter princippet "Props down, events up". En barn-komponent må aldrig direkte ændre på data, den har modtaget fra sin forælder. Den skal i stedet råbe op, så forælderen selv kan opdatere sine data.

[ Forældre-komponent ]
       │
       ├─ (Sender data ned via Props) ──> [ Barn-komponent ]
       │                                         │
       └─ <── (Råber op via Custom Events) ──────┘
            

1. Props: Data fra forælder til barn
En prop er en specialfremstillet HTML-attribut, som du definerer på din komponent.

Barn-komponent (ProductCard.vue):

<template>
  <div class="card">
    <h4>{{ title }}</h4>
    <p>Pris: {{ price }} kr.</p>
    <!-- Når der klikkes, udsendes et event kaldet 'add-to-cart' -->
    <button @click="$emit('add-to-cart')">Tilføj til kurv</button>
  </div>
</template>

<script setup>
// Vi definerer hvilke props komponenten accepterer
defineProps({
  title: String,
  price: Number
})

// Vi definerer hvilke events komponenten kan udsende
defineEmits(['add-to-cart'])
</script>
            

Vær forsigtig med at bruge denne kode.

Forældre-komponent (App.vue):

<template>
  <div class="shop">
    <!-- Vi looper over data og sender dem ned som props samt lytter på eventet -->
    <ProductCard 
      v-for="item in products" 
      :key="item.id"
      :title="item.name" 
      :price="item.price"
      @add-to-cart="handleNewItemInCart(item)"
    />
  </div>
</template>

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

const products = ref([
  { id: 1, name: 'Vue Kursus', price: 499 },
  { id: 2, name: 'Tailwind Bog', price: 199 }
])

const handleNewItemInCart = (item) => {
  console.log(`${item.name} er tilføjet til kurven i App.vue!`)
}
</script>
            

Vær forsigtig med at bruge denne kode.

6.2 Komponentens livscyklus (Lifecycle Hooks)

Hver komponent i Vue gennemgår en livscyklus: Den bliver oprettet i hukommelsen, sat ind i HTML-træet (DOM'en), opdateret når data ændres, og til sidst slettet, hvis brugeren navigerer væk.
Vue giver dig adgang til Lifecycle Hooks – funktioner, der automatisk kører på specifikke tidspunkter i denne cyklus. De tre vigtigste er:

Lifecycle Hook Hvornår kører den? Typisk brugsscenarie
onMounted()Efter at komponenten er sat fysisk ind i browserens DOM.Hente data fra et API via Fetch/Axios, eller starte en timer.
onUpdated()Hver gang en reaktiv variabel ændres, og UI er blevet gen-tegnet.Debugging eller synkronisering med tredjeparts JavaScript-biblioteker.
onUnmounted()Lige inden komponenten slettes fuldstændig fra skærmen og hukommelsen.Rengøring: Stop intervaller (clearInterval), fjern globale event-lyttere.

Eksempel på livscyklusstyring:

import { onMounted, onUnmounted, ref } from 'vue'

const currentTime = ref(new Date().toLocaleTimeString())
let timerId = null

onMounted(() => {
  console.log('Komponenten er klar på skærmen. Starter ur...')
  timerId = setInterval(() => {
    currentTime.value = new Date().toLocaleTimeString()
  }, 1000)
})

onUnmounted(() => {
  console.log('Komponenten lukkes ned. Rydder op i hukommelsen...')
  // VIGTIGT: Hvis vi ikke stopper intervallet, vil det køre evigt i baggrunden (memory leak)!
  clearInterval(timerId)
})
            

Vær forsigtig med at bruge denne kode.

6.3 Slots (Indholdsdistribution)

Nogle gange vil du gerne lave en komponent, der fungerer som en generisk ramme – f.eks. en popup-boks (Modal) eller et layout-kort. Du ved dog ikke på forhånd, hvilken HTML-struktur eller tekst der skal indeni.
Her bruger vi <slot>. Det fungerer som et pladsholder-hul i barnet, som forælderen kan fylde ud med valgfri HTML.

Barn-komponent (ModalBox.vue):

<template>
  <div class="modal-backdrop">
    <div class="modal-content">
      <!-- Alt hvad forælderen skriver indeni komponenten lander her -->
      <slot></slot> 
    </div>
  </div>
</template>
            

Vær forsigtig med at bruge denne kode.

Brug i Forældre-komponent:

<ModalBox>
  <h2>Advarsel!</h2>
  <p>Er du sikker på, at du vil slette din profil?</p>
  <button>Ja, slet alt</button>
</ModalBox>
            

Vær forsigtig med at bruge denne kode.

6.4 Template Refs

Vue styrer DOM'en automatisk, men i 1% af tilfældene har du brug for at række direkte ud og røre ved et fysisk HTML-element (f.eks. for at sætte fokus i et inputfelt, når en side åbner, eller måle højden på et element).
Vi gør dette ved at give elementet en ref-attribut i HTML'en og oprette en matchende ref(null) i JavaScript.

<template>
  <!-- Vi forbinder HTML-elementet til variablen via ref-attributten -->
  <input ref="searchField" type="text" placeholder="Søg her..." />
</template>

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

// Navnet skal matche stringen i HTML-skabelonen nøjagtigt
const searchField = ref(null)

onMounted(() => {
  // .value indeholder nu det ægte, rå HTMLInputElement fra browseren
  searchField.value.focus()
})
</script>
            

Vær forsigtig med at bruge denne kode.

Modul 7: Single-Page Architecture med Vue Router

Når en webapplikation vokser, får du brug for flere sider (f.eks. en forside, en produktside og et lukket dashboard). I traditionel webudvikling vil et klik på et link tvinge browseren til at hente en helt ny HTML-side fra serveren, hvilket resulterer i et hvidt blink og ventetid.
Med Vue Router bygger vi i stedet en ægte SPA. Browseren indlæser kun koden én gang. Når brugeren skifter URL, udskifter Vue lynhurtigt komponenterne på skærmen uden at genindlæse siden.

7.1 Opsætning af Vue Router

For at aktivere routing skal vi oprette en konfigurationsfil, der definerer, hvilke URL-stier (paths) der hører til hvilke komponenter (views).

1. Konfigurationsfilen (router/index.js)

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import DashboardView from '../views/DashboardView.vue'

const router = createRouter({
  // createWebHistory sikrer pæne URL'er (f.eks. /dashboard i stedet for /#/dashboard)
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      path: '/dashboard',
      name: 'dashboard',
      component: DashboardView
    }
  ]
})

export default router
            

Vær forsigtig med at bruge denne kode.

2. Visning i hovedkomponenten (App.vue)
I din App.vue skal du fortælle Vue Router, hvor de matchede sider skal spspyttes ud. Det gør du med <RouterView>. Du navigerer mellem siderne ved hjælp af <RouterLink> i stedet for almindelige <a>-tags.

<template>
  <nav class="navigation">
    <!-- RouterLink forhindrer browseren i at genindlæse siden ved klik -->
    <RouterLink to="/">Forside</RouterLink> | 
    <RouterLink to="/dashboard">Dashboard</RouterLink>
  </nav>

  <main class="content-area">
    <!-- Her bliver enten HomeView eller DashboardView indsat dynamisk -->
    <RouterView />
  </main>
</template>
            

Vær forsigtig med at bruge denne kode.

7.2 Dynamisk routing (URL-parametre)

Ofte har du brug for en rute, der kan tilpasse sig dynamisk – f.eks. en produktside, hvor URL'en indeholder produktets unikke ID (/produkt/42 eller /produkt/99).
Dette løses ved at indsætte et kolon (:) i rutens path.

Rutedefinition:

{
  path: '/product/:id',
  name: 'product-detail',
  component: () => import('../views/ProductDetailView.vue')
}
            

Vær forsigtig med at bruge denne kode.

Hentning af parameteren inde i komponenten (ProductDetailView.vue):
For at få fat i ID'et fra URL'en, bruger vi useRoute-funktionen direkte i vores JavaScript.

<template>
  <div class="product-details">
    <h2>Viser detaljer for produkt ID: {{ productId }}</h2>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()

// route.params indeholder alle dynamiske parametre fra URL'en
const productId = computed(() => route.params.id)
</script>
            

Vær forsigtig med at bruge denne kode.

7.3 Navigation Guards (beforeEach)

I professionelle it-systemer må brugere ikke have adgang til alt. Hvis en bruger forsøger at tilgå /dashboard, skal systemet tjekke, om vedkommende er logget ind. Hvis ikke, skal de omdirigeres til /login.
Dette styrer vi centralt i routeren ved hjælp af en Navigation Guard (beforeEach), som fungerer som en dørmand, der tjekker alle ruteskift.

// Vi tilføjer et 'meta'-felt på de ruter, der kræver login
{
  path: '/dashboard',
  component: DashboardView,
  meta: { requiresAuth: true }
}

// Global dørmand (Guard)
router.beforeEach((to, from, next) => {
  const isLoggedIn = ref(localStorage.getItem('user_token') !== null)

  // Tjek om ruten kræver login, og om brugeren IKKE er logget ind
  if (to.meta.requiresAuth && !isLoggedIn.value) {
    // Afvis adgang og send brugeren til login-siden
    next({ name: 'login' })
  } else {
    // Giv grønt lys til at fortsætte navigationen
    next()
  }
})
            

Vær forsigtig med at bruge denne kode.

7.4 Lazy Loading af ruter (Performance-optimering)

Hvis din applikation har 50 forskellige sider, vil den samlede JavaScript-fil blive enorm. Hvis brugeren kun besøger forsiden, er det spild af båndbredde at downloade koden til de 49 andre sider med det samme.
Vue Router understøtter Lazy Loading. Ved at importere komponenter via en funktion, hentes koden til en specifik side først i det sekund, brugeren faktisk klikker på linket.

// ❌ DÅRLIG PERFORMANCE: Hentes med det samme ved opstart
import AdminView from '../views/AdminView.vue'

// ✅ OPTIMERET PERFORMANCE: Hentes kun når stien '/admin' besøges
const routes = [
  {
    path: '/admin',
    name: 'admin',
    component: () => import('../views/AdminView.vue')
  }
]
            

Vær forsigtig med at bruge denne kode.

Modul 8: Global State Management med Pinia

Når din Single-Page Application vokser, støder du hurtigt ind i et stort problem: Flere komponenter placeret forskellige steder i applikationen har brug for at tilgå og ændre de samme data.
Hvis en bruger logger ind, skal både navigationsbaren, profilsiden og checkout-siden kende brugerens navn. Hvis du forsøger at sende disse data op og ned som props og events gennem 5-10 niveauer af komponenter (et fænomen kaldet Prop Drilling), bliver din kode ulæselig og umulig at vedligeholde.
Pinia er det officielle globale datalager (State Management) til Vue 3. Det fungerer som en central "databank" i skyen af din applikation, som alle komponenter kan læse fra og skrive til direkte.

8.1 Arkitekturen: Hvorfor Pinia?

Uden et globalt lager skal data rejse en lang og besværlig omvej. Med Pinia trækker alle komponenter blot på den samme centrale kilde:

Uden Pinia (Prop Drilling):
[Komponent A] ➔ Prop ➔ [Komponent B] ➔ Prop ➔ [Komponent C]

Med Pinia (Centraliseret databank):

                [ Global Pinia Store ]
               /          │           \
   (Læs/Skriv)        (Læs/Skriv)      (Læs/Skriv)
             /            │             \
     [Komponent A]  [Komponent B]  [Komponent C]
            

8.2 Definer en Store

En Store i Pinia er opbygget fuldstændig ligesom en Vue-komponent med det moderne Composition API. Den består af tre grundelementer:

  1. state (svarer til ref()): De rå data.
  2. getters (svarer til computed()): Beregnede værdier baseret på dine data.
  3. actions (svarer til function): Metoder, der ændrer dine data (understøtter også asynkron kode som API-kald).

Eksempel: En global indkøbskurv (stores/cart.js)

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  // 1. State: Listen over varer i kurven
  const items = ref([])

  // 2. Getters: Beregnede værdier (caches automatisk)
  const totalItems = computed(() => items.value.length)
  
  const totalPrice = computed(() => {
    return items.value.reduce((sum, item) => sum + item.price, 0)
  })

  // 3. Actions: Funktioner til at ændre state
  function addToCart(product) {
    items.value.push(product)
  }

  function removeFromCart(productId) {
    items.value = items.value.filter(item => item.id !== productId)
  }

  // Vi returnerer alt det, som komponenterne skal have adgang til
  return { items, totalItems, totalPrice, addToCart, removeFromCart }
})
            

Vær forsigtig med at bruge denne kode.

8.3 Data-manipulation i komponenter

Når din store er defineret, kan enhver komponent i din applikation importere og bruge den med ganske få linjer kode.

Eksempel: Vareoversigt, der tilføjer til kurv (views/ShopView.vue)

<template>
  <div class="shop">
    <h2>Webshop</h2>
    <div v-for="product in products" :key="product.id" class="product-card">
      <p>{{ product.name }} - {{ product.price }} kr.</p>
      <!-- Vi kalder Pinia action direkte ved klik -->
      <button @click="cart.addToCart(product)">Læg i kurv</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useCartStore } from '../stores/cart'

// Initialiser vores globale store
const cart = useCartStore()

const products = ref([
  { id: 1, name: 'Avanceret Vue 3 Bog', price: 299 },
  { id: 2, name: 'Tailwind T-shirt', price: 150 }
])
</script>
            

Vær forsigtig med at bruge denne kode.

Eksempel: Navigationsbar, der viser kurvens status (components=Navbar.vue)

<template>
  <nav class="navbar">
    <h1>Min Webshop</h1>
    <div class="cart-status">
      <!-- Vi udnytter vores getters direkte i HTML'en -->
      🛒 Kurv ({{ cart.totalItems }} stk) — Total: {{ cart.totalPrice }} kr.
    </div>
  </nav>
</template>

<script setup>
import { useCartStore } from '../stores/cart'
const cart = useCartStore()
</script>
            

Vær forsigtig med at bruge denne kode.

8.4 Persistering af State (Overlev side-reload)

En standard Pinia store lever kun i browserens midlertidige hukommelse. Hvis brugeren trykker på "F5" eller genindlæser siden, bliver hukommelsen tømt, og indkøbskurven slettes.
For at løse dette i professionelle apps, synkroniserer vi vores store med browserens localStorage. Det kan gøres manuelt med en watch-funktion direkte i din store:

import { defineStore } from 'pinia'
import { ref, watch } from 'vue'

export const useUserStore = defineStore('user', () => {
  // Hent indledende data fra localStorage, hvis det findes
  const token = ref(localStorage.getItem('user_token') || null)

  // Hold øje med ændringer i token, og gem det automatisk til localStorage
  watch(token, (newToken) => {
    if (newToken) {
      localStorage.setItem('user_token', newToken)
    } else {
      localStorage.removeItem('user_token')
    }
  })

  function logout() {
    token.value = null
  }

  return { token, logout }
})
            

Vær forsigtig med at bruge denne kode.

Modul 9: API-integration, håndtering af asynkron data og PHP backend

Indtil nu har vores applikationer udelukkende opereret med statiske testdata gemt direkte i browserens hukommelse. I den virkelige verden skal data hentes fra en database via et API, og ændringer skal sendes tilbage til serveren, så de ikke forsvinder.
Dette modul gennemgår, hvordan du bruger Fetch API (eller Axios) i Vue 3 til at kommunikere asynkront med en backend, og hvordan du designer en robust brugergrænseflade, der elegant håndterer ventetider og netværksfejl.

9.1 Fetch i Vue: Hentning af asynkron data

Når vi skal hente data fra en server, gør vi det typisk i en onMounted lifecycle hook, så anmodningen sendes afsted i samme sekund, komponenten lander på skærmen.
Fordi netværkskald tager tid, skal vi pakke vores kode ind i asynkrone funktioner (async/await).

Eksempel: Hent produkter fra et API (views/ProductsView.vue)

<template>
  <div class="products-container">
    <h2>Vores Produkter</h2>

    <!-- HÅNDTERING AF LOADING: Vises kun mens vi venter på serveren -->
    <div v-if="isLoading" class="loading-spinner">
      Indlæser produkter... Venligst vent.
    </div>

    <!-- HÅNDTERING AF FEJL: Vises hvis serveren svarer med en fejl -->
    <div v-else-if="errorMessage" class="error-box">
      ⚠️ Fejl: {{ errorMessage }}
    </div>

    <!-- VISNING AF DATA: Vises når data er modtaget fejlfrit -->
    <ul v-else class="product-list">
      <li v-for="product in products" :key="product.id">
        <strong>{{ product.title }}</strong> - {{ product.price }} kr.
      </li>
    </ul>
  </div>
</template>

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

const products = ref([])
const isLoading = ref(true)
const errorMessage = ref(null)

// Asynkron funktion til at hente data
const fetchProducts = async () => {
  try {
    isLoading.value = true
    errorMessage.value = null

    const response = await fetch('https://eksempel.dk')
    
    // Tjek om serveren svarede med en HTTP-statusfejl (f.eks. 404 eller 500)
    if (!response.ok) {
      throw new Error(`Serveren svarede med status: ${response.status}`)
    }

    // Omdan JSON-svaret til en JavaScript-struktur og gem i vores ref
    products.value = await response.json()
  } catch (error) {
    // Fang netværksfejl (f.eks. hvis brugeren ikke har internet)
    errorMessage.value = error.message
  } finally {
    // loading stopper UANSET om det gik godt eller skidt
    isLoading.value = false
  }
}

// Udløs API-kaldet når komponenten er klar
onMounted(() => {
  fetchProducts()
})
</script>
            

Vær forsigtig med at bruge denne kode.

9.2 Håndtering af Loading- og Fejltilstande

Som vist i eksemplet ovenfor er det dårlig UX (brugeroplevelse) blot at efterlade skærmen tom, mens et API-kald er i gang. Professionelle applikationer opererer altid med et tredelt stadie for asynkron data:

Tilstand Visuelt element Formål
LoadingSpinner, skeleton screens eller loading-tekst.Fortæller brugeren, at appen arbejder, og forhindrer dem i at klikke febrilsk flere gange.
ErrorTydelig, ikke-teknisk fejlmeddelelse med en "Prøv igen"-knap.Gør appen robust mod servernedbrud og dårlig internetforbindelse.
SuccessDet faktiske data-interface (tabeller, kort, lister).Vises først, når variablen er fyldt med valide data.

9.3 Samspil med en PHP Backend (Afsendelse af data)

Når du skal sende data fra din Vue-frontend til en PHP-backend (f.eks. når en bruger opretter et nyt produkt i en formular), skal du foretage en POST-anmodning, hvor data pakkes som JSON.

Frontenden: Vue send-funktion

const newProduct = ref({ title: 'PHP & Vue Masterclass', price: 899 })

const submitProduct = async () => {
  try {
    const response = await fetch('https://minbackend.dk', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json' // Fortæl PHP at vi sender JSON
      },
      body: JSON.stringify(newProduct.value) // Omdan objekt til JSON-streng
    })

    if (response.ok) {
      alert('Produktet blev oprettet i databasen!')
    }
  } catch (error) {
    console.error('Fejl ved oprettelse:', error)
  }
}
            

Vær forsigtig med at bruge denne kode.

Backenden: create-product.php (Hvordan PHP modtager det)
I Vanilla PHP kan du ikke fange JSON-data via $_POST. Du skal i stedet læse serverens rå input-stream og omdanne det fra JSON til et PHP-objekt.

<?php
// 1. Tillad CORS (så din Vue-app må tale med din PHP-server på en anden port/domæne)
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Headers: Content-Type");
header("Content-Type: application/json; charset=UTF-8");

// 2. Læs den rå input stream fra serveren
$json_data = file_get_contents("php://input");

// 3. Omdan JSON-strengen til et associerativt PHP-array
$data = json_decode($json_data, true);

// 4. Valider og brug dataen
if (!empty($data['title']) && !empty($data['price'])) {
    
    // HER KAN DU INDSÆTTE I DIN SQL-DATABASE VIA PDO
    // $stmt = $pdo->prepare("INSERT INTO products...");
    
    // 5. Send et professionelt JSON-svar tilbage til Vue
    http_response_code(201); // 201 = Created
    echo json_encode(["message" => "Produktet blev oprettet succesfuldt."]);
} else {
    http_response_code(400); // 400 = Bad Request
    echo json_encode(["message" => "Ugyldige data modtaget."]);
}
?>
            

Vær forsigtig med at bruge denne kode.

Modul 10: Avancerede teknikker, genanvendelighed og optimering

Når du bygger store enterprise-applikationer, er det ikke nok, at koden virker. Koden skal også være modulær, genanvendelig og lynhurtig. Hvis du kopierer den samme logik (f.eks. et API-kald eller en formular-validering) på tværs af 5 forskellige komponenter, bryder du DRY-princippet (Don't Repeat Yourself).
Dette modul lærer dig at trække forretningslogik ud i uafhængige, reaktive funktioner kaldet Composables, samt hvordan du optimerer din applikations ydeevne og finder flaskehalse.

10.1 Composables (Genanvendelig logik)

En Composable er en funktion, der udnytter Vues Composition API til at indkapsle og genanvende reaktiv logik. Det er Vues svar på React Hooks.
Lad os tage API-kaldet med Loading og Error fra Modul 9 og flytte det ud i en fuldstændig genanvendelig Composable.

1. Definer din Composable (composables/useFetch.js)

import { ref } from 'vue'

// Vi eksporterer en funktion, der kan genbruges til ALLE API-kald i systemet
export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const isLoading = ref(false)

  const doFetch = async () => {
    isLoading.value = true
    error.value = null
    try {
      const res = await fetch(url)
      if (!res.ok) throw new Error(`HTTP fejl! Status: ${res.status}`)
      data.value = await res.json()
    } catch (err) {
      error.value = err.message
    } finally {
      isLoading.value = false
    }
  }

  // Kør kaldet med det samme
  doFetch()

  // Returner reaktive variable, så komponenten kan lytte på dem
  return { data, error, isLoading, retry: doFetch }
}
            

Vær forsigtig med at bruge denne kode.

2. Brug din Composable i en komponent (views/UserListView.vue)
Nu bliver din komponent ekstremt ren og let at læse, fordi alt "computearbejdet" er flyttet væk.

<template>
  <div>
    <h2>Brugerliste</h2>
    <div v-if="isLoading">Henter brugere...</div>
    <div v-else-if="error">
      Fejl: {{ error }} <button @click="retry">Prøv igen</button>
    </div>
    <ul v-else-if="users">
      <li v-for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

<script setup>
import { useFetch } from '../composables/useFetch'

// Vi destrukturerer og omdøber 'data' til 'users'
const { data: users, error, isLoading, retry } = useFetch('https://api.com')
</script>
            

Vær forsigtig med at bruge denne kode.

10.2 Custom Directives (Egne HTML-attributter)

Vue har indbyggede direktiver som v-if og v-model. Nogle gange har du brug for at lave dit helt eget direktiv til at styre direkte DOM-adfærd på tværs af projektet – f.eks. et direktiv, der automatisk formaterer teksten som valuta eller tilføjer en log-hændelse.

Eksempel: Et direktiv der automatisk sætter fokus (v-focus)
I stedet for Template Refs (Modul 6.4) kan du lave et globalt eller lokalt direktiv:

<template>
  <!-- feltet får automatisk fokus når siden indlæses -->
  <input v-focus type="text" placeholder="Skriv noget..." />
</template>

<script setup>
// Et lokalt direktiv skal starte med et lille 'v' efterfulgt af stort begyndelsesbogstav
const vFocus = {
  mounted: (el) => el.focus() // 'el' er det ægte HTML-element
}
</script>
            

Vær forsigtig med at bruge denne kode.

10.3 Performance-optimering

Selvom Vues Virtuelle DOM er lynhurtig, kan store datamængder (f.eks. en tabel med 10.000 rækker) sløve browseren. Her er de tre vigtigste teknikker til optimering:

1. v-once (Statisk optimering)
Hvis du har elementer, der aldrig ændrer sig, efter de er tegnet første gang (f.eks. en virksomhedsbeskrivelse eller en overskrift hentet fra et API), kan du tilføje v-once. Det fortæller Vue, at elementet skal ignoreres ved fremtidige re-renders, hvilket sparer hukommelse.

<h1 v-once>{{ staticTitle }}</h1>
            

Vær forsigtig med at bruge denne kode.

2. v-memo (Betinget gentegning)
Introduceret i Vue 3. Giver dig mulighed for at cache en hel HTML-understruktur. Vue vil kun gentegne elementet, hvis en af de variable, der er angivet i arrayet, ændrer sig. Perfekt til store lister.

<!-- Gen-tegn KUN denne række, hvis 'item.id' eller 'item.updateTime' ændrer sig -->
<div v-for="item in hugeList" :key="item.id" v-memo="[item.id, item.updateTime]">
  <p>{{ item.name }} - {{ item.status }}</p>
</div>
            

Vær forsigtig med at bruge denne kode.

3. Vue DevTools (Fejlfinding)
Som professionel udvikler skal du installere Vue DevTools som udvidelse i din browser (Chrome/Firefox). Værktøjet giver dig adgang til:

  • Timeline / Performance Profiler: Se præcis hvor mange millisekunder en komponent er om at loade, og find ud af, hvilken funktion der sløver systemet.
  • State Inspector: Se dine reaktive ref-værdier og din globale Pinia Store ændre sig i realtid, mens du klikker rundt.

Modul 11: Det store afgangsprojekt (Enterprise Case)

Dette modul er 100% praksisorienteret. Kursisterne skal ikke længere følge små guidede eksempler; de skal i stedet agere som professionelle softwareudviklere og bygge en komplet, produktionsklar applikation fra bunden ud fra en kravspecifikation.
Målet er at binde HTML5, CSS3, Tailwind CSS, Vue 3, Vue Router, Pinia og en PHP-backend sammen i én sammenhængende enterprise-arkitektur.

11.1 Projektbeskrivelse: Enterprise Task Manager (SaaS)
Kursisterne skal bygge et avanceret projektstyringsværktøj (i stil med Trello eller Asana). Applikationen skal give brugere mulighed for at oprette projekter, styre opgaver, tracke statusser i realtid og gemme alt sikkert i en database via en PHP-backend.

11.2 Tekniske Kravspecifikationer
For at bestå afgangsprojektet skal applikationen opfylde følgende strukturelle og arkitektoniske minimumskrav:

Område Tekniske Krav (Skal opfyldes)
Frontend & DesignBygges i Vue 3 (Composition API). Al styling skal laves utility-first med Tailwind CSS. Interfacet skal være fuldt responsivt (mobil og desktop).
Routing (Vue Router)Minimum 3 ruter: Home/Login, Dashboard (opgaveskærm) og ProjectDetail/:id. Dashboardet skal være beskyttet af en Navigation Guard, så kun indloggede brugere kan se det.
State (Pinia)En useAuthStore til at håndtere login-status og brugerdata, samt en useTaskStore til at styre projekter og opgaver globalt. State skal persisteres, så et sideredirect ikke sletter data.
GenanvendelighedAl API-kommunikation skal trækkes ud i en genanvendelig Composable (useApi.js). Der skal designes mindst 3 genanvendelige UI-komponenter (f.eks. BaseButton, TaskCard, Modal).
Backend (PHP)En funktionel REST API-struktur i PHP. PHP skal modtage JSON-data, validere input mod sårbarheder, anvende korrekte HTTP-statuskoder og kommunikere med frontenden via CORS-headers.

11.3 Produktions-build og Deploy (npm run build)
Når applikationen er færdigudviklet i lokalmiljøet (npm run dev), er den ikke klar til internettet. Udviklingskoden indeholder unødvendig debug-logik og er splittet op i hundredvis af filer, som ville gøre appen langsom for en rigtig bruger.
Kursisterne skal lære at køre Vites build-optimeringsværktøj i terminalen:

npm run build
            

Vær forsigtig med at bruge denne kode.

Hvad sker der under et produktions-build?

  1. Minificering: Alt overflødigt mellemrum, kommentarer og linjeskift slettes fra JavaScript og CSS for at minimere filstørrelsen.
  2. Tree Shaking: Vue-motoren analyserer koden og fjerner automatisk de dele af frameworket, som kursisten ikke har brugt i sit projekt.
  3. Cache Busting / Hashing: Filerne navngives med unikke koder (f.eks. index-C8j2x9.js). Dette sikrer, at hvis du opdaterer appen på serveren, tvinges browseren til at hente den nye version i stedet for at vise en gammel, cachet version.

Resultatet er en ultralet, statisk folder kaldet /dist, som indeholder én index.html samt optimerede CSS- og JS-filer. Denne folder kan uploades direkte til enhver webserver sammen med PHP-backendfilerne.

11.4 Kriterier for Code Review og Bedømmelse
Når projektet afleveres, skal det gennemgå et professionelt Code Review baseret på følgende fire parametre:

  • Clean Code & SOLID: Er koden selvdokumenterende? Er funktioner små og fokuserede, eller er der tegn på spaghettikode?
  • Sikkerhed: Håndterer PHP-backenden data sikkert? Er der brugt parametriserede queries, og beskytter Vue mod XSS via standard interpolation?
  • Performance: Hentes sider via Lazy Loading? Er loops (v-for) forsynet med korrekte og unikke :key-attributter for at undgå unødvendige DOM-opdateringer?
  • UX (User Experience): Vises der en loading-indikator, mens API'et kaldes? Fejler appen yndefuldt, hvis backenden slukkes?

Tag skridtet videre med Vue.js

Brug denne viden til at bygge lynhurtige og skalerbare webapplikationer.