Migração Vue 3 na Mercafacil
Um guia de como será a migração do Vue 2 para o Vue 3 dentro da Mercafacil
Com o Vue 3 se tornando a versão default para a criação de novos projetos a partir do dia 07 de Fevereiro de 2022 e o Vuetify lançando a beta de sua 3ª versão e tendo previsão de release para Maio deste ano (2022), surgiu a dúvida em nosso time de front end:
Será que não está na hora de ver o que será necessário para podermos migrarmos para as novas versões?
Foi então que tomei as rédeas e decidi verificar a documentação das principais bibliotecas utilizadas em nosso projeto relacionados ao ecossistema Vue, selecionar as mudanças que mais vão interferir no processo e aqui está o resultado das informações que encontrei, de acordo com suas respectivas bibliotecas.
Vue
Começando pelo nosso querido Vue, sua mais recente versão vem com grandes novidades como a Composition API, melhoria de performance, nova Api Global entre outras coisas, que apesar de serem importantes, não entrarei em detalhes, por não ser o foco deste artigo. Assim como as novidades, há uma lista gigante de mudanças, remoções e alterações que foram implementadas, que agora listo logo a̶p̶ó̶s̶ ̶o̶s̶ ̶c̶o̶m̶e̶r̶c̶i̶a̶i̶s̶ abaixo:
Filters
Os filters sempre foram boas opções na hora de se formatar valores para se apresentar algo, mas infelizmente foram removidos. Na documentação de migração é sugerido duas formas de se fazer a alteração, a primeira para uso local no componente e a segunda para uso global na aplicação, atualmente na plataforma estamos com a seguinte implementação do mesmo:
// currency.js
// formatação para valoresexport default (value) => {
if (value === '-') return value
if (value || value === 0) return `R$ ${value}`
}// main.js
import Vue from 'vue'
import filterCurrency from '@/filters/currency.js'Vue.filter('currency', filterCurrency)// uso nos componentes
<template>
<h1>{{ value | localeString(0) }}%</h1>
</template>
Para uso local, pode ser feita a partir do uso de methods ou computeds, já para uso global é recomendado o uso da globalProperties:
// main.js
const app = createApp(App)app.config.globalProperties.$filters = {
currency(value) {
if (value === '-') return value
if (value || value === 0) return `R$ ${value}`
}
}// uso nos componentes
<template>
<h1>>{{ $filters.currencyUSD(accountBalance) }}</h1>
</template>
Emit
Os emits também são recursos muito utilizados em nosso dia a dia, devido a sua facilidade em enviar informações de componentes filhos para componentes pais e sofreu uma leve mudança na nova atualização, agora todos os emits, assim como as props, devem ser declarados e podem ter validações, garantindo assim sua confiabilidade no que é retornado. Da mesma forma que as props, os emits podem ser declarados de duas formas, a primeira como um array:
<script>
export default {
emits: ['input'],
methods: {
closeDialog() {
this.$emit('input', false)
},
}
</script>
E a segunda como um objeto podendo ou não ter validações:
<script>
export default {
emits: {
// sem validação
click: null,
// com validação
input: (value) => {
return typeof value === 'string'
}
},
methods: {
closeDialog() {
this.$emit('input', false)
},
}
</script>
v-model
A diretiva “v-model” sempre foi restrita a um uso por componente, limitando e as vezes dificultando sua criação, isso foi alterado e agora componentes aceitam mais de um v-model, fazendo com que seja eliminada a necessidade de se utilizar o modificador .sync em props que necessitem ser alteradas, sua nova versão conta também com duas formas de implementação, a primeira, que particularmente achei menos elegante e menos entendível:
<NewComponent
:search="searchValue"
@update:search="searchValue = $event"
:content="contentValue"
@update:content="contentValue = $event"
/>
E a segunda que achei melhor, pela responsabilidade de atualização das props ser dentro do próprio componente e não em sua implementação
<template>
<NewComponent v-model:search="title" v-model:content="content" /></template>app.component('new-component', {
props: {
search: String,
content: String
},
emits: ['update:search', 'update:content'],
template: `
<input
type="text"
:value="search"
@input="$emit(update:search, $event.target.value)"
>
<input
type="text"
:value="content"
@input="$emit(update:content, $event.target.value)"
>`
})
Nova api global
A nova api global do Vue trouxe algumas mudanças pontuais, que alteram o fluxo de como são implementadas certas funcionalidades como a instância do próprio vue, que antes era feito através de uma classe, da seguinte forma:
import Vue from 'vue'new Vue({
router,
store,
i18n,
wait: new VueWait({ useVuex: true }),
vuetify,
apolloProvider,
render: (h) => h(App),
}).$mount('#app')
e agora é feita através da atribuição de uma função:
import { createApp } from 'vue'const app = createApp({})
app.use(router)
app.use(store)
app.use(vuetify)
app.mount('#app')
Além disso e de poder ter acesso a algumas propriedades globais, como já foi mostrado acima com os filtros, outra mudança que vem junto com essa nova api é na criação de plugins, juntamente com essa alteração, aproveito e já vou exemplificar a remoção do Vue.prototype e uso do globalProperties em seu lugar, abaixo segue o modelo atual:
//Plugin Vue 2
import { helpers } from 'mf-vue-components'export const getWhitelabelConfiguration = helpers.whitelabel.getWhitelabelConfigurationexport default {
install(Vue, { env }) {
const getWhitelabelLogo = () => {
return require(`@/whitelabel/${env?.VUE_APP_WHITELABEL || 'mercafacil'}/logo.png`)
}
//Uso prototype no Vue 2
Vue.prototype.$getWhitelabelConfiguration = Vue.getWhitelabelConfiguration = getWhitelabelConfiguration Vue.prototype.$getWhitelabelLogo = Vue.getWhitelabelLogo = getWhitelabelLogo },
}
E aqui a versão com as alterações:
// Plugin Vue 3
import { helpers } from 'mf-vue-components'export const getWhitelabelConfiguration = helpers.whitelabel.getWhitelabelConfigurationexport default {
install(app, { env }) {
const getWhitelabelLogo = () => {
return require(`@/whitelabel/${env?.VUE_APP_WHITELABEL || 'mercafacil'}/logo.png`)
}//Adição ao global properties
app.config.globalProperties.$getWhitelabelConfiguration = getWhitelabelConfiguration
app.config.globalProperties.$getWhitelabelLogo = getWhitelabelLogo
},
}
Vue-Router
A principal biblioteca de rotas do ecossistema vue, como esperado, também entrou na brincadeira e trouxe uma leva de alterações para ser compatível com a nova versão do vue, grazadeus elas vieram só trazendo paz e amor e será uma das menores dores que teremos nessa migração. Houve apenas duas mudanças na implementação que deveremos fazer, sendo as seguintes:
Nova forma de importação
Para começar, o Vue Router deixou de ser uma classe e se tornou um conjunto de funções, mas que não altera seu funcionamento em si. De acordo com a documentação a nova importação deve ser feito da seguinte forma:
import { createApp } from 'vue'
import { createRouter } from 'vue-router'const app = createApp = ({})
const router = createRouter({
history: createWebHistory(),
routes: [{
path: '/home',
component: () => import('@/modules/Home.vue'),
}],
})app.use(router)
Router Link dentro de transitions
Essa alteração a princípio tinha passado despercebido, mas após criar um projeto teste para as migrações, me deparei com esse problema, agora a tag <transition> e <keep-alive> quando usadas junto com a tag <router-view>, devem ser postas dentro do mesmo juntamente com um um slot, parece meio confuso, mas com o exemplo deve ficar mais claro:
// versão antiga<template>
<v-app id="app">
<v-content>
<v-container fluid class="pa-0">
// Não havia problema ter router-view dentro de uma transition
<transition-group name="slide-up">
<router-view class="child-view" />
</transition-group>
</v-container>
</v-content>
</v-app>
</template>// nova versão<template>
<v-app id="app">
<v-content>
<v-container fluid class="pa-0">
<router-view v-slot="{ Component }">
<transition>
<keep-alive>
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</v-container>
</v-content>
</v-app>
</template>
Vuex
O atual gerenciador de estados do vue veio com uma pequena alteração no que se refere a como o usamos aqui na Mercafacil, assim como o Vue e o Vue-router, mudando somente sua forma de importação:
// versao antiga
import Vue from 'vue'
import Vuex from 'vuex'import actions from '@/store/actions.js'
import getters from '@/store/getters.js'
import mutations from '@/store/mutations.js'
import state from '@/store/state.js'Vue.use(Vuex)export default new Vuex.Store({
actions,
getters,
mutations,
state
})// versão nova
import { createApp } from 'vue'
import { createStore } from 'vuex'import actions from '@/store/actions.js'
import getters from '@/store/getters.js'
import mutations from '@/store/mutations.js'
import state from '@/store/state.js'const app = createApp({})const store = createStore({
actions,
getters,
mutations,
state,
})app.use(store)
Vuelidate
Para a validação de informações nos forms, utilizamos o Vuelidate, sua facilidade para criação e customização, independente do tipo, é muito útil para garantir que o que está sendo enviado é de fato o que se espera, como se foi de praxe até aqui, sua nova versão trouxe algumas alterações para fazer a alegria da equipe, começando com:
Nova importação:
Sei que já está repetitivo, mas como isso nos afeta, deve ser listado aqui, dessa vez, um pouco diferente de suas predecessoras o Vuelidate foi separado em dois pacotes @vuelidate/core e @vuelidate/validators, interferindo em como é feita sua importação, a primeira para uso na aplicação:
//Antigo
import Vue from 'vue'
import Vuelidate from 'vuelidate'Vue.use(Vuelidate)// Novo
import { createApp } from 'vue'
import useVuelidate from '@vuelidate/core'const app = createApp({})app.use(useVuelidate)
E a segunda para uso das validações em arquivos locais
// Antigoimport { minLength, maxLength, required, minValue } from 'vuelidate/lib/validators'// Novoimport { required, minLength } from '@vuelidate/validators'
Remoção do $each
Esse talvez seja o ponto mais crítico de nossa migração, a remoção do $each, que usamos muito para validar arrays, vai fazer com que tenhamos que alterar diversos componentes e seguir a recomendação da documentação e esmiuçá-los em componentes ainda menores para que se consiga validá-los, mas você deve estar se perguntando `Separar em componentes é muito tranquilo!`, realmente dependendo do componente realmente é, mas não quando você tem uma tabela complexa, como a que temos aqui, com interações em praticamente todos os seus campos, além de expandir podendo conter outra tabela dentro ou algumas checkboxes, mas enfim esse é um problema para o futuro, agora para explicar melhor segue abaixo o exemplo de como usamos e a recomendação de como deve ser alterado:
import { required, minLength } from 'vuelidate/lib/validators'
import { notOnlySpace } from '@/helpers/validators.js'export default {
name: 'ProductsOnOfferCombo',
data: () => ({
products: []
}),
validations() {
return {
offerComboArray: {
$each: {
name: { required, notOnlySpace, minLength: minLength(3) },
}
},
}
},
}
</script>
E aqui como deve ser:
<template>
<div>
<ProductInput
v-for="(product, index) of products"
:product="product"
:key="index"
@updateProduct="product = $event"
/>
</div>
</template><script>
import { required, minLength } from '@vuelidate/validators'
import { notOnlySpace } from '@/helpers/validators.js'export default {
name: 'ProductsOnOfferCombo',
data: () => ({
products: []
})
}
</script>// ProductInput
<template>
<div>
<input
type="text"
:value="product.name"
@input="$emit('updateProduct', { name: $event })"
/>
</div>
</template><script>
import useVuelidate from '@vuelidate/core'export default {
props: {
product: { type: Object, required: true },
},
setup () {
return { v$: useVuelidate() }
},
validations: {
product: {
name: { required, notOnlySpace, minLength: minLength(3) }
}
}
}
</script>
Bom essas foram as principais mudanças que encontrei referentes a migração para o Vue 3, caso seja necessário farei um update do artigo no futuro, deixarei abaixo os links referentes as migração. Obrigado e até a próxima.
Referências: