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.
Jeg forklarede WebJars og Thymeleaf i det sidste indlæg. Her er opsætningen, server- og klientsiden.
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>
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 } }
Todo
objekter
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:
/templates
og præfikset er .html
; i dette tilfælde forventer Thymeleaf en visning på /templates/vue.html
Her er koden på HTML-siden:
<script th:src="@{/webjars/axios/dist/axios.js}" src="https://cdn.jsdelivr.net/npm/[email protected]/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>
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
.
Lad os nu dykke ned i Vue-koden.
Vi ønsker at implementere flere funktioner:
Todo
elementerTodo
gennemført, skal det aktivere/deaktivere den completed
attributTodo
Todo
til listen over Todo
med følgende værdier:id
: Server-side beregnet ID som det maksimale af alle andre ID'er plus 1label
: værdien af feltet Label for label
completed
: indstillet til false
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 }
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>
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, }
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');
render()
-funktionenh()
for hyperscript opretter en virtuel node ud af objektet og dets egenskabertitle
med den værdigenererede serverside
På dette tidspunkt viser Vue titlen.
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>
Todo
-id'etTodo
-etikettencompleted
attribut er true
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 } }
props
-arrayet, så vi senere kan få adgang til detevent
, der udløste opkaldetI det forrige afsnit lavede jeg to fejl:
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 } }
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');
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ærdierstate
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>
Todo
objekteris
-attributten er afgørende for at klare den måde, browseren analyserer HTML på. Se Vue-dokumentationen for flere detaljerJeg har beskrevet den tilsvarende skabelon ovenfor.
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 } } }
create()
funktionTodo
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.
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: