I mit tidligere indlæg lagde jeg grunden til at bygge videre på; nu er det tid til at starte "for alvor".
Jeg har hørt meget om Vue.js. Derudover fortalte en ven, der gik fra udvikler til manager, mig gode ting om Vue, hvilket yderligere vækkede min interesse. Jeg besluttede at tage et kig på det: det vil være den første "lette" JavaScript-ramme, jeg vil studere - fra en nybegynders synspunkt, som jeg er.
Udlægning af arbejdet
Jeg forklarede WebJars og Thymeleaf i det sidste indlæg. Her er opsætningen, server- og klientsiden.
Server-side
Sådan integrerer jeg begge i POM:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <!--1--> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <!--2--> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator</artifactId> <!--3--> <version>0.52</version> </dependency> <dependency> <groupId>org.webjars.npm</groupId> <artifactId>vue</artifactId> <!--4--> <version>3.4.34</version> </dependency> </dependencies>
- Spring Boot selv; Jeg besluttede mig for den almindelige, ikke-reaktive tilgang
- Spring Boot Thymeleaf integration
- WebJars locator, for at undgå at angive Vue-versionen på klientsiden
- Vue, endelig!
Jeg bruger Kotlin Router og Bean DSL'er på Spring Boot-siden:
fun vue(todos: List<Todo>) = router { //1 GET("/vue") { ok().render("vue", mapOf("title" to "Vue.js", "todos" to todos)) //2-3 } }
- Send en statisk liste over
Todo
objekter - Se nedenfor
- Send modellen til Thymeleaf
Hvis du er vant til at udvikle API'er, er du bekendt med body()
-funktionen; det returnerer nyttelasten direkte, sandsynligvis i JSON-format. render()
sender flowet til visningsteknologien, i dette tilfælde Thymeleaf. Den accepterer to parametre:
- Udsigtens navn. Som standard er stien
/templates
og præfikset er.html
; i dette tilfælde forventer Thymeleaf en visning på/templates/vue.html
- Et modelkort over nøgleværdi-par
Kundesiden
Her er koden på HTML-siden:
<script th:src="@{/webjars/axios/dist/axios.js}" src="https://cdn.jsdelivr.net/npm/axios@1.7/dist/axios.min.js"></script> <!--1--> <script th:src="@{/webjars/vue/dist/vue.global.js}" src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script> <!--2--> <script th:src="@{/vue.js}" src="../static/vue.js"></script> <!--3--> <script th:inline="javascript"> /*<![CDATA[*/ window.vueData = { <!--4--> title: /*[[${ title }]]*/ 'A Title', todos: /*[[${ todos }]]*/ [{ 'id': 1, 'label': 'Take out the trash', 'completed': false }] }; /*]]>*/ </script>
- Axios hjælper med at lave HTTP-anmodninger
- Vue selv
- Vores kode på klientsiden
- Indstil dataene
Som forklaret i sidste uges artikel, er en af Thymeleafs fordele, at den tillader både statisk filgengivelse og gengivelse på serversiden. For at få magien til at virke, specificerer jeg en sti på klientsiden, dvs. , src
, og en sti på serversiden, dvs. th:src
.
Vue-koden
Lad os nu dykke ned i Vue-koden.
Vi ønsker at implementere flere funktioner:
- Efter sideindlæsningen skal siden vise alle
Todo
elementer - Når du klikker på afkrydsningsfeltet
Todo
gennemført, skal det aktivere/deaktivere dencompleted
attribut - Når du klikker på knappen Oprydning , sletter den alle fuldførte
Todo
- Når du klikker på knappen Tilføj , skal den tilføje en
Todo
til listen overTodo
med følgende værdier:-
id
: Server-side beregnet ID som det maksimale af alle andre ID'er plus 1 -
label
: værdien af feltet Label forlabel
-
completed
: indstillet tilfalse
-
Vores første skridt ind i Vue
Det første skridt er at bootstrap rammen. Vi har allerede oprettet referencen til vores tilpassede vue.js
-fil ovenfor.
document.addEventListener('DOMContentLoaded', () => { //1 // The next JavaScript code snippets will be inside the block }
- Kør blokken, når DOM er færdig med at indlæse
Det næste trin er at lade Vue administrere en del af siden. På HTML-siden skal vi beslutte, hvilken del på øverste niveau Vue administrerer. Vi kan vælge en vilkårlig <div>
og ændre den senere, hvis det er nødvendigt.
<div id="app"> </div>
På JavaScript-siden opretter vi en app , der passerer CSS-vælgeren for den tidligere HTML <div>
.
Vue.createApp({}).mount('#app');
På dette tidspunkt starter vi Vue, når siden indlæses, men der sker ikke noget synligt.
Det næste trin er at oprette en Vue -skabelon . En Vue-skabelon er en almindelig HTML <template>
der administreres af Vue. Du kan definere Vue i Javascript, men jeg foretrækker at gøre det på HTML-siden.
Lad os starte med en rodskabelon, der kan vise titlen.
<template id="todos-app"> <!--1--> <h1>{{ title }}</h1> <!--2--> </template>
- Indstil ID for nem binding
- Brug
title
egenskaben; det mangler at blive sat op
På JavaScript-siden skal vi oprette administrationskoden.
const TodosApp = { props: ['title'], //1 template: document.getElementById('todos-app').innerHTML, }
- Erklær
title
egenskaben, den der bruges i HTML-skabelonen
Endelig skal vi videregive dette objekt, når vi opretter appen:
Vue.createApp({ components: { TodosApp }, //1 render() { //2 return Vue.h(TodosApp, { //3 title: window.vueData.title, //4 }) } }).mount('#app');
- Konfigurer komponenten
- Vue forventer
render()
-funktionen -
h()
for hyperscript opretter en virtuel node ud af objektet og dets egenskaber - Initialiser
title
med den værdigenererede serverside
På dette tidspunkt viser Vue titlen.
Grundlæggende interaktioner
På dette tidspunkt kan vi implementere handlingen, når brugeren klikker på et afkrydsningsfelt: det skal opdateres på serversiden.
Først tilføjede jeg en ny indlejret Vue-skabelon til tabellen, der viser Todo
. For at undgå at forlænge indlægget, vil jeg undgå at beskrive det i detaljer. Hvis du er interesseret, så tag et kig på kildekoden .
Her er startlinjeskabelonens kode, henholdsvis JavaScript og HTML:
const TodoLine = { props: ['todo'], template: document.getElementById('todo-line').innerHTML }
<template id="todo-line"> <tr> <td>{{ todo.id }}</td> <!--1--> <td>{{ todo.label }}</td> <!--2--> <td> <label> <input type="checkbox" :checked="todo.completed" /> </label> </td> </tr> </template>
- Vis
Todo
-id'et - Vis
Todo
-etiketten - Marker afkrydsningsfeltet, hvis dens
completed
attribut ertrue
Vue tillader hændelseshåndtering via @
-syntaksen.
<input type="checkbox" :checked="todo.completed" @click="check" />
Vue kalder skabelonens check()
funktion, når brugeren klikker på linjen. Vi definerer denne funktion i en setup()
parameter:
const TodoLine = { props: ['todo'], template: document.getElementById('todo-line').innerHTML, setup(props) { //1 const check = function (event) { //2 const { todo } = props axios.patch( //3 `/api/todo/${todo.id}`, //4 { checked: event.target.checked } //5 ) } return { check } //6 } }
- Accepter
props
-arrayet, så vi senere kan få adgang til det - Vue sender den
event
, der udløste opkaldet - Axios er et JavaScript-lib, der forenkler HTTP-kald
- Serversiden skal levere en API; det er uden for dette indlægs rammer, men du er velkommen til at tjekke kildekoden.
- JSON nyttelast
- Vi returnerer alle definerede funktioner for at gøre dem tilgængelige fra HTML
Model på klientsiden
I det forrige afsnit lavede jeg to fejl:
- Jeg styrede ikke nogen lokal model
- Jeg brugte ikke HTTP-svarets opkaldsmetode
Det vil vi gøre ved at implementere den næste funktion, som er oprydning af afsluttede opgaver.
Vi ved nu, hvordan vi håndterer begivenheder via Vue:
<button class="btn btn-warning" @click="cleanup">Cleanup</button>
På TodosApp
objektet tilføjer vi en funktion af samme navn:
const TodosApp = { props: ['title', 'todos'], components: { TodoLine }, template: document.getElementById('todos-app').innerHTML, setup() { const cleanup = function() { //1 axios.delete('/api/todo:cleanup').then(response => { //1 state.value.todos = response.data //2-3 }) } return { cleanup } //1 } }
- Som ovenfor
- Axios tilbyder automatisk JSON-konvertering af HTTP-kaldet
-
state
er hvor vi opbevarer modellen
I Vues semantik er Vue-modellen en indpakning omkring data, som vi ønsker skal være reaktive . Reaktiv betyder tovejsbinding mellem udsigten og modellen. Vi kan gøre en eksisterende værdi reaktiv ved at overføre den til ref()
metoden:
I Composition API er den anbefalede måde at erklære reaktiv tilstand på at bruge
ref()
-funktionen.
ref()
tager argumentet og returnerer det pakket ind i et ref-objekt med en .value-egenskab.
For at få adgang til refs i en komponents skabelon skal du deklarere og returnere dem fra en komponents
setup()
-funktion.
Lad os gøre det:
const state = ref({ title: window.vueData.title, //1-2 todos: window.vueData.todos, //1 }) createApp({ components: { TodosApp }, setup() { return { ...state.value } //3-4 }, render() { return h(TodosApp, { todos: state.value.todos, //5 title: state.value.title, //5 }) } }).mount('#app');
- Hent datasættet på HTML-siden via Thymeleaf, som forklaret ovenfor
- Vi ændrer måden, vi angiver
title
på. Det er ikke nødvendigt, da der ikke er nogen tovejsbinding - vi opdaterer ikke titlen på klientsiden, men jeg foretrækker at holde håndteringen sammenhængende på tværs af alle værdier - Returner dommerne i henhold til Vues forventninger
- Se, mor, jeg bruger JavaScript-spredningsoperatoren
- Konfigurer objektets attributter fra
state
På dette tidspunkt har vi en reaktiv model på klientsiden.
På HTML-siden bruger vi de relevante Vue-attributter:
<tbody> <tr is="vue:todo-line" v-for="todo in todos" :key="todo.id" :todo="todo"></tr> <!--1-2--> </tbody>
- Sløjfe over listen over
Todo
objekter -
is
-attributten er afgørende for at klare den måde, browseren analyserer HTML på. Se Vue-dokumentationen for flere detaljer
Jeg har beskrevet den tilsvarende skabelon ovenfor.
Opdatering af modellen
Vi kan nu implementere en ny funktion: tilføje en ny Todo
fra klienten. Når vi klikker på knappen Tilføj , læser vi Label- feltets værdi, sender dataene til API'et og opdaterer modellen med svaret.
Her er den opdaterede kode:
const TodosApp = { props: ['title', 'todos'], components: { TodoLine }, template: document.getElementById('todos-app').innerHTML, setup() { const label = ref('') //1 const create = function() { //2 axios.post('/api/todo', { label: label.value }).then(response => { state.value.todos.push(response.data) //3 }).then(() => { label.value = '' //4 }) } const cleanup = function() { axios.delete('/api/todo:cleanup').then(response => { state.value.todos = response.data //5 }) } return { label, create, cleanup } } }
- Opret en reaktiv indpakning omkring titlen, hvis omfang er begrænset til funktionen
- Den egentlige
create()
funktion - Føj det nye JSON-objekt, der returneres af API-kaldet, til listen over
Todo
- Nulstil feltets værdi
- Erstat hele listen ved sletning; mekanismen er den samme
På HTML-siden tilføjer vi en knap og binder til create()
-funktionen. Ligeledes tilføjer vi feltet Label og binder det til modellen.
<form> <div class="form-group row"> <label for="new-todo-label" class="col-auto col-form-label">New task</label> <div class="col-10"> <input type="text" id="new-todo-label" placeholder="Label" class="form-control" v-model="label" /> </div> <div class="col-auto"> <button type="button" class="btn btn-success" @click="create">Add</button> </div> </div> </form>
Vue binder funktionen create()
til HTML-knappen. Det kalder det asynkront og opdaterer den reaktive Todo
liste med det nye element, der returneres af opkaldet. Vi gør det samme for knappen Oprydning for at fjerne afkrydsede Todo
objekter.
Bemærk, at jeg ikke med vilje implementerede nogen fejlhåndteringskode for at undgå at gøre koden mere kompleks end nødvendigt. Jeg stopper her, da vi har fået nok indsigt til en første oplevelse.
Konklusion
I dette indlæg tog jeg mine første skridt i at udvide en SSR-app med Vue. Det var ret ligetil. Det største problem, jeg stødte på, var, at Vue skulle erstatte linjeskabelonen: Jeg læste ikke dokumentationen grundigt og savnede is
-attributten.
Jeg var dog nødt til at skrive en del linjer JavaScript, selvom jeg brugte Axios til at hjælpe mig med HTTP-kald og ikke klarede fejl.
I det næste indlæg vil jeg implementere de samme funktioner med Alpine.js.
Den komplette kildekode til dette indlæg kan findes på GitHub:
Gå videre: