7. Vite
7. Vite
In dit hoofdstuk bespreken we hoe we Vite kunnen gebruiken om een TypeScript project op te zetten en uitendelijk te bundelen en transpileren naar JavaScript code die in de browser kan draaien.
Om het gebruik van Vite te illustreren bouwen we een eenvoudige pagina waarop gebruikers boeken kunnen toevoegen aan hun collectie. Deze les plaatsen we alles op één pagina, volgende les breiden we de applicatie uit met meerdere pagina's waarop auteurs en uitgevers beheerd kunnen worden.
Notitie
Om de theorie uit deze lessen te verwerken, moet de development environment correct ingesteld zijn zoals beschreven in de development environment handleiding.
Vite
Aangezien TypeScript code niet uitgevoerd kan worden in de browser, kunnen we niet zomaar een .ts file aanmaken en daar TypeScript code in beginnen te schrijven. Vorige les hebben we een heel eenvoudig TypeScript project opgezet zonder gebruik te maken van een bundler, maar dit is niet ideaal. In realistische projecten gebeurt er veel meer dan het compileren van TypeScript files. Via een build tools zoals Vite worden verschillende files samengevoegd tot één productiebuild, worden er optimalisaties uitgevoerd, worden er polyfills toegevoegd voor features die nog niet in alle browsers ondersteund worden, worden afbeeldingen, CSS, en andere statische assets verwerkt, ...
Om een project aan te maken met een build tool, gebruiken we een package manager zoals npm, yarn, pnpm of bun. Deze package managers gebruiken een template die publiek op GitHub aangeboden wordt om een project te bootstrappen zodat we het niet zelf vanaf nul moeten configureren.[1]
Door onderstaand commando uit te voeren in een terminal naar keuze kunnen we een nieuw project aanmaken.
Het commando vraagt eerst naar de naam van het nieuwe project, deze naam moet
- minder dan 215 karakters lang zijn;
- mag niet beginnen met een punt of een underscore;
- mag geen hoofdletters bevatten;
- mag geen tekens bevatten die niet in een URL kunnen voorkomen (&, %, ?, ...).
Vervolgens moeten we een framework kiezen, aangezien we in deze cursus geen gebruik maken van een framework kiezen we Vanilla (JavaScript zonder framework). Tenslotte selecteren we TypeScript als variant.
â—‡ Project name:
│ javascript_lecture7_example_complete
│
â—‡ Select a framework:
│ Vanilla
│
â—‡ Select a variant:
│ TypeScript
│
â—‡ Install with pnpm and start now?
│ NoZoals de uitvoer van pnpm create vite aangeeft, moeten we nog drie commando's uitvoeren om het project te starten.
# Wijzig de huidige directory naar de nieuwe projectmap.
# Dit commando ziet er natuurlijk anders uit als je een andere naam hebt gekozen.
cd javascript_lecture7_example_complete
Info
In een degelijk JavaScript project zijn er linting en formatting regels toegevoegd. De linting regels zorgen ervoor dat er consistente code geschreven wordt en dat bepaalde praktijken, zoals het gebruik van var in plaats van let of const, vermeden worden
Het configureren van de linting regels valt buiten de scope van deze cursus, maar we verwijzen de geïnteresseerde lezer graag door naar de appendix.
Projectstructuur
Het gegenereerde project bevat een aantal interessante bestanden, verschillende van deze bestanden komen doorheen de rest van de JavaScript vakken terug. Hieronder bespreken we deze één per één.

node_modules
De node_modules map bevat alle bibliotheken en tools die nodig zijn om een JavaScript applicatie te ontwikkelen en publiceren. Daarnaast komen ook alle extra pakketten die jij, als ontwikkelaar, installeert in deze map te staan. Deze map mag niet mee op version control geplaatst worden, de map kan heel groot worden en is eenvoudig reproduceerbaar. Voeg deze map dus altijd toe aan je .gitignore file.
Info
De node_modules map kan eenvoudig gereproduceerd worden met onderstaand commando.
public
De map public bevat statische resources die niet mee gecompileerd, gebundeld of minified moeten worden en bevat standaard enkel iconen. Normaliter moet er niet veel toegevoegd worden aan deze folder, behalve een favicon en eventuele statische bestanden (bijvoorbeeld een CV (pdf) op een portfolio). Alle code, stylesheets en images komen in de src map te staan. Zo kunnen assets die uiteindelijk niet gebruikt worden, uitgesloten worden uit de production bundle.
src
Deze map bevat de eigenlijke code van de JavaScript applicatie, standaard bevat deze map een hele reeks bestanden. In deze cursus vertrekken we steeds van een leeg project, de inhoud van de src map mag je dus weggooien als je een nieuw project aanmaakt.
.gitignore
Elk JavaScript project (ongeacht het framework) wordt standaard geïnitialiseerd als een git project. Het .gitignore bestand bevat een opsomming van de bestanden die niet in version control geplaatst mogen worden, bijvoorbeeld de node_modules en dist (productiebuild) mappen.
package.json
In package.json worden alle geïnstalleerde pakketten opgesomd via twee attributen, dependencies en devDependencies.
Het eerste attribuut, dependencies, bevat een lijst van alle geïnstalleerde pakketten die relevant zijn voor de eindgebruiker. Deze libraries worden dus ook gekopieerd naar de productiebuild, eventueel geminified en geoptimaliseerd.
Het tweede attribuut, devDependencies, bevat een lijst van alle geïnstalleerde pakketten die enkel relevant zijn tijdens de ontwikkeling van de applicatie. Zaken zoals linters, transpilers, build-tools, en testing libraries, horen hier thuis. De devDependencies worden gebruikt om de productiebuild te genereren, maar worden verder niet gebruikt in de rest van de applicatie. Bijgevolg zijn deze libraries niet aanwezig in de productiebuild.
Verder kunnen er scripts geconfigureerd worden in package.json die bepaalde taken automatiseren (linting, formatting, building, servers starten, ...).
Tenslotte bevat package.json ook metadata over de applicatie, zoals de naam, versie, auteur, een beschrijving, het git repository, de package manager, ... Deze informatie wordt, onder anderen, gebruikt als je de library publiceert via npm.
pnpm-lock.yaml
Deze file beschrijft de exacte versies van elke library die in de node_modules map geïnstalleerd is. Alhoewel package.json de dependencies beschrijft, kunnen de gebruikte versies tussen twee installs toch verschillen. Via ^ en ~ worden version ranges aangeduid in package.json, de package manager kan dan gecachte versies gebruiken of de laatste versie downloaden, zolang deze binnen de version range vallen.[2]
tsconfig.json
Via tsconfig.json beschrijven we hoe TypeScript de applicatie moet compileren naar JavaScript en bepalen we welke dingen toegestaan zijn in de code en welke niet. Af en toe passen we iets aan in de configuratie van deze bestanden, maar normaliter is de standaardconfiguratie voldoende voor de projecten in deze cursus.
index.html
Dit is de enige pure HTML-file die we gebruiken in het Vite project. We passen dit bestand zo weinig mogelijk aan en proberen alle content via TypeScript in te laden. Eventuele statische onderdelen die op elke pagina aanwezig moeten zijn, zoals een navbar, kunnen hier toegevoegd worden, maar de dynamische onderdelen (de pagina's van de app), worden via JavaScript ingeladen.
De inhoud van het head tag kan natuurlijk wel aangepast worden en we kunnen onderaan in de body ook JavaScript libraries inladen via een CDN, al is het beter om dit via een npm library te doen.
Het gemarkeerde <div> element vormt de root van onze pagina, doorheen de cursus bouwen vullen we deze div op via JavaScript. De UI wordt dus niet in deze file gedefinieerd. In latere cursussen werken we nog steeds op dezelfde manier, maar wordt de div opgevuld via het React framework en doen we dit niet meer zelf.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>javascript_lecture7_example_complete</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>Het script tag maakt gebruik van ES modules.
Begrip: Modules
Moderne JavaScript code maakt gebruik van ES Modules (ESM) om code te structureren in JavaScript en TypeScript projecten[3]. Via modules kan code verspreid worden over verschillende bestanden, zo blijft de code overzichtelijk, ook in grote projecten. Variabelen en functies die in het éné bestand geëxporteerd worden, kunnen in een ander bestand geïmporteerd worden.
In een project met ES modules, worden twee soorten exports onderscheiden, named exports en default exports. Via named exports leg je de naam van de variabele of functie vast, het is dus onmogelijk om de export te importeren onder een andere naam. Een bestand kan een willekeurig aantal named exports bevatten, deze worden allemaal samengevoegd in een object. Tijdens import moet deconstructing gebruikt worden om de verschillende named exports te importeren.
Default exports zijn exports die onder eender welke naam geïmporteerd kunnen worden. Zoals de naam doet vermoeden is een default export de "standaard" export van een bestand, het volgt dus dat er maar één default export per file kan zijn.
// Een named export, moet geïmporteerd worden onder de naam helloWorld
// en moet binnen accolades staan.
export const helloWorld = () => console.log('Hello World')
// Een default export, kan onder eender welke naam geïmporteerd worden
// en moet niet binnen accolades staan.
const helloWorlDefault = () => console.log('Hello World')
export default helloWorlDefault// Import van een named export, accolades zijn vereist.
import {helloWorld} from './helloWorld.ts'
// Import van een default export, accolades zijn niet nodig.
import helloWorlDefault from './helloWorld.ts'
// Import van een defauklt export, onder aan andere naam.
import aRenamedHalloWorldDefaultExport from './helloWorld.ts':::
Bootstrap
In het verdere verloop van de les maken we gebruik van Bootstrap om de UI van onze applicatie te bouwen. Alhoewel we natuurlijk link tags en scripts kunnen toevoegen aan index.html is het interessanter om gebruik te maken van een package manager zoals bun, npm of pnpm. Aangezien het project aangemaakt is met bun, gebruiken we dit ook om Bootstrap te installeren. Hiervoor voer je onderstaande commando's in je terminal.
Dit commando heeft Bootstrap (en de dependencies) toegevoegd aan de node_modules map en heeft deze daarnaast ook toegevoegd aan package.json. Hierdoor kunnen we alle dependencies (niet enkel Bootstrap) terug installeren als we de code van het project clonen van git.
{
"name": "javascript_lecture7_example_complete",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "~5.9.3",
"vite": "^8.0.1"
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8"
}
}Nu dat de library geïnstalleerd is, moeten we Bootstrap nog importeren in de JavaScript code. Dit doen we in main.ts aangezien deze file het ingangspunt van de applicatie is.
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap'QuerySelector
Nu dat de configuratie afgewerkt is, wordt het tijd om iets op het scherm te tonen, hiervoor moeten we eerst de div ophalen met het id app zodat we vervolgens, via de innerHTML property, iets kunnen tonen.
Natuurlijk werken de document.getElementById, document.getElementByClassName, ... methodes nog steeds, toch verkiezen we in TypeScript de document.querySelector en document.querySelectorAll methodes. Deze laatste twee methodes zijn generisch, wat betekent dat we het soort HTML-element kunnen aangeven. Zo is de IDE of editor in staat om foutmeldingen te tonen als je properties probeert te gebruiken die niet bestaan, het wordt bijvoorbeeld onmogelijk om de type property in te stellen of uit te lezen voor een div element. Voor een input element gaat dat dan weer wel.
De generische parameter wordt aangegeven tussen kleiner dan en groter dan symbolen die na de methodenaam geplaatst worden. We komen zo tot onderstaande code.
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap'
const root = document.querySelector<HTMLDivElement>('#app')
root.innerHTML = '<h1 class="mt-3 ms-3">Hello world</h1>'Alhoewel deze code goed lijkt en ook werkt, geeft TypeScript toch nog een foutmelding.
Het probleem wordt veroorzaakt doordat TypeScript los staat van de markdown, de compiler heeft dus geen weet van de elementen die bestaan op de HTML-pagina. Bijgevolg weet TypeScript niet of er ook daadwerkelijk een div met id app is. Om dit probleem op te lossen zijn er twee opties:
- Voeg een if toe die eerst controleert of het root element bestaat en dan pas de HTML-code toevoegt.
- Gebruikt de non-null assertion.
Aangezien wij als programmeur zeker zijn dat het element bestaat kiezen we hier voor de tweede optie, door de ! operator toe te voegen vertellen we TypeScript dat we de foutmelding genegeerd mag worden omdat het onmogelijk is dat het element null is.
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap'
const root = document.querySelector<HTMLDivElement>('#app')!
root.innerHTML = '<h1 class="mt-3 ms-3">Hello world</h1>'Nu dat er een basis aan code is, kunnen we het project opstarten en uittesten. Hiervoor moet een development server gestart worden via onderstaand commando. Deze server blijft aan staan en ververst de webpagina automatisch als er aanpassingen gedaan worden.
De applicatie is nu beschikbaar op http://localhost:5173/.

HTML pagina inladen
Nu dat we een hello-world voorbeeld gebouwd hebben, kunnen we dezelfde werkwijze gebruiken om een echte pagina te bouwen. Alhoewel dit zou werken, is het niet ideaal om grote hoeveelheden HTML-code in een TypeScript bestand te plaatsen, dit wordt snel heel onoverzichtelijk.
Aangezien we een bundler gebruiken, kunnen we HTML-code afzonderen in een afzonderlijk bestand, vervolgens importeren we dit en renderen we het via de innerHTML property. Om het HTML-bestand te importeren moet de ?raw parameter toegevoegd worden aan de bestandsnaam, zo importeert Vite het bestand als plain tekst.
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap'
import homePage from './pages/home/home.html?raw'
const root = document.querySelector<HTMLDivElement>('#app')!
root.innerHTML = homePage<div class="container mt-5">
<h2 class="mb-4">My library</h2>
<hr/>
<form id="bookForm">
<div class="d-md-flex justify-content-between gap-4">
<div class="mb-3 flex-grow-1">
<label for="title" class="form-label">Title</label>
<input type="text" class="form-control" id="title" name="title" required>
</div>
<div class="mb-3">
<label for="year" class="form-label">Publication Year</label>
<input type="number" class="form-control" id="year" name="year" min="1000" max="9999" required>
</div>
</div>
<div class="d-md-flex justify-content-between gap-4">
<div class="mb-3 flex-grow-1">
<label for="seriesName" class="form-label">Series Name</label>
<input type="text" class="form-control" name="seriesName" id="seriesName">
</div>
<div class="mb-3">
<label for="seriesNumber" class="form-label">Series Number</label>
<input type="number" class="form-control" name="seriesNumber" id="seriesNumber" min="1">
</div>
</div>
<div class="mb-3">
<label for="author" class="form-label">Author</label>
<input type="text" class="form-control" id="author" name="author" required>
</div>
<div class="mb-3">
<label class="form-label">Type</label>
<select class="form-select" id="type" name="type" required>
<option value="">Select type</option>
<option value="ebook">Ebook</option>
<option value="audiobook">Audiobook</option>
<option value="print">Print</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Add to library</button>
</form>
<hr/>
<table class="table table-bordered mt-3">
<thead>
<tr>
<th>Title</th>
<th>Publication Year</th>
<th>Author</th>
<th>Type</th>
<th>Series</th>
<th>Delete</th>
</tr>
</thead>
<tbody id="bookTableBody">
<!-- Book entries will be added here -->
</tbody>
</table>
</div>Na deze aanpassing ziet de webpagina er als volgt uit.

Info
Het import statement kan nog aangepast worden zodat de ?raw parameter niet toegevoegd moet worden, hier is echter wat extra configuratie voor nodig. We verwijzen de geïnteresseerde lezer door naar de appendix.
Model vastleggen
De applicatie moet dienen om een lijst van boeken te bewaren, we moeten dus eerst een interface definiëren die beschrijft welke informatie er bijgehouden moet worden voor een boek.
export type BookType = 'ebook' | 'audiobook' | 'print'
export interface Book {
id: string
title: string
publicationYear: number
series?: {
name: string
number: number
}
author: string
type: BookType
}Vervolgens kunnen we dit model gebruiken om een array te definiëren waarin alle boeken in de collectie geplaatst worden. Aangezien de interface Book enkel gebruikt wordt als een type, gebruiken we een type-only import, dit is een import die enkel bestaat tijdens het schrijven van je code. In een productiebuild wordt al deze type-informatie verwijderd (en is de productiebuild eventueel kleiner).
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap'
import type {Book} from './models/book.ts'
import homePage from './pages/home/home.html?raw'
const root = document.querySelector<HTMLDivElement>('#app')!
root.innerHTML = homePage
let books: Book[] = []De rest van de code bevat weinig nieuws, we gebruiken heel wat TypeScript types/interfaces, maar verder komt de logica allemaal uit de vorige lessen.
import type {Book, BookType} from './models/book.ts'
// Rest van de code verborgen
const bookList = document.querySelector<HTMLTableSectionElement>('#bookTableBody')!
const bookFrom = document.querySelector<HTMLFormElement>('#bookForm')!
const titleInput = document.querySelector<HTMLInputElement>('input[name="title"]')!
const yearInput = document.querySelector<HTMLInputElement>('input[name="year"]')!
const seriesNameInput = document.querySelector<HTMLInputElement>('input[name="seriesName"]')!
const seriesNumberInput = document.querySelector<HTMLInputElement>('input[name="seriesNumber"]')!
const authorInput = document.querySelector<HTMLInputElement>('input[name="author"]')!
const typeSelect = document.querySelector<HTMLInputElement>('select[name="type"]')!
function renderBooks() {
bookList.innerHTML = ''
books.forEach(b => {
bookList.appendChild(buildBookRow(b))
})
}
function buildBookRow(book: Book): HTMLTableRowElement {
const bookRow = document.createElement('tr')
bookRow.appendChild(createTableCell(book.title))
bookRow.appendChild(createTableCell(book.publicationYear.toString()))
bookRow.appendChild(createTableCell(book.author))
bookRow.appendChild(createTableCell(book.type))
bookRow.appendChild(createTableCell(book.series ? `${book.series?.name} (${book.series?.number})` : ''))
const deleteButton = document.createElement('button')
deleteButton.innerHTML = 'Delete'
deleteButton.addEventListener('click', () => {
books = books.filter(b => b.id !== book.id)
renderBooks()
})
bookRow.appendChild(createTableCell(deleteButton))
return bookRow
}
function createTableCell(content: string | HTMLElement): HTMLTableCellElement {
const cell = document.createElement('td')
if (typeof content === 'string') {
cell.innerHTML = content
} else {
cell.appendChild(content)
}
return cell
}
function emptyForm(): void {
titleInput.value = ''
seriesNameInput.value = ''
seriesNumberInput.value = ''
typeSelect.value = ''
authorInput.value = ''
yearInput.value = ''
}
bookFrom.addEventListener('submit', (evt: SubmitEvent) => {
evt.preventDefault()
books.push({
id: window.crypto.randomUUID(),
title: titleInput.value,
series:
seriesNameInput.value === ''
? undefined
: {
name: seriesNameInput.value,
number: Number(seriesNumberInput.value),
},
type: typeSelect.value as BookType,
author: authorInput.value,
publicationYear: Number(yearInput.value),
})
emptyForm()
renderBooks()
})Voorbeeldcode
Uitgewerkt lesvoorbeeld met commentaar
Het is natuurlijk ook mogelijk om een project vanaf nul te configureren, maar dit vergt wat opzoekwerk over hoe de TypeScript Compiler en Vite werken. ↩︎
Voor meer informatie over wat de verschillende delen van de versienummers betekenen, verwijzen we naar de Semantic Versioning documentatie. Voor meer info over version ranges verwijzen we naar de npm documentatie. ↩︎
Alhoewel ESM de beste manier is om code te structureren, gebruiken verschillende oudere libraries nog steeds het Common JS (CJS) formaat. We raden elke lezer aan om hier meer over te lezen. ↩︎