
Recentemente, tive a oportunidade de experimentar a nova API Vue Composition em um projeto real para verificar onde ela pode ser útil e como poderíamos usá-la no futuro.
Até agora, quando estávamos criando um novo componente, estávamos usando a API de opções . Essa API nos forçou a separar o código do componente por opções, o que significa que precisamos ter todos os dados reativos em um local ( data
), todas as propriedades computadas em um local ( computed
), todos os métodos em um local ( methods
) e assim por diante.
Como é prático e legível para componentes menores, torna-se doloroso quando o componente fica mais complicado e lida com várias funcionalidades. Geralmente, a lógica relacionada a uma funcionalidade específica contém alguns dados reativos, propriedades computadas, um método ou alguns deles; às vezes, também envolve o uso de ganchos do ciclo de vida do componente. Isso faz você alternar constantemente entre diferentes opções no código ao trabalhar em uma única preocupação lógica.
O outro problema que você pode ter encontrado ao trabalhar com o Vue é como extrair uma lógica comum que pode ser reutilizada por vários componentes. O Vue já tem poucas opções para fazer isso, mas todos eles têm suas próprias desvantagens (por exemplo , mixins e slots com escopo ).
A API Composition traz uma nova maneira de criar componentes, separar código e extrair partes reutilizáveis de código.
Vamos começar com a composição do código dentro de um componente.
# Composição do código
Imagine que você tem um componente principal que configura poucas coisas para todo o aplicativo Vue (como o layout no Nuxt ). Ele lida com as seguintes coisas:
- localidade de configuração
- verificar se o usuário ainda está autenticado e redirecioná-lo, se não estiver
- impedindo que o usuário recarregue o aplicativo muitas vezes
- rastreando a atividade do usuário e reagindo quando o usuário está inativo por um período específico
- ouvindo um evento usando EventBus (ou evento de objeto de janela)
Essas são apenas algumas coisas que o componente pode fazer. Você provavelmente pode imaginar um componente mais complexo, mas isso servirá ao objetivo deste exemplo. Por uma questão de legibilidade, estou apenas usando nomes dos adereços sem a implementação real.
É assim que o componente se pareceria usando a API de opções:
<template>
<div id="app">
...
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
userActivityTimeout: null,
lastUserActivityAt: null,
reloadCount: 0
}
},
computed: {
isAuthenticated() {...}
locale() {...}
},
watch: {
locale(value) {...},
isAuthenticated(value) {...}
},
async created() {
const initialLocale = localStorage.getItem('locale')
await this.loadLocaleAsync(initialLocale)
},
mounted() {
EventBus.$on(MY_EVENT, this.handleMyEvent)
this.setReloadCount()
this.blockReload()
this.activateActivityTracker()
this.resetActivityTimeout()
},
beforeDestroy() {
this.deactivateActivityTracker()
clearTimeout(this.userActivityTimeout)
EventBus.$off(MY_EVENT, this.handleMyEvent)
},
methods: {
activateActivityTracker() {...},
blockReload() {...},
deactivateActivityTracker() {...},
handleMyEvent() {...},
async loadLocaleAsync(selectedLocale) {...}
redirectUser() {...}
resetActivityTimeout() {...},
setI18nLocale(locale) {...},
setReloadCount() {...},
userActivityThrottler() {...},
}
}
</script>
Como você pode ver, cada opção contém partes de todas as funcionalidades. Não existe uma separação clara entre eles e isso dificulta a leitura do código, especialmente se você não é a pessoa que o escreveu e está olhando pela primeira vez. É muito difícil encontrar qual método é usado por qual funcionalidade.
Vamos analisar novamente, mas identificar as preocupações lógicas como comentários. Esses seriam:
- Rastreador de atividades
- Recarregar bloqueador
- Verificação de autenticação
- Localidade
- Registro de barramento de eventos
<template>
<div id="app">
...
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
userActivityTimeout: null, // Activity tracker
lastUserActivityAt: null, // Activity tracker
reloadCount: 0 // Reload blocker
}
},
computed: {
isAuthenticated() {...} // Authentication check
locale() {...} // Locale
},
watch: {
locale(value) {...},
isAuthenticated(value) {...} // Authentication check
},
async created() {
const initialLocale = localStorage.getItem('locale') // Locale
await this.loadLocaleAsync(initialLocale) // Locale
},
mounted() {
EventBus.$on(MY_EVENT, this.handleMyEvent) // Event Bus registration
this.setReloadCount() // Reload blocker
this.blockReload() // Reload blocker
this.activateActivityTracker() // Activity tracker
this.resetActivityTimeout() // Activity tracker
},
beforeDestroy() {
this.deactivateActivityTracker() // Activity tracker
clearTimeout(this.userActivityTimeout) // Activity tracker
EventBus.$off(MY_EVENT, this.handleMyEvent) // Event Bus registration
},
methods: {
activateActivityTracker() {...}, // Activity tracker
blockReload() {...}, // Reload blocker
deactivateActivityTracker() {...}, // Activity tracker
handleMyEvent() {...}, // Event Bus registration
async loadLocaleAsync(selectedLocale) {...} // Locale
redirectUser() {...} // Authentication check
resetActivityTimeout() {...}, // Activity tracker
setI18nLocale(locale) {...}, // Locale
setReloadCount() {...}, // Reload blocker
userActivityThrottler() {...}, // Activity tracker
}
}
</script>
Vê como é difícil desembaraçar tudo isso? 🙂
Agora imagine que você precisa fazer uma alteração em uma funcionalidade (por exemplo, lógica de rastreamento de atividades). Não apenas é necessário saber quais elementos estão relacionados a essa lógica, mas mesmo quando você sabe, ainda precisa pular para cima e para baixo entre as diferentes opções de componentes.
Vamos usar a API Composition para separar o código por preocupações lógicas. Para isso, criamos uma única função para cada lógica relacionada a uma funcionalidade específica. Isso é o que chamamos de função de composição .
// Activity tracking logic
function useActivityTracker() {
const userActivityTimeout = ref(null)
const lastUserActivityAt = ref(null)
function activateActivityTracker() {...}
function deactivateActivityTracker() {...}
function resetActivityTimeout() {...}
function userActivityThrottler() {...}
onBeforeMount(() => {
activateActivityTracker()
resetActivityTimeout()
})
onUnmounted(() => {
deactivateActivityTracker()
clearTimeout(userActivityTimeout.value)
})
}
// Reload blocking logic
function useReloadBlocker(context) {
const reloadCount = ref(null)
function blockReload() {...}
function setReloadCount() {...}
onMounted(() => {
setReloadCount()
blockReload()
})
}
// Locale logic
function useLocale(context) {
async function loadLocaleAsync(selectedLocale) {...}
function setI18nLocale(locale) {...}
watch(() => {
const locale = ...
loadLocaleAsync(locale)
})
// No need for a 'created' hook, all logic that runs in setup function is placed between beforeCreate and created hooks
const initialLocale = localStorage.getItem('locale')
loadLocaleAsync(initialLocale)
}
// Event bus listener registration
import EventBus from '@/event-bus'
function useEventBusListener(eventName, handler) {
onMounted(() => EventBus.$on(eventName, handler))
onUnmounted(() => EventBus.$off(eventName, handler))
}
Como você pode ver, podemos declarar dados reativos ( ref
/ reactive
), adereços computados, métodos (funções simples), observadores ( watch
) e ganchos do ciclo de vida ( onMounted
/ onUnmounted
). Basicamente, tudo o que você normalmente usa em um componente.
Temos duas opções quando se trata de onde manter o código. Podemos deixá-lo dentro do componente ou extraí-lo em um arquivo separado. Como a API Composition ainda não está oficialmente lá, não existem práticas recomendadas ou regras sobre como lidar com ela. Do meu ponto de vista, se a lógica estiver fortemente acoplada a um componente específico (ou seja, não será reutilizada em nenhum outro lugar) e não puder viver sem o próprio componente, sugiro deixá-lo dentro do componente. Por outro lado, se for a funcionalidade geral que provavelmente será reutilizada, sugiro extraí-la para um arquivo separado. No entanto, se queremos mantê-lo em um arquivo separado, precisamos lembrar de exportar a função do arquivo e importá-la em nosso componente.
É assim que nosso componente se parece com as funções de composição recém-criadas:
<template>
<div id="app">
</div>
</template>
<script>
export default {
name: 'App',
setup(props, context) {
useEventBusListener(MY_EVENT, handleMyEvent)
useActivityTracker()
useReloadBlocker(context)
useLocale(context)
const isAuthenticated = computed(() => ...)
watch(() => {
if (!isAuthenticated) {...}
})
function handleMyEvent() {...},
function useLocale() {...}
function useActivityTracker() {...}
function useEventBusListener() {...}
function useReloadBlocker() {...}
}
}
</script>
Isso nos dá uma única função para cada preocupação lógica. Se queremos usar alguma preocupação específica, precisamos chamar a função de composição relacionada na nova setup
função.
Imagine novamente que você precisa fazer algumas alterações na lógica de rastreamento de atividades. Tudo relacionado a essa funcionalidade vive na useActivityTracker
função. Agora você sabe instantaneamente onde procurar e pular para o lugar certo para ver todas as partes relacionadas do código. Bela!
# Extraindo partes de código reutilizáveis
No nosso caso, o registro do ouvinte do Event Bus parece um código que podemos usar em qualquer componente que ouça eventos no Event Bus.
Como mencionado anteriormente, podemos manter a lógica relacionada a uma funcionalidade específica em um arquivo separado. Vamos mover nossa configuração de ouvinte do Event Bus para um arquivo separado.
// composables/useEventBusListener.js
import EventBus from '@/event-bus'
export function useEventBusListener(eventName, handler) {
onMounted(() => EventBus.$on(eventName, handler))
onUnmounted(() => EventBus.$off(eventName, handler))
}
Para usá-lo em um componente, precisamos ter certeza de exportar nossa função (nomeada ou padrão) e importá-la em um componente.
<template>
<div id="app">
...
</div>
</template>
<script>
import { useEventBusListener } from '@/composables/useEventBusListener'
export default {
name: 'MyComponent',
setup(props, context) {
useEventBusListener(MY_EVENT, myEventHandled)
useEventBusListener(ANOTHER_EVENT, myAnotherHandled)
}
}
</script>
É isso aí! Agora podemos usar isso em qualquer componente que precisarmos.
# Finalizando
Há uma discussão em andamento sobre a API de composição. Este post não tem a intenção de promover nenhum lado da discussão. É mais uma questão de mostrar quando isso pode ser útil e, em quais casos, agrega valor adicional.
Eu acho que é sempre mais fácil entender o conceito em um exemplo da vida real, como acima. Existem mais casos de uso e, quanto mais você usa a nova API, mais padrões você verá. Esta postagem é apenas alguns padrões básicos para você começar.
Vamos examinar novamente os casos de uso apresentados e ver onde a API Composition pode ser útil:
Recursos gerais que podem viver por conta própria sem acoplamento rígido com qualquer componente específico
- Toda a lógica relacionada a um recurso específico em um arquivo
- Mantenha-o
@/composables/*.js
e importe-o em componentes - Exemplos: rastreador de atividades, bloqueador de recarga e local
Recursos reutilizáveis usados em vários componentes
- Toda a lógica relacionada a um recurso específico em um arquivo
- Mantenha
@/composables/*.js
e importe em componentes - Exemplos: registro do ouvinte do Barramento de Eventos, registro de eventos da janela, lógica de animação comum, uso da biblioteca comum
Organização do código no componente
- Toda a lógica relacionada a um recurso específico em uma função
- Mantenha o código em uma função de composição dentro do componente
- O código relacionado à mesma preocupação lógica está no mesmo local (ou seja, não há necessidade de alternar entre dados, computados, métodos, ganchos do ciclo de vida, etc.)
# Lembre-se: tudo isso é um trabalho em andamento!
Atualmente, a API do Vue Composition está em andamento e está sujeita a alterações futuras. Nada mencionado nos exemplos acima é certo, e a sintaxe e os casos de uso podem mudar. Ele deve ser enviado com o Vue versão 3.0. Enquanto isso, você pode conferir o view-use-web para obter uma coleção de funções de composição que devem ser incluídas no Vue 3, mas que podem ser usadas com a API Composition no Vue 2.