paint-brush
Ampliando el cliente con Vue.jspor@nfrankel
Nueva Historia

Ampliando el cliente con Vue.js

por Nicolas Fränkel17m2024/09/26
Read on Terminal Reader

Demasiado Largo; Para Leer

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.
featured image - Ampliando el cliente con Vue.js
Nicolas Fränkel HackerNoon profile picture
0-item


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>
  1. Spring Boot en sí; decidí utilizar el enfoque regular, no reactivo.
  2. Integración de Spring Boot con Thymeleaf
  3. Localizador de WebJars, para evitar especificar la versión de Vue en el lado del cliente
  4. ¡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 } }
  1. Pasar una lista estática de objetos Todo
  2. Vea abajo
  3. 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:


  1. 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
  2. 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/[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>
  1. Axios ayuda a realizar solicitudes HTTP
  2. Vue en sí
  3. Nuestro código del lado del cliente
  4. 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:


  1. Después de cargar la página, la página debería mostrar todos los elementos Todo
  2. Al hacer clic en una casilla de verificación Todo completado, debe activar o desactivar el atributo completed
  3. Al hacer clic en el botón Limpiar , se eliminan todas Todo completadas.
  4. Al hacer clic en el botón Agregar , debería agregarse un Todo a la lista de Todo 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 para label
    • completed : establecido en false

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 }
  1. 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>
  1. Establezca el ID para facilitar la vinculación
  2. 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, }
  1. 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');
  1. Configurar el componente
  2. Vue espera la función render()
  3. h() para hiperíndice crea un nodo virtual a partir del objeto y sus propiedades
  4. 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>
  1. Mostrar el ID Todo
  2. Mostrar la etiqueta Todo
  3. Marque la casilla si su atributo completed es true


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 } }
  1. Acepte la matriz props para que podamos acceder a ella más tarde.
  2. Vue pasa el event que activó la llamada
  3. Axios es una biblioteca de JavaScript que simplifica las llamadas HTTP
  4. 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.
  5. Carga útil JSON
  6. 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 } }
  1. Como arriba
  2. Axios ofrece conversión JSON automatizada de la llamada HTTP
  3. 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');
  1. Obtenga el conjunto de datos en la página HTML, a través de Thymeleaf, como se explicó anteriormente
  2. 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.
  3. Devolver las referencias, según las expectativas de Vue
  4. Mira, mamá, estoy usando el operador de propagación de JavaScript.
  5. 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>
  1. Recorrer la lista de objetos Todo
  2. 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 } } }
  1. Crea un contenedor reactivo alrededor del título cuyo alcance esté limitado a la función
  2. La función create() propiamente dicha
  3. Añade el nuevo objeto JSON devuelto por la llamada API a la lista de Todo
  4. Restablecer el valor del campo
  5. 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á: