...
Cada componente se define como un archivo .vue, cuya estructura básica es estaindependientemente de si el componente es una vista, o una parte de esta. Disponemos de dos formas de construir nuestros componentes, que vamos a ver a continuación.
La estructura básica de un componente es la siguiente:
| Bloque de código | ||
|---|---|---|
| ||
<template>
<!-- Bloque HTML -->
</template>
<script>
export default {
// Funcionalidad Javascript
};
</script>
<style scoped>
// Estilos CSS del componente
</style>
<i18n>
// Internacionalización
</i18n> |
Como podemos ver, estos archivos se dividen en diferentes bloques. En este caso vemos tres, pero hay alguno más que podemos incluir.
Template: código HTML
En el bloque template se incluye el código HTML del código. Se trata de HTML enriquecido, pues podremos referenciar a variables y métodos definidos en la parte JavaScript del componente. Hay que tener en cuenta que sólo puede haber un componente HTML en el primer nivel. Por ejemplo, teniendo todo lo que contenga envuelto en un div. Vamos a ver la sintaxis de las funciones que nos ofrece este HTML enriquecido.
...
| Bloque de código | ||
|---|---|---|
| ||
<button v-on:click="counter += 1">Add 1</button> <button @click="counter += 1">Add 1</button> |
Script
El bloque script contiene la funcionalidad JavaScript del componente, pudiendo definir tanto variables propias como métodos, así como modificar el funcionamiento del propio componente. Para entender cómo funcionan los componentes, y cómo modificar su comportamiento, tenemos que conocer su ciclo de vida:
En este diagrama podemos ver los diferentes pasos que se siguen, y podemos ver que antes y después de los diferentes estados salen en rojo, señalados con una línea de puntos, nombres de funciones. Estas funciones son las llamadas llamadas lifecycle hooks, y nos permiten ejecutar código en esos momentos. Esto nos puede ser útil, por ejemplo, para cargar algún valor llamando a un servicio REST antes de que se monte el componente. Para definir estos métodos, lo haremos dentro del bloque script, y dentro del export default { ... }.
A partir de aquí, tenemos que diferenciar entre dos formas de organizar el bloque script, Options API y Composition API. Options API es la forma tradicional de organizar los componentes, la que ya utilizábamos en Vue 2 y que sigue estando disponible en Vue 3. A continuación vamos a ver cada una de ellas, a partir de aquí porque los lifecycle hooks ya se nombran de forma diferente.
Options API
Las diferentes opciones, aunque podemos verlas también en el diagrama, son:
beforeCreate() { ... }created() { ... }beforeMount() { ... }mounted() { ... }beforeUpdate() { ... }upadated() { ... }beforeUnmount(){ ... }para Vue 3,beforeDestroy() { ... }para Vue 2.unmounted(){ ... }para Vue 3,destroyed() { ... }para Vue 2.
* Como vemos, los dos últimos cambian su sintaxis en Vue 3. Ahora mismo, el portal está en modo compatibilidad, los componentes se toman como si fueran de Vue 2, por lo que se seguirá utilizando la sintaxis Vue 2, pero esto es transitorio, pues se acabará terminando de migrar todo a Vue 3.
Además de definir estos métodos, dentro del export default { ... } también podemos definir los siguientes campos:
name: nombre del componente. Si no lo definimos, por defecto será el del archivo.props: variables que se le pasarán como parámetro en el template.data(): variables definidas internamente en el propio componente, con alcance local.computed(): variables calculadas.methods: métodos definidos en el componente.watch: listeners, funciones que se ejecutarán cuando el valor de la variable que indiquemos que se observa cambie.components: otros componentes que se usan en el template. Para registrar componentes localmente.emits: lista de eventos personalizados que pueden ser emitidos desde el componente.
Antes del export default { ... } se pueden importar otros componentes para registrarlos localmente, o archivos JavaScript para utilizar su funcionalidad. Hay una página de documentación dedicada a cómo registrar componentes.
...
| Bloque de código | ||
|---|---|---|
| ||
<script>
import OtroComponente from './OtroComponente.vue';
export default {
name: Componente,
props: ['prop1', 'prop2'],
data() {
return {
mensaje: "Hola"
}
},
computed: {
mensajeAlReves: function() {
return this.mensaje.reverse();
}
},
methods: {
hacerCosas() {
// Código js
}
},
watch: {
mensaje: function(newValue, oldValue) {
console.log("El mensaje ha cambiado de '" + oldValue + "' a '" + newValue + "'");
}
},
emits: ["algo", "otroEvento"],
components: {
OtroComponente
},
beforeCreate() {},
created() {},
beforeMount() {},
mounted() {},
beforeUpdate() {},
updated() {},
beforeDestroy() {},
destroyed() {}
}
</script> |
Composition API
Composition API es una forma de organizar los componentes que permite una mayor flexibilidad a la hora de organizar el código y una mayor reusabilidad de éste. Ahora en lugar de la estructura:
| Bloque de código | ||
|---|---|---|
| ||
<script>
export default {
...
}
</script> |
Tendremos:
| Bloque de código | ||
|---|---|---|
| ||
<script setup>
...
</script> |
Dentro podemos escribir el código que queramos, y será ejecutando. Tendremos que ir definiendo las diferentes variables, que se compartirán con el template, por ejemplo, los diferentes métodos, etc. Este setup se ejecuta al inicio del ciclo de vida, y desaparecen beforeCreate y created, pues lo que incluyamos se ejecutará en ese mismo punto. Las variables y métodos que definamos aquí serán expuestas al template, es decir, podrán ser accedidas desde éste.
Vamos a ir viendo las cosas que se utilizan de forma diferente:
Lifecycle hooks
Los lifecycle hooks, en Composition API, tenemos que importarlos de vue, y definirlos de esta forma:
| Bloque de código | ||
|---|---|---|
| ||
<script setup>
import { onUpdated } from 'vue'
onUpdated(() => {
...
})
</script> |
Todos se definirán de esta forma. A continuación tenemos la sintaxis de cada uno en Composition API (y a qué equivalen en Options API):
| Options API | Composition API | Descripción |
|---|---|---|
| beforeCreate | - | Se escribe directamente en el setup |
| created | - | Se escribe directamente en el setup |
| beforeMount | onBeforeMount | Se ejecuta justo antes de que se monte el componente. |
| mounted | onMounted | Se ejecuta una vez se ha montado el componente. |
| beforeUpdate | onBeforeUpdate | Se ejecuta antes de una actualización del DOM. |
| updated | onUpdated | Se ejecuta cada vez que se actualiza el DOM por un cambio de estado reactivo. |
| beforeUnmount | onBeforeUnmount | Se ejecuta antes de quitar el componente. |
| unmounted | onUnmounted | Se ejecuta una vez se ha quitado el componente. |
Props y emits
En lugar de en un bloque props, lo haremos así:
| Bloque de código | ||
|---|---|---|
| ||
<script setup>
const props = defineProps({
foo: String
})
const emit = defineEmits(['change', 'delete'])
...
</script> |
ref() y reactive(): variables del componente
Mientras que antes teníamos que definir las variables reactivas del componente dentro del bloque data, ahora podemos definirlas con dos métodos, ref() y reactive(). Las variables que definamos con éstos, como decimos, serán reactivas, es decir, el template se enterará de los cambios en estas variables, reaccionará a ellos. Una variable definida de manera normal no será reactiva, el DOM no se actualizará cuando su valor cambie. Para utilizarlo, tenemos que importarlos primero, y se utilizan así:
| Bloque de código | ||
|---|---|---|
| ||
import { ref, reactive } from 'vue';
const count = ref(0);
console.log(count.value)
const person = reactive({
name: 'Albert',
age: 30,
isNinja: true,
});
console.log(person) |
Vamos a ver las diferencias entre cada uno:
Ref actúa como un un wrapper para valores reactivos. Devuelve un objeto con un campo .value que contiene el valor, y que nos permite mutarlo.
Reactive funciona de forma similar, pero los objetos no son wrappeados, podemos acceder al valor directamente sin necesidad de especificar value.
Entonces... ¿Cuál es la diferencia? ¿Cuándo usar uno u otro? La principal diferencia entre ellos es que reactive sólo acepta objetos, no valores primitivos. Sin embargo ref sí que acepta valores primitivos. En realidad, ref utiliza reactive, ref(0) sería equivalente a reactive({ value : 0}). Ref también acepta objetos, con la diferencia de que sí se puede reasignar un objeto completo, reasignando .value, mientras que reactive no, se pueden cambiar los campos del objeto pero no se puede cambiar por un objeto diferente. Sabiendo esto, utilizaremos:
- ref() cuando sea un valor primitivo, o un objeto o array que queramos reasignar más tarde.
- reactive() cuando sea un objeto que no necesites reasignar, evitando así tener que pasar por .value para acceder a o modificar su valor.
En el template, para acceder a sus valores, no hace falta poner .value, se lee directamente.
Estas son las opciones que tenemos, pero realmente podemos utilizar ref() para todo. Pues permite lo mismo que reactive y más (valores primitivos, reemplazar objeto), y el uso de .value hará que seamos conscientes siempre de que estamos trabajando con una variable que es reactiva para el template, de modo que tengamos una diferenciación entre estas variables y las variables normales, y tratemos con ellas siempre del mismo modo.
Computed
Para las variables computadas, disponemos de la función computed(). Cada una la definiremos así:
| Bloque de código | ||
|---|---|---|
| ||
const color = computed(() => {
if(contador.value < 0){
return 'red'
}else{
return 'blue'
}
}) |
Watchers
Para reaccionar a cambios en variables que requieran algo más complejo que un computed, disponemos de watch(). A esta, se le especifica la variable a observar, y una función cuyos parámetros son el valor anterior y el nuevo.
| Bloque de código | ||
|---|---|---|
| ||
watch(data, (currentValue, oldValue) => {
console.log(currentValue);
console.log(oldValue);
}); |
Esto, con valores primitivos es bastante simple, pero se puede complicar para valores complejos. Mientras no tengamos documentado esto, podéis mirar esta guía, que es bastante completa: https://www.netlify.com/blog/2021/01/29/deep-dive-into-the-vue-composition-apis-watch-method/
Métodos
Como con la sintaxis que estamos utilizando, se están exponiendo al template las variables y métodos que utilizamos, podemos definir lo que antes teníamos dentro de methods como funciones normales:
| Bloque de código | ||
|---|---|---|
| ||
const aumentar = () => { contador.value ++ } |
También podemos importar métodos definidos en otros archivos, proporcionando así más flexibilidad y reutilización del código, sin tener que pasar por mixins. Esto se utiliza generalmente para hacer lo que se llaman funciones "composable", funciones que encapsulan lógica reutilizable sin estado. Estas funciones, por convención, se llaman con nomenclatura camelCase, y empiezan por "use".
Por ejemplo, podríamos tener una función que obtenga la posición del cursor en todo momento. Para ello, tendríamos un archivo aparte así:
| Bloque de código | ||
|---|---|---|
| ||
// mouse.js import { ref, onMounted, onUnmounted } from 'vue' // by convention, composable function names start with "use" export function useMouse() { // state encapsulated and managed by the composable const x = ref(0) const y = ref(0) // a composable can update its managed state over time. function update(event) { x.value = event.pageX y.value = event.pageY } // a composable can also hook into its owner component's // lifecycle to setup and teardown side effects. onMounted(() => window.addEventListener('mousemove', update)) onUnmounted(() => window.removeEventListener('mousemove', update)) // expose managed state as return value return { x, y } } |
Y luego simplemente importarlo y utilizarlo en componentes:
| Bloque de código | ||
|---|---|---|
| ||
<script setup> import { useMouse } from './mouse.js' const { x, y } = useMouse() </script> <template>Mouse position is at: {{ x }}, {{ y }}</template> |
Emit: emitir eventos
Para que nuestro componente lance eventos, devemos definir los que puede lanzar con defineEmits(), y después podremos lanzarlos directamente con emit():
| Bloque de código | ||
|---|---|---|
| ||
<script setup>
const emit = defineEmits(['eventA', 'eventB'])
function btnClick(params) {
emit('eventA')
emit('eventB', params)
}
</script> |
Importar componentes
Para registrar componentes de forma local, es decir, en otros componentes, en Composition API vale simplemente con importarlos:
| Bloque de código | ||
|---|---|---|
| ||
<template>
<MyComponent />
</template>
<script setup>
import MyComponent from './MyComponent.vue'
</script> |
Sin embargo, en Options API, tenemos que importarlos y añadirlos al bloque components:
| Bloque de código | ||
|---|---|---|
| ||
<template>
<MyComponent />
</template>
<script setup>
import MyComponent from './MyComponent.vue'
export default {
components: {
MyComponent,
}
}
</script> |
También se pueden registrar componentes de forma global, para que estén disponibles desde todos los componentes del proyecto, pero esto no lo haremos, pues tenemos un sólo proyecto para todos los servicios del Portal de Servicios.
Style
Los bloques style incluyen estilos css. En este caso, se pueden tener varios bloques style:
<styles>: estilos globales, se aplicarán a todo el proyecto.<styles scoped>: se aplica sólo al componente. Estos no se aplican tampoco a componentes hijos, para eso podemos utilizar::v-deep, de esta forma:Bloque de código language css <style scoped> #tabla-asignaturas ::v-deep .p-datatable-tbody tr td { border: none !important; } #tabla-asignaturas ::v-deep .p-datatable-row { vertical-align: top; } </style>
i18n
En la página de documentación de internacionalización podemos ver cómo incluir un bloque para especificar las cadenas de texto y sus traducciones en el propio componente.


