É comum ver a Orientação a Objetos ser relacionado a herança, mais especificamente reuso de lógicas e comportamentos. Porém, herança não é algo exclusivo da Orientação a Objetos.
Existem diversas técnicas que provém herança. Dentro do contexto da Orientação a Objetos existe a composição, que infelizmente muitos deixam de lado ou simplesmente não conhecem.
Nesse artigo vou listar as diversas maneiras de reaproveitamento e herança de código com Vue.js.
Estrutura de um componente
Um componente Vue.js é criado a partir de um objeto de configuração. Cada propriedade desse objeto determina os recursos e comportamentos do componente. As mais comuns são data, methods, computed e props, há ainda alguns hooks como created, mounted e beforeDestroy.
Essas propriedades podem ser objetos ou funções, a documentação é bem rica e explica cada um deles. Alguns plugins como vue-router adicionam outros opções a essa lista.
Esses plugins usam de mixins ou manipulação do prototype do Vue para adicionar features e comportamentos ao Vue.
Antes de entrar em detalhes sobre mixins e até mesmo o extends, quero que você se atente a algumas coisas.
Estrutura dinamica
Estamos falando de JavaScript, e objetos JavaScript podem ser criados dinamicamente.
const data = () => ({ title: 'Construindo um componente' }) const methods = { changeTitle(title) { this.title = title } } const template = `<div> <h1 @click="changeTitle('novo titulo')" >{{ title }}<h1/> </div>` export default { data, methods, template }
Esse é um exemplo muito bobo e simplório, porém ilustra a capacidade de criar um componente a partir de outras “partes” de código, algo parecido com isso é usado nos helpers mapState, mapGetters e mapActions do Vuex.
import { mapState, mapGetters, mapActions } from 'vuex' export default { computed: { ...mapState(['user']), ...mapGetters(['isLogged']), }, methods: { ...mapActions(['logout']) } }
Que pode ser “traduzido” para o código abaixo, ilustrando o que acontece por baixo dos panos: mapState([‘user’]) dinamicamente cria um objeto com funções que acessam o state do store da aplicação.
const state = { user() { return this.$store.state.user } } const getters = { isLogged() { return this.$store.gettes.isLogged } } const actions = { logout() { return this.$store.dispatch('logout') } } export default { // computed: { ...state, ...getters } computed: Object.assign({ }, state, getters), methods: Object.assign({ }, actions) }
Ter uma plena compreensão desses mecanismos abre inúmeras possibilidades de reuso e herança.
Extends
A opção extends é bem simples de se entender, pois lembra muito herança clássica. Essa opção permite herdar/injetar comportamento/código no componente.
Ao estender um componente, você pode sobrescrever seu comportamento, criando um comportamento local novo, extremamente semelhante a herança clássica.
Como você também herda coisas como template, é possível criar um componente base e estender ele adicionando coisas especificas.
Imagine uma componente responsável por listar um determinado recurso.
import Grid from 'my-core/components/grid/common' export default { extends: Grid, data () { return { resource: '/clients/', actions: ['edit'], columns: [{ key: 'name', label: 'Name' }, { key: 'email', label: 'E-Mail' }] } }, methods: { callEdit(user) { // do something } } }
As possibilidades são inúmeras, é possível criar um factory que receba as opções e construa em tempo de execução um objeto a ser estendido. Tudo depende do objetivo e da maneira que se quer usar.
import Grid from 'my-core/components/grid/common' export default { extends: Grid({ resource: '/clients/', actions: ['edit'], columns: [{ key: 'name', label: 'Name' }, { key: 'email', label: 'E-Mail' }] }), methods: { callEdit(user) { // do something } } }
É possível estender componentes de uma biblioteca de terceiros por exemplo, isso facilita e muito o reaproveitamento.
Mixins
Os mixins são extremamente semelhantes ao extends. A principal diferença é a possibilidade de usar mais de um mixin em um único componente.
Há dois usos comuns para mixins, encapsular regras ou métodos muito utilizados em vários componentes, e injetar comportamentos em todos os componentes da aplicação (global mixin), muito comuns em plugins.
export default Vue => { Vue.mixin({ created () { const { title } = this.$options if (title !== undefined) { this.$title = title } } }) Object.defineProperty(Vue.prototype, '$title', { get () { return this.$store.state.title }, set () { this.$store.dispatch('setTitle', ...arguments) } }) }
import Vue from 'vue' import titlePlugin from './title-plugin' Vue.use(titlePlugin) ///
<script> export default { title: 'Page Title' } </script> <template> <div> <h1> {{ $title }} </h1> </div> </template>
Esses snippets demostram o uso de mixins de maneira global. Partindo do principio que você esta usando vuex e possui um estado chamado title e uma action chamada setTitle.
A função created do mixin será executada em todos os componentes, e quando a opção title existir, ela vai modificar $title, que usa get/set para recuperar e modificar o state title do vuex.
Usar mixins abre um grande leque de possibilidades, ainda é possível usar factories e ampliar ainda mais as possibilidades.
import ProtectMinStep from './steps/mixins/protect-min-step' import StepData from './steps/mixins/step-data' import UserData from './users/mixins/user-data' export default { mixins: [ ProtectMinStep(3), StepData(4), UserData ], }
Imagine um cenário onde você tem uma aplicação que possui etapas a serem cumpridas, ou onde você usa …mapState([‘user’]) com muita frequência…
Qualquer situação de “repetição de código” um mixin pode ser utilizado.
Vue.prototype
JavaScript é uma linguagem multiparadigma baseada em protótipos. Ela permite mudar comportamentos e objetos em tempo de execução (runtime).
Essas características dão ao JavaScript um poder incrível.
Um componente Vue.js é uma instancia do próprio Vue, isso significa que podemos mudar ou adicionar comportamentos em todos os componentes da aplicação.
No exemplo acima, foram injetadas duas propriedades no protótipo do Vue: $http e $avatar, $http expõe um cliente HTTP do axios, enquanto $avatar é apenas uma função que retorna uma url.
Essas propriedades ficam disponíveis em todos os componentes, então é possível usar this.$http no script e $avatar na template. $avatar é um exemplo bobo, porém $http é uma implementação muito comum.
Essa manipulação não se limita a isso, é possível fazer modificações profundas, como no exemplo abaixo (abra o exemplo em uma nova janela e veja o console do navegador)
Não é um exemplo muito comum, porém prova o quão longe podemos ir.
Slots
Slots é uma maneira extremamente comum e simples de se compor componentes. Há um excelente artigo no Vue.js Brasil sobre isso, como o tema é muito bem abordado na documentação e no artigo, não entrarei em detalhes, apenas deixarei dois exemplos de como uma aplicação poderia ser feita com slots. A dica é pensar em legos.
<template> <div> <Modal ref="modal"> <h1 slot="title">{{ user.name }}</h1> <p>{{ user.bio }}</p> </Modal> <button type="button" @click="open">Open</button> </div> </template> <script> import Modal from './modal' export default { components: { Modal }, data () { return { user: { name: 'Vinicius', bio: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit...' } } }, methods: { open () { this.$refs.modal.open() } } } </script>
<template> <RootContainer :menu="menuData" :show-sidebar="showSidebar"> <view-router></view-router> <footer slot="footer">copyright</footer> </RootContainer> </template> <script> import RootContainer from './root-container' export default { components: { RootContainer }, data () { return { menuData: {} } }, computed: { showSidebar () { // logic here } } } </script>
Se quiser saber mais sobre meu trabalho, visite codecasts.com.br. Lá você vai ver vídeos sobre JavaScript, jQuery, Gulp, ES6, Vue.JS, Docker, e muito mais.
Thanks to William and Emanuel G de Souza.