En mi post anterior senté las bases para seguir construyendo; ahora es el momento de empezar "de verdad".
Había oído hablar mucho de Vue.js. Además, un amigo que pasó de desarrollador a gerente me contó cosas buenas sobre Vue, lo que despertó aún más mi interés. Decidí echarle un vistazo: será el primer framework JavaScript "liviano" que estudiaré, desde el punto de vista de un novato, que es lo que soy.
Disposición del trabajo
Expliqué WebJars y Thymeleaf en la última publicación. Aquí se muestra la configuración, tanto del lado del servidor como del cliente.
Del lado del servidor
Así es como integro ambos en el 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 en sí; decidí utilizar el enfoque regular, no reactivo.
- Integración de Spring Boot con Thymeleaf
- Localizador de WebJars, para evitar especificar la versión de Vue en el lado del cliente
- ¡Vue, por fin!
Estoy usando el enrutador Kotlin y los DSL de Bean en el lado de Spring Boot:
fun vue(todos: List<Todo>) = router { //1 GET("/vue") { ok().render("vue", mapOf("title" to "Vue.js", "todos" to todos)) //2-3 } }
- Pasar una lista estática de objetos
Todo
- Vea abajo
- Pasar el modelo a Thymeleaf
Si estás acostumbrado a desarrollar APIs, estarás familiarizado con la función body()
; devuelve la carga útil directamente, probablemente en formato JSON. La render()
pasa el flujo a la tecnología de visualización, en este caso, Thymeleaf. Acepta dos parámetros:
- El nombre de la vista. De manera predeterminada, la ruta es
/templates
y el prefijo es.html
; en este caso, Thymeleaf espera una vista en/templates/vue.html
- Un mapa modelo de pares clave-valor
Del lado del cliente
Aquí está el código en el lado HTML:
<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 ayuda a realizar solicitudes HTTP
- Vue en sí
- Nuestro código del lado del cliente
- Establecer los datos
Como se explicó en el artículo de la semana pasada, uno de los beneficios de Thymeleaf es que permite tanto la representación estática de archivos como la representación del lado del servidor. Para que la magia funcione, especifico una ruta del lado del cliente, es decir , src
, y una ruta del lado del servidor, es decir , th:src
.
El código Vue
Ahora, profundicemos en el código Vue.
Queremos implementar varias características:
- Después de cargar la página, la página debería mostrar todos los elementos
Todo
- Al hacer clic en una casilla de verificación
Todo
completado, debe activar o desactivar el atributocompleted
- Al hacer clic en el botón Limpiar , se eliminan todas
Todo
completadas. - Al hacer clic en el botón Agregar , debería agregarse un
Todo
a la lista deTodo
con los siguientes valores:-
id
: ID calculado del lado del servidor como el máximo de todos los demás ID más 1 -
label
: valor del campo Etiqueta paralabel
-
completed
: establecido enfalse
-
Nuestros primeros pasos en Vue
El primer paso es iniciar el framework. Ya hemos configurado la referencia para nuestro archivo vue.js
personalizado más arriba.
document.addEventListener('DOMContentLoaded', () => { //1 // The next JavaScript code snippets will be inside the block }
- Ejecute el bloque cuando el DOM haya terminado de cargarse
El siguiente paso es dejar que Vue administre parte de la página. En el lado HTML, debemos decidir qué parte de nivel superior administra Vue. Podemos elegir un <div>
arbitrario y cambiarlo más tarde si es necesario.
<div id="app"> </div>
En el lado de JavaScript, creamos una aplicación , pasando el selector CSS del HTML anterior <div>
.
Vue.createApp({}).mount('#app');
En este punto, lanzamos Vue cuando se carga la página, pero no sucede nada visible.
El siguiente paso es crear una plantilla Vue. Una plantilla Vue es una <template>
HTML normal administrada por Vue. Puedes definir Vue en Javascript, pero yo prefiero hacerlo en la página HTML.
Comencemos con una plantilla raíz que pueda mostrar el título.
<template id="todos-app"> <!--1--> <h1>{{ title }}</h1> <!--2--> </template>
- Establezca el ID para facilitar la vinculación
- Utilice la propiedad
title
; aún queda por configurar
En el lado de JavaScript, debemos crear el código de administración.
const TodosApp = { props: ['title'], //1 template: document.getElementById('todos-app').innerHTML, }
- Declarar la propiedad
title
, la utilizada en la plantilla HTML
Por último, debemos pasar este objeto cuando creamos la aplicación:
Vue.createApp({ components: { TodosApp }, //1 render() { //2 return Vue.h(TodosApp, { //3 title: window.vueData.title, //4 }) } }).mount('#app');
- Configurar el componente
- Vue espera la función
render()
-
h()
para hiperíndice crea un nodo virtual a partir del objeto y sus propiedades - Inicialice la propiedad
title
con el valor generado en el servidor
En este punto, Vue muestra el título.
Interacciones básicas
En este punto, podemos implementar la acción cuando el usuario hace clic en una casilla de verificación: debe actualizarse en el estado del lado del servidor.
Primero, agregué una nueva plantilla anidada de Vue para la tabla que muestra la Todo
. Para no alargar la publicación, evitaré describirla en detalle. Si te interesa, echa un vistazo al código fuente .
Aquí está el código de la plantilla de línea de partida, respectivamente JavaScript y 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>
- Mostrar el ID
Todo
- Mostrar la etiqueta
Todo
- Marque la casilla si su atributo
completed
estrue
Vue permite el manejo de eventos a través de la sintaxis @
.
<input type="checkbox" :checked="todo.completed" @click="check" />
Vue llama a la función check()
de la plantilla cuando el usuario hace clic en la línea. Definimos esta función en un parámetro setup()
:
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 } }
- Acepte la matriz
props
para que podamos acceder a ella más tarde. - Vue pasa el
event
que activó la llamada - Axios es una biblioteca de JavaScript que simplifica las llamadas HTTP
- El lado del servidor debe proporcionar una API; está fuera del alcance de esta publicación, pero no dudes en consultar el código fuente.
- Carga útil JSON
- Devolvemos todas las funciones definidas para hacerlas accesibles desde HTML
Modelo del lado del cliente
En la sección anterior cometí dos errores:
- No logré ningún modelo local
- No utilicé el método de llamada de la respuesta HTTP
Lo haremos implementando la siguiente función, que es la limpieza de tareas completadas.
Ahora sabemos cómo manejar eventos a través de Vue:
<button class="btn btn-warning" @click="cleanup">Cleanup</button>
En el objeto TodosApp
, agregamos una función con el mismo nombre:
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 } }
- Como arriba
- Axios ofrece conversión JSON automatizada de la llamada HTTP
-
state
es donde almacenamos el modelo.
En la semántica de Vue, el modelo de Vue es un contenedor de datos que queremos que sean reactivos . Reactivo significa un enlace bidireccional entre la vista y el modelo. Podemos hacer que un valor existente sea reactivo pasándolo al método ref()
:
En Composition API, la forma recomendada de declarar el estado reactivo es usando la función
ref()
.
ref()
toma el argumento y lo devuelve envuelto dentro de un objeto ref con una propiedad .value.
Para acceder a las referencias en la plantilla de un componente, declárelas y devuélvalas desde la función
setup()
de un componente.
--Declaración del estado reactivo
Vamos a hacerlo:
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');
- Obtenga el conjunto de datos en la página HTML, a través de Thymeleaf, como se explicó anteriormente
- Cambiamos la forma en que configuramos el
title
. No es necesario ya que no hay un enlace bidireccional: no actualizamos el título del lado del cliente, pero prefiero mantener la coherencia en el manejo de todos los valores. - Devolver las referencias, según las expectativas de Vue
- Mira, mamá, estoy usando el operador de propagación de JavaScript.
- Configurar los atributos del objeto desde el
state
En este punto, tenemos un modelo reactivo del lado del cliente.
En el lado HTML, utilizamos los atributos Vue relevantes:
<tbody> <tr is="vue:todo-line" v-for="todo in todos" :key="todo.id" :todo="todo"></tr> <!--1-2--> </tbody>
- Recorrer la lista de objetos
Todo
- El atributo
is
es fundamental para gestionar la forma en que el navegador analiza el código HTML. Consulte la documentación de Vue para obtener más detalles.
He descrito la plantilla correspondiente más arriba.
Actualizando el modelo
Ahora podemos implementar una nueva función: agregar un nuevo Todo
desde el cliente. Al hacer clic en el botón Agregar , leemos el valor del campo Etiqueta , enviamos los datos a la API y actualizamos el modelo con la respuesta.
Aquí está el código actualizado:
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 } } }
- Crea un contenedor reactivo alrededor del título cuyo alcance esté limitado a la función
- La función
create()
propiamente dicha - Añade el nuevo objeto JSON devuelto por la llamada API a la lista de
Todo
- Restablecer el valor del campo
- Reemplazar toda la lista al eliminar; el mecanismo es el mismo
En el lado HTML, agregamos un botón y lo vinculamos a la función create()
. Asimismo, agregamos el campo Label y lo vinculamos al modelo.
<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 vincula la función create()
al botón HTML. La llama de forma asincrónica y actualiza la lista reactiva Todo
con el nuevo elemento devuelto por la llamada. Hacemos lo mismo con el botón Limpiar para eliminar los objetos Todo
marcados.
Tenga en cuenta que no implementé intencionalmente ningún código de manejo de errores para evitar que el código fuera más complejo de lo necesario. Me detendré aquí porque obtuvimos suficientes conocimientos para una primera experiencia.
Conclusión
En esta publicación, di mis primeros pasos para ampliar una aplicación SSR con Vue. Fue bastante sencillo. El mayor problema que encontré fue que Vue reemplazara la plantilla de línea: no leí la documentación en profundidad y pasé por alto el atributo is
.
Sin embargo, tuve que escribir bastantes líneas de JavaScript, aunque utilicé Axios para ayudarme con las llamadas HTTP y no administré los errores.
En la próxima publicación, implementaré las mismas características con Alpine.js.
El código fuente completo de esta publicación se puede encontrar en GitHub:
Ir más allá: