¿Qué hay de nuevo en Vue 3?
Publicado: 2022-03-10Con el lanzamiento de Vue 3, los desarrolladores deben realizar la actualización de Vue 2, ya que viene con un puñado de características nuevas que son muy útiles para crear componentes fáciles de leer y fáciles de mantener y formas mejoradas de estructurar nuestra aplicación en Vue. Vamos a echar un vistazo a algunas de estas características en este artículo.
Al final de este tutorial, los lectores podrán;
- Conozca acerca de
provide / inject
y cómo usarlo. - Tener una comprensión básica de Teleport y cómo usarlo.
- Conozca los Fragmentos y cómo utilizarlos.
- Conozca los cambios realizados en la API de Global Vue.
- Conozca los cambios realizados en la API de Eventos.
Este artículo está dirigido a aquellos que tienen una comprensión adecuada de Vue 2.x. Puede encontrar todo el código utilizado en este ejemplo en GitHub.
provide / inject
En Vue 2.x, teníamos props
que facilitaban el paso de datos (cadenas, matrices, objetos, etc.) de un componente principal directamente a su componente secundario. Pero durante el desarrollo, a menudo encontramos instancias en las que necesitábamos pasar datos del componente principal a un componente profundamente anidado, lo que era más difícil de hacer con props
. Esto dio como resultado el uso de Vuex Store, Event Hub y, a veces, pasar datos a través de los componentes profundamente anidados. Veamos una aplicación simple;
Es importante tener en cuenta que Vue 2.2.0 también vino con provide / inject
, cuyo uso no se recomienda en el código de la aplicación genérica.
# parentComponent.vue <template> <div class="home"> <img alt="Vue logo" src="../assets/logo.png" /> <HelloWorld msg="Vue 3 is liveeeee!" :color="color" /> <select name="color" v-model="color"> <option value="" disabled selected> Select a color</option> <option :value="color" v-for="(color, index) in colors" :key="index">{{ color }}</option></select > </div> </template> <script> import HelloWorld from "@/components/HelloWorld.vue"; export default { name: "Home", components: { HelloWorld, }, data() { return { color: "", colors: ["red", "blue", "green"], }; }, }; </script>
# childComponent.vue <template> <div class="hello"> <h1>{{ msg }}</h1> <color-selector :color="color"></color-selector> </div> </template> <script> import colorSelector from "@/components/colorComponent.vue"; export default { name: "HelloWorld", components: { colorSelector, }, props: { msg: String, color: String, }, }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> h3 { margin: 40px 0 0; } ul { list-style-type: none; padding: 0; } li { display: inline-block; margin: 0 10px; } a { color: #42b983; } </style>
# colorComponent.vue <template> <p :class="[color]">This is an example of deeply nested props!</p> </template> <script> export default { props: { color: String, }, }; </script> <style> .blue { color: blue; } .red { color: red; } .green { color: green; } </style>
Aquí, tenemos una página de inicio con un menú desplegable que contiene una lista de colores y estamos pasando el color
seleccionado a childComponent.vue
como accesorio. Este componente secundario también tiene un msg
de mensaje que acepta un texto para mostrar en la sección de plantilla. Finalmente, este componente tiene un componente secundario ( colorComponent.vue
) que acepta una propiedad de color
del componente principal que se utiliza para determinar la clase del texto en este componente. Este es un ejemplo de paso de datos a través de todos los componentes.
Pero con Vue 3, podemos hacer esto de una manera más limpia y breve usando el nuevo par Proporcionar e inyectar. Como su nombre lo indica, usamos provide
como una función o un objeto para hacer que los datos estén disponibles desde un componente principal para cualquiera de sus componentes anidados, independientemente de qué tan profundamente anidado esté dicho componente. Hacemos uso de la forma de objeto al pasar valores codificados para provide
como este;
# parentComponent.vue <template> <div class="home"> <img alt="Vue logo" src="../assets/logo.png" /> <HelloWorld msg="Vue 3 is liveeeee!" :color="color" /> <select name="color" v-model="color"> <option value="" disabled selected> Select a color</option> <option :value="color" v-for="(color, index) in colors" :key="index">{{ color }}</option></select > </div> </template> <script> import HelloWorld from "@/components/HelloWorld.vue"; export default { name: "Home", components: { HelloWorld, }, data() { return { colors: ["red", "blue", "green"], }; }, provide: { color: 'blue' } }; </script>
Pero para los casos en los que necesita pasar una propiedad de instancia de componente para provide
, usamos el modo de función para que esto sea posible;
# parentComponent.vue <template> <div class="home"> <img alt="Vue logo" src="../assets/logo.png" /> <HelloWorld msg="Vue 3 is liveeeee!" /> <select name="color" v-model="selectedColor"> <option value="" disabled selected> Select a color</option> <option :value="color" v-for="(color, index) in colors" :key="index">{{ color }}</option></select > </div> </template> <script> import HelloWorld from "@/components/HelloWorld.vue"; export default { name: "Home", components: { HelloWorld, }, data() { return { selectedColor: "blue", colors: ["red", "blue", "green"], }; }, provide() { return { color: this.selectedColor, }; }, }; </script>
Dado que no necesitamos los accesorios de color
tanto en childComponent.vue
como en colorComponent.vue
, nos deshacemos de ellos. Lo bueno de usar provide
es que el componente principal no necesita saber qué componente necesita la propiedad que está proporcionando.
Para hacer uso de esto en el componente que lo necesita en este caso, colorComponent.vue
hacemos esto;
# colorComponent.vue <template> <p :class="[color]">This is an example of deeply nested props!</p> </template> <script> export default { inject: ["color"], }; </script> <style> .blue { color: blue; } .red { color: red; } .green { color: green; } </style>
Aquí, usamos inject
que toma una matriz de las variables requeridas que necesita el componente. En este caso, solo necesitamos la propiedad de color
, así que solo pasamos eso. Después de eso, podemos usar el color
de la misma manera que lo usamos cuando usamos accesorios.
Podríamos notar que si tratamos de seleccionar un nuevo color usando el menú desplegable, el color no se actualiza en colorComponent.vue
y esto se debe a que, de forma predeterminada, las propiedades provide
no son reactivas. Para arreglar eso, hacemos uso del método computed
.
# parentComponent.vue <template> <div class="home"> <img alt="Vue logo" src="../assets/logo.png" /> <HelloWorld msg="Vue 3 is liveeeee!" /> <select name="color" v-model="selectedColor"> <option value="" disabled selected> Select a color</option> <option :value="color" v-for="(color, index) in colors" :key="index">{{ color }}</option></select > </div> </template> <script> import HelloWorld from "@/components/HelloWorld.vue"; import { computed } from "vue"; export default { name: "Home", components: { HelloWorld, }, data() { return { selectedColor: "", todos: ["Feed a cat", "Buy tickets"], colors: ["red", "blue", "green"], }; }, provide() { return { color: computed(() => this.selectedColor), }; }, }; </script>
Aquí, importamos computed
y pasamos nuestro color selectedColor
para que pueda ser reactivo y actualizarse cuando el usuario selecciona un color diferente. Cuando pasa una variable al método calculado, devuelve un objeto que tiene un value
. Esta propiedad contiene el valor de su variable, por lo que para este ejemplo, tendríamos que actualizar colorComponent.vue
para que se vea así;
# colorComponent.vue <template> <p :class="[color.value]">This is an example of deeply nested props!</p> </template> <script> export default { inject: ["color"], }; </script> <style> .blue { color: blue; } .red { color: red; } .green { color: green; } </style>
Aquí, cambiamos color
a color.value
para representar el cambio después de hacer que el color
sea reactivo usando el método computed
. En este punto, la class
del texto en este componente siempre cambiará cuando el color selectedColor
cambie en el componente principal.
teletransportarse
Hay casos en los que creamos componentes y los colocamos en una parte de nuestra aplicación debido a la lógica que usa la aplicación, pero están destinados a mostrarse en otra parte de nuestra aplicación. Un ejemplo común de esto sería un modal o una ventana emergente destinada a mostrar y cubrir toda la pantalla. Si bien podemos crear una solución para esto usando la propiedad de position
de CSS en dichos elementos, con Vue 3, también podemos hacerlo usando Teleport.
Teleport nos permite sacar un componente de su posición original en un documento, desde el contenedor #app
predeterminado en el que se envuelven las aplicaciones Vue y moverlo a cualquier elemento existente en la página que se está utilizando. Un buen ejemplo sería usar Teleport para mover un componente de encabezado desde dentro del div #app
a un header
. Es importante tener en cuenta que solo puede Teleportar a elementos que existen fuera de Vue DOM.
El componente Teleport acepta dos props que determinan el comportamiento de este componente y son;
-
to
Esta propiedad acepta un nombre de clase, una identificación, un elemento o un atributo data-*. También podemos hacer que este valor sea dinámico pasando un:to
prop en lugarto
y cambiando el elemento Teleport dinámicamente. -
:disabled
Este accesorio acepta unBoolean
y se puede usar para alternar la función de teletransporte en un elemento o componente. Esto puede ser útil para cambiar dinámicamente la posición de un elemento.
Un ejemplo ideal del uso de Teleport se ve así;
# index.html** <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width,initial-scale=1.0" /> <link rel="icon" href="<%= BASE_URL %>favicon.ico" /> <title> <%= htmlWebpackPlugin.options.title %> </title> </head> <!-- add container to teleport to --> <header class="header"></header> <body> <noscript> <strong >We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong > </noscript> <div></div> <!-- built files will be auto injected --> </body> </html>
En el archivo index.html
predeterminado en su aplicación Vue, agregamos un elemento de header
porque queremos teletransportar nuestro componente de encabezado a ese punto en nuestra aplicación. También agregamos una clase a este elemento para diseñar y para facilitar la referencia en nuestro componente Teleport.
# Header.vue** <template> <teleport to="header"> <h1 class="logo">Vue 3 </h1> <nav> <router-link to="/">Home</router-link> </nav> </teleport> </template> <script> export default { name: "app-header", }; </script> <style> .header { display: flex; align-items: center; justify-content: center; } .logo { margin-right: 20px; } </style>
Aquí, creamos el componente de encabezado y agregamos un logotipo con un enlace a la página de inicio en nuestra aplicación. También agregamos el componente Teleport y le damos to
prop un valor de header
porque queremos que este componente se represente dentro de este elemento. Finalmente, importamos este componente a nuestra aplicación;
# App.vue <template> <router-view /> <app-header></app-header> </template> <script> import appHeader from "@/components/Header.vue"; export default { components: { appHeader, }, }; </script>
En este archivo, importamos el componente de encabezado y lo colocamos en la plantilla para que pueda verse en nuestra aplicación.
Ahora, si inspeccionamos el elemento de nuestra aplicación, notaremos que nuestro componente de encabezado está dentro del elemento de header
;
Fragmentos
Con Vue 2.x, era imposible tener varios elementos raíz en la template
de su archivo y, como solución alternativa, los desarrolladores comenzaron a envolver todos los elementos en un elemento principal. Si bien esto no parece un problema grave, hay instancias en las que los desarrolladores quieren renderizar un componente sin un contenedor que envuelva dichos elementos, pero tienen que conformarse con eso.
Con Vue 3, se introdujo una nueva característica llamada Fragmentos y esta característica permite a los desarrolladores tener múltiples elementos en su archivo de plantilla raíz. Entonces, con Vue 2.x, así es como se vería un componente de contenedor de campo de entrada;
# inputComponent.vue <template> <div> <label :for="label">label</label> <input :type="type" : :name="label" /> </div> </template> <script> export default { name: "inputField", props: { label: { type: String, required: true, }, type: { type: String, required: true, }, }, }; </script> <style></style>
Aquí, tenemos un componente de elemento de formulario simple que acepta dos accesorios, label
y type
, y la sección de plantilla de este componente está envuelta en un div. Esto no es necesariamente un problema, pero si desea que la etiqueta y el campo de entrada estén directamente dentro de su elemento de form
. Con Vue 3, los desarrolladores pueden reescribir fácilmente este componente para que se vea así;
# inputComponent.vue <template class="testingss"> <label :for="label">{{ label }}</label> <input :type="type" : :name="label" /> </template>
Con un solo nodo raíz, los atributos siempre se atribuyen al nodo raíz y también se conocen como atributos no prop . Son eventos o atributos pasados a un componente que no tienen propiedades correspondientes definidas en props
o emits
. Ejemplos de tales atributos son class
e id
. Sin embargo, es necesario definir explícitamente a cuál de los elementos de un componente de nodo multirraíz se debe atribuir.
Esto es lo que esto significa usar inputComponent.vue
desde arriba;
- Al agregar
class
a este componente en el componente principal, se debe especificar a qué componente se atribuiría estaclass
; de lo contrario, el atributo no tiene efecto.
<template> <div class="home"> <div> <input-component class="awesome__class" label="name" type="text" ></input-component> </div> </div> </template> <style> .awesome__class { border: 1px solid red; } </style>
Cuando hace algo como esto sin definir dónde se deben atribuir los atributos, recibe esta advertencia en su consola;
Y el border
no tiene efecto sobre el componente;
- Para solucionar esto, agregue
v-bind="$attrs"
en el elemento al que desea que se distribuyan dichos atributos;
<template> <label :for="label" v-bind="$attrs">{{ label }}</label> <input :type="type" : :name="label" /> </template>
Aquí, le decimos a Vue que queremos que los atributos se distribuyan al elemento de la label
, lo que significa que queremos que se le aplique la awesome__class
. Ahora, si inspeccionamos nuestro elemento en el navegador, veremos que la clase ahora se ha agregado a la label
y, por lo tanto, ahora hay un borde alrededor de la etiqueta.
API globales
No era raro ver Vue.component
o Vue.use
en el archivo main.js
de una aplicación Vue. Estos tipos de métodos se conocen como API globales y hay bastantes de ellos en Vue 2.x. Uno de los desafíos de este método es que hace imposible aislar ciertas funcionalidades en una instancia de su aplicación (si tiene más de una instancia en su aplicación) sin que afecte a otras aplicaciones porque todas están montadas en Vue. Esto es lo que quiero decir;
Vue.directive('focus', { inserted: el => el.focus() }) Vue.mixin({ /* ... */ }) const app1 = new Vue({ el: '#app-1' }) const app2 = new Vue({ el: '#app-2' })
Para el código anterior, es imposible afirmar que la Directiva Vue esté asociada con la aplicación 1 y app1
con la app2
, pero en cambio, ambas están disponibles en las dos aplicaciones.
Vue 3 viene con una nueva API global en un intento de solucionar este tipo de problema con la introducción de createApp
. Este método devuelve una nueva instancia de una aplicación Vue. Una instancia de aplicación expone un subconjunto de las API globales actuales. Con esto, todas las API (componentes, mezclas, directivas, uso, etc.) que mutan Vue
de Vue 2.x ahora se moverán a instancias de aplicaciones individuales y ahora, cada instancia de su aplicación Vue puede tener funcionalidades que son únicas para ellos sin afectar a otras aplicaciones existentes.
Ahora, el código anterior se puede reescribir como;
const app1 = createApp({}) const app2 = createApp({}) app1.directive('focus', { inserted: el => el.focus() }) app2.mixin({ /* ... */ })
Sin embargo, es posible crear funcionalidades que desee compartir entre todas sus aplicaciones y esto se puede hacer mediante una función de fábrica.
API de eventos
Una de las formas más comunes que adoptaron los desarrolladores para pasar datos entre componentes que no tienen una relación principal a secundaria que no sea usar Vuex Store es el uso de Event Bus. Una de las razones por las que este método es común es por lo fácil que es empezar a utilizarlo;
# eventBus.js const eventBus = new Vue() export default eventBus;
Luego de esto, lo siguiente sería importar este archivo a main.js
para que esté disponible globalmente en nuestra app o importarlo en archivos que lo necesites;
# main.js import eventBus from 'eventBus' Vue.prototype.$eventBus = eventBus
Ahora, puede emitir eventos y escuchar eventos emitidos como este;
this.$eventBus.$on('say-hello', alertMe) this.$eventBus.$emit('pass-message', 'Event Bus says Hi')
Hay una gran cantidad de código base de Vue que está lleno de código como este. Sin embargo, con Vue 3, sería imposible hacerlo porque $on
, $off
y $once
se han eliminado, pero $emit
aún está disponible porque es necesario para que el componente secundario emita eventos a sus componentes principales. Una alternativa a esto sería usar provide / inject
o cualquiera de las bibliotecas de terceros recomendadas.
Conclusión
En este artículo, hemos cubierto cómo puede pasar datos de un componente principal a un componente secundario profundamente anidado utilizando el par provide / inject
. También hemos visto cómo podemos reposicionar y transferir componentes de un punto de nuestra aplicación a otro. Otra cosa que analizamos es el componente de nodo raíz múltiple y cómo asegurarnos de que distribuimos los atributos para que funcionen correctamente. Finalmente, también cubrimos los cambios en la API de eventos y la API global.
Más recursos
- "Funciones de fábrica de JavaScript con ES6+", Eric Elliott, Medium
- "Uso de Event Bus para compartir accesorios entre los componentes de Vue", Kingsley Silas, CSS-Tricks
- Uso de múltiples telepuertos en el mismo objetivo, Vue.js Docs
- Atributos no prop, Vue.js Docs
- Trabajando con Reactividad, Vue.js Docs
-
teleport
, Vue.js Docs - Fragmentos, Vue.js Docs
- Sintaxis 2.x, Vue.js Docs