O que há de novo no Vue 3?

Publicados: 2022-03-10
Resumo rápido ↬ O Vue 3 vem com muitos novos recursos interessantes e mudanças em alguns dos existentes que visam tornar o desenvolvimento com o framework muito mais fácil e sustentável. Neste artigo, veremos alguns desses novos recursos e como começar a usá-los. Também vamos dar uma olhada em algumas das mudanças feitas nos recursos existentes.

Com o lançamento do Vue 3, os desenvolvedores precisam fazer a atualização do Vue 2, pois ele vem com um punhado de novos recursos que são super úteis na construção de componentes de fácil leitura e manutenção e maneiras aprimoradas de estruturar nosso aplicativo no Vue. Vamos dar uma olhada em alguns desses recursos neste artigo.

Ao final deste tutorial, os leitores irão;

  1. Saiba sobre provide / inject e como usá-lo.
  2. Ter uma compreensão básica do Teleport e como usá-lo.
  3. Conheça os Fragments e como usá-los.
  4. Conheça as mudanças feitas na API Global Vue.
  5. Conheça as alterações feitas na API de Eventos.

Este artigo destina-se àqueles que possuem uma compreensão adequada do Vue 2.x. Você pode encontrar todo o código usado neste exemplo no GitHub.

provide / inject

No Vue 2.x, tínhamos props que facilitavam a passagem de dados (string, arrays, objetos, etc) de um componente pai diretamente para seu componente filho. Mas durante o desenvolvimento, muitas vezes encontramos casos em que precisávamos passar dados do componente pai para um componente profundamente aninhado, o que era mais difícil de fazer com props . Isso resultou no uso do Vuex Store, do Event Hub e, às vezes, na passagem de dados pelos componentes profundamente aninhados. Vejamos um aplicativo simples;

É importante notar que o Vue 2.2.0 também veio com o provide / inject que não era recomendado para uso em código de aplicação genérico.

 # 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>

Aqui, temos uma landing page com um dropdown contendo uma lista de cores e estamos passando a color selecionada para childComponent.vue como prop. Este componente filho também tem um prop msg que aceita um texto para exibir na seção de template. Finalmente, este componente tem um componente filho ( colorComponent.vue ) que aceita um prop de color do componente pai que é usado para determinar a classe para o texto neste componente. Este é um exemplo de passagem de dados por todos os componentes.

Mas com o Vue 3, podemos fazer isso de uma maneira mais limpa e curta usando o novo par Provide e inject. Como o nome indica, usamos provide como uma função ou um objeto para disponibilizar dados de um componente pai para qualquer um de seus componentes aninhados, independentemente de quão profundamente aninhado seja esse componente. Fazemos uso do formulário de objeto ao passar valores codificados para provide assim;

 # 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>

Mas para instâncias em que você precisa passar uma propriedade de instância de componente para provide , usamos o modo de função para que isso seja possível;

 # 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>

Como não precisamos das props de color em childComponent.vue e colorComponent.vue , estamos nos livrando delas. O bom de usar o provide é que o componente pai não precisa saber qual componente precisa da propriedade que está fornecendo.

Para fazer uso disso no componente que precisa neste caso, colorComponent.vue fazemos isso;

 # 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>

Aqui, usamos inject que recebe uma matriz das variáveis ​​necessárias que o componente precisa. Nesse caso, precisamos apenas da propriedade color , então passamos apenas isso. Depois disso, podemos usar a color da mesma forma que usamos ao usar adereços.

Podemos notar que, se tentarmos selecionar uma nova cor usando o menu suspenso, a cor não será atualizada em colorComponent.vue e isso ocorre porque, por padrão, as propriedades em provide não são reativas. Para corrigir isso, fazemos uso do 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>

Aqui, importamos computed e passamos nossa selectedColor para que ela possa ser reativa e atualizada conforme o usuário seleciona uma cor diferente. Quando você passa uma variável para o método computado, ele retorna um objeto que possui um value . Esta propriedade contém o valor de sua variável, portanto, para este exemplo, teríamos que atualizar colorComponent.vue para ficar assim;

 # 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>

Aqui, alteramos color para color.value para representar a alteração após tornar a color reativa usando o método computed . Neste ponto, a class do texto neste componente sempre mudaria sempre que selectedColor mudasse no componente pai.

Mais depois do salto! Continue lendo abaixo ↓

teleporte

Há instâncias em que criamos componentes e os colocamos em uma parte de nosso aplicativo devido à lógica que o aplicativo usa, mas devem ser exibidos em outra parte de nosso aplicativo. Um exemplo comum disso seria um modal ou um pop-up destinado a exibir e cobrir toda a tela. Embora possamos criar uma solução alternativa para isso usando a propriedade position do CSS em tais elementos, com o Vue 3, também podemos usar o Teleport.

Teleport nos permite tirar um componente de sua posição original em um documento, do contêiner padrão #app que os aplicativos Vue são encapsulados e movê-lo para qualquer elemento existente na página que está sendo usado. Um bom exemplo seria usar o Teleport para mover um componente de cabeçalho de dentro do #app div para um header É importante notar que você só pode Teleportar para elementos que existem fora do Vue DOM.

mensagem de erro no console quando você se teletransporta para um elemento inválido
Mensagem de erro de teletransporte no console: Mensagem de erro de destino de teletransporte inválida no terminal. (Visualização grande)

O componente Teleport aceita dois props que determinam o comportamento deste componente e são;

  1. to
    Este prop aceita um nome de classe, um id, um elemento ou um atributo data-*. Também podemos tornar esse valor dinâmico passando um :to prop em oposição to e alterando o elemento Teleport dinamicamente.
  2. :disabled
    Este prop aceita um Boolean e pode ser usado para alternar o recurso Teleport em um elemento ou componente. Isso pode ser útil para alterar dinamicamente a posição de um elemento.

Um exemplo ideal de uso do Teleport se parece com isso;

 # 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>

No arquivo index.html padrão em seu aplicativo Vue, adicionamos um elemento de header porque queremos Teleportar nosso componente de cabeçalho para esse ponto em nosso aplicativo. Também adicionamos uma classe a esse elemento para estilizar e facilitar a referência em nosso 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>

Aqui, criamos o componente de cabeçalho e adicionamos um logotipo com um link para a página inicial em nosso aplicativo. Também adicionamos o componente Teleport e damos to prop um valor de header porque queremos que esse componente seja renderizado dentro desse elemento. Por fim, importamos esse componente para nosso aplicativo;

 # App.vue <template> <router-view /> <app-header></app-header> </template> <script> import appHeader from "@/components/Header.vue"; export default { components: { appHeader, }, }; </script>

Nesse arquivo, importamos o componente de cabeçalho e o colocamos no modelo para que fique visível em nosso aplicativo.

Agora, se inspecionarmos o elemento de nosso aplicativo, perceberemos que nosso componente de cabeçalho está dentro do elemento de header ;

Componente de cabeçalho no DevTools
Componente de cabeçalho no DevTools (visualização grande)

Fragmentos

Com o Vue 2.x, era impossível ter vários elementos raiz no template do seu arquivo e, como solução alternativa, os desenvolvedores começaram a agrupar todos os elementos em um elemento pai. Embora isso não pareça um problema sério, há casos em que os desenvolvedores desejam renderizar um componente sem um contêiner envolvendo esses elementos, mas precisam se contentar com isso.

Com o Vue 3, um novo recurso chamado Fragments foi introduzido e esse recurso permite que os desenvolvedores tenham vários elementos em seu arquivo de modelo raiz. Assim, com o Vue 2.x, é assim que um componente de contêiner de campo de entrada se pareceria;

 # 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>

Aqui, temos um componente de elemento de formulário simples que aceita dois props, label e type , e a seção de template deste componente é encapsulada em um div. Isso não é necessariamente um problema, mas se você quiser que o rótulo e o campo de entrada estejam diretamente dentro do seu elemento de form . Com o Vue 3, os desenvolvedores podem facilmente reescrever esse componente para ficar assim;

 # inputComponent.vue <template class="testingss"> <label :for="label">{{ label }}</label> <input :type="type" : :name="label" /> </template>

Com um único nó raiz, os atributos são sempre atribuídos ao nó raiz e também são conhecidos como atributos não-propriedade . São eventos ou atributos passados ​​para um componente que não possuem propriedades correspondentes definidas em props ou emits . Exemplos de tais atributos são class e id . No entanto, é necessário definir explicitamente a qual dos elementos em um componente de nó multi-raiz deve ser atribuído.

Aqui está o que isso significa usando o inputComponent.vue acima;

  1. Ao adicionar class a este componente no componente pai, deve-se especificar a qual componente essa class seria atribuída, caso contrário o atributo não terá efeito.
 <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>

Quando você faz algo assim sem definir onde os atributos devem ser atribuídos, você recebe este aviso em seu console;

mensagem de erro no terminal quando os atributos não são distribuídos
Mensagem de erro no terminal quando os atributos não são distribuídos (visualização grande)

E a border não tem efeito no componente;

componente sem distribuição de atributo
Componente sem distribuição de atributos (visualização grande)
  1. Para corrigir isso, adicione um v-bind="$attrs" no elemento para o qual você deseja que esses atributos sejam distribuídos;
 <template> <label :for="label" v-bind="$attrs">{{ label }}</label> <input :type="type" : :name="label" /> </template>

Aqui, estamos dizendo ao Vue que queremos que os atributos sejam distribuídos para o elemento label , o que significa que queremos que o awesome__class seja aplicado a ele. Agora, se inspecionarmos nosso elemento no navegador, veremos que a classe agora foi adicionada ao label e, portanto, uma borda agora está ao redor do rótulo.

componente com distribuição de atributos
Componente com distribuição de atributos (visualização grande)

API global

Não era incomum ver Vue.component ou Vue.use no arquivo main.js de uma aplicação Vue. Esses tipos de métodos são conhecidos como APIs globais e existem vários deles no Vue 2.x. Um dos desafios desse método é que ele impossibilita o isolamento de certas funcionalidades para uma instância do seu aplicativo (se você tiver mais de uma instância em seu aplicativo) sem afetar outros aplicativos, pois todos eles são montados no Vue. É isso que eu quero dizer;

 Vue.directive('focus', { inserted: el => el.focus() }) Vue.mixin({ /* ... */ }) const app1 = new Vue({ el: '#app-1' }) const app2 = new Vue({ el: '#app-2' })

Para o código acima, é impossível afirmar que a Diretiva Vue esteja associada ao app1 e o Mixin ao app2 , mas em vez disso, ambos estão disponíveis nos dois aplicativos.

O Vue 3 vem com uma nova API Global na tentativa de corrigir esse tipo de problema com a introdução do createApp . Este método retorna uma nova instância de um aplicativo Vue. Uma instância de aplicativo expõe um subconjunto das APIs globais atuais. Com isso, todas as APIs (componente, mixin, diretiva, uso, etc) que alteram o Vue do Vue 2.x agora serão movidas para instâncias de aplicativos individuais e agora, cada instância do seu aplicativo Vue pode ter funcionalidades exclusivas para sem afetar outros aplicativos existentes.

Agora, o código acima pode ser reescrito como;

 const app1 = createApp({}) const app2 = createApp({}) app1.directive('focus', { inserted: el => el.focus() }) app2.mixin({ /* ... */ })

No entanto, é possível criar funcionalidades que você deseja compartilhar entre todos os seus aplicativos e isso pode ser feito usando uma função de fábrica.

API de eventos

Uma das formas mais comuns adotadas pelos desenvolvedores para passar dados entre componentes que não têm um relacionamento pai para filho além do uso da Vuex Store é o uso do Event Bus. Uma das razões pelas quais esse método é comum é a facilidade com que é iniciado;

 # eventBus.js const eventBus = new Vue() export default eventBus;

Depois disso, o próximo passo seria importar este arquivo para main.js para disponibilizá-lo globalmente em nosso aplicativo ou importá-lo em arquivos que você precisar;

 # main.js import eventBus from 'eventBus' Vue.prototype.$eventBus = eventBus

Agora, você pode emitir eventos e ouvir eventos emitidos como este;

 this.$eventBus.$on('say-hello', alertMe) this.$eventBus.$emit('pass-message', 'Event Bus says Hi')

Há muita base de código Vue que é preenchida com código como este. No entanto, com o Vue 3, seria impossível fazer isso porque $on , $off e $once foram todos removidos, mas $emit ainda está disponível porque é necessário que o componente filho emita eventos para seus componentes pai. Uma alternativa para isso seria usar provide / inject ou qualquer uma das bibliotecas de terceiros recomendadas.

Conclusão

Neste artigo, abordamos como você pode passar dados de um componente pai para um componente filho profundamente aninhado usando o par provide / inject . Também analisamos como podemos reposicionar e transferir componentes de um ponto em nosso aplicativo para outro. Outra coisa que analisamos é o componente de nó multi-raiz e como garantir que distribuímos atributos para que funcionem corretamente. Por fim, também abordamos as alterações na API de eventos e na API global.

Recursos adicionais

  • “JavaScript Factory Functions com ES6+,” Eric Elliott, Medium
  • “Usando o Event Bus para Compartilhar Props entre os Componentes Vue,” Kingsley Silas, CSS-Tricks
  • Usando vários teletransportes no mesmo destino, Vue.js Docs
  • Atributos não Prop, Documentos Vue.js
  • Trabalhando com Reatividade, Documentos Vue.js
  • teleport , Documentos Vue.js
  • Fragmentos, Documentos Vue.js
  • Sintaxe 2.x, Documentos Vue.js