Aguarde...

15 de novembro de 2019

Uma análise antecipada da API de composição do Vue 3 em estado selvagem

Uma análise antecipada da API de composição do Vue 3 em estado selvagem

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 ( refreactive), adereços computados, métodos (funções simples), observadores ( watch) e ganchos do ciclo de vida ( onMountedonUnmounted). 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 setupfunção.

Imagine novamente que você precisa fazer algumas alterações na lógica de rastreamento de atividades. Tudo relacionado a essa funcionalidade vive na useActivityTrackerfunçã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/*.jse 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/*.jse 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.

Postado em Blog
Escreva um comentário