Vue.Js 3, la Programación Orientada Al Futuro Que No Hay Que Perderse!

by Luigi Nori Date: 25-10-2019 vue hooks javascript react mixin


Si estás interesado en Vue.js, probablemente conozcas la 3ª versión de este framework, que se publicará en breve. La nueva versión de momento está en desarrollo, pero todas las características posibles se pueden encontrar en un repositorio RFC separado: https://github.com/vuejs/rfcs. Una de esas, function-api, puede cambiar dramáticamente el estilo de desarrollo de las aplicaciones Vue.

Este artículo está dirigido a personas que tienen al menos algún tipo de experiencia en JavaScript y Vue.

¿Qué tiene de malo la API actual de VUE 2?

La mejor manera es mostrarlo todo en un ejemplo. Por lo tanto, imaginemos que necesitamos implementar un componente que debería obtener los datos de algunos usuarios, mostrar el estado de carga y la barra superior dependiendo de la desviación de desplazamiento. Aquí está el resultado final:

Es una buena práctica extraer algo de lógica para reutilizarla en múltiples componentes. Con la API actual de Vue 2.x, hay una serie de patrones comunes, los más conocidos son:

  • Mixins (usando la opción mixins )
  • Componentes Higher-order (HOCs)

Por lo tanto, movamos la lógica de seguimiento de desplazamiento a un mixin, y busquemos la lógica en un componente de orden superior. La implementación típica con Vue se puede ver a continuación.

Scroll mixin:

const scrollMixin = {
    data() {
        return {
            pageOffset: 0
        }
    },
    mounted() {
        window.addEventListener('scroll', this.update)
    },
    destroyed() {
        window.removeEventListener('scroll', this.update)
    },
    methods: {
        update() {
            this.pageOffset = window.pageYOffset
        }
    }
}

Aquí añadimos la escucha de evento scroll (event listener), rastrear el desplazamiento de página y guardarlo en la propiedad pageOffset.

El componente de orden superior tendrá este aspecto::

import { fetchUserPosts } from '@/api'

const withPostsHOC = WrappedComponent => ({
    props: WrappedComponent.props,
    data() {
        return {
            postsIsLoading: false,
            fetchedPosts: []
        }
    },
    watch: {
        id: {
            handler: 'fetchPosts',
            immediate: true
        }
    },
    methods: {
        async fetchPosts() {
            this.postsIsLoading = true
            this.fetchedPosts = await fetchUserPosts(this.id)
            this.postsIsLoading = false
        }
    },
    computed: {
        postsCount() {
            return this.fetchedPosts.length
        }
    },
    render(h) {
        return h(WrappedComponent, {
            props: {
                ...this.$props,
                isLoading: this.postsIsLoading,
                posts: this.fetchedPosts,
                count: this.postsCount
            }
        })
    }
})

Aquí se inicializan las propiedades isLoading, posts para el estado de carga y posts data respectivamente. El método fetchPosts se invocará después de crear una instancia y cada vez que props.id cambia, para poder obtener datos para nuevas id.

No es una implementación completa de la COA, pero para este ejemplo, será suficiente. Aquí sólo tenemos que envolver el componente de destino y pasar las propriedades originales junto con las propriedades relacionadas con el fetch.

El componente de destino tiene este aspecto:

// ...
<script>
export default {
    name: 'PostsPage',
    mixins: [scrollMixin],
    props: {
        id: Number,
        isLoading: Boolean,
        posts: Array,
        count: Number
    }
}
</script>
// ...

Para obtener las propriedades especificadas debe ser envuelto en HOC creado:

_const_ PostsPage = **withPostsHOC(**PostsPage**)**

El componente completo con plantilla y estilos se puede encontrar en este enlace.

¡Genial! Acabamos de implementar nuestra tarea utilizando mixin y HOC, para que puedan ser utilizados por otros componentes. Pero no todo es tan prometedor, hay varios problemas con estos enfoques.

1. Choque de Namespace

Imagina que tenemos que añadir el método update a nuestro componente:

// ...
<script>
export default {
    name: 'PostsPage',
    mixins: [scrollMixin],
    props: {
        id: Number,
        isLoading: Boolean,
        posts: Array,
        count: Number
    },
    methods: {
        update() {
            console.log('some update logic here')
        }
    }
}
</script>
// ...

Si vuelves a abrir la página y la desplaza, la barra superior ya no se mostrará. Esto se debe a la sobreescritura del método de mixin update. Lo mismo sucede con las COA. Si modificas el campo de datos fetchedPosts a posts:

const withPostsHOC = WrappedComponent => ({
    props: WrappedComponent.props, // ['posts', ...]
    data() {
        return {
            postsIsLoading: false,
            posts: [] // fetchedPosts -> posts
        }
    },
    // ...

…obtendrás errores como este:

La razón de esto es que el componente envuelto ya especificó la propiedad con el nombre posts.

2. Fuentes poco claras

¿Que pasa si después de un tiempo decidieras usar otro mixin en tu componente?

// ...
<script>
export default {
    name: 'PostsPage',
    mixins: [scrollMixin, mouseMixin],
// ...

¿Se puede saber exactamente de qué mixin se inyectó una propiedad pageOffset? O en otro escenario, ambos mixins pueden tener, por ejemplo, la propiedad yOffset, por lo que el ultimo mixin anulará la propiedad de lo anterior. Eso no es bueno y puede causar muchos errores inesperados.

3. Rendimiento

Otro problema con las HOC es que necesitamos instancias de componentes separadas creadas sólo para fines de reutilización lógica que tienen un coste de rendimiento.

Vamos a hacer un “setup” de VUE 3

Veamos qué alternativa puede ofrecer la próxima versión de Vue.js y cómo podemos resolver el mismo problema usando la API basada en funciones.

Como Vue 3 aún no está disponible, se ha creado el plugin de ayuda — vue-function-api. Proporciona la función api de Vue3.x a Vue2.x para el desarrollo de aplicaciones Vue de nueva generación.

En primer lugar, es necesario instalarlo:

$ **npm** install vue-function-api

e instalar explícitamente a través de Vue.use():

import Vue from 'vue'
import { plugin } from 'vue-function-api'

Vue.use(plugin)

La principal adición que proporciona la API basada en funciones es una nueva opción de componente - setup(). Como su nombre indica, este es el lugar donde utilizamos las funciones de la nueva API para configurar la lógica de nuestro componente. Por lo tanto, vamos a implementar una característica para mostrar la barra superior en función de la desviación de desplazamiento. Ejemplo de componente básico:

// ...
<script>
export default {
  setup(props) {
    const pageOffset = 0
    return {
      pageOffset
    }
  }
}
</script>
// ...

Ten en cuenta que la función de configuración setup recibe el objeto de puntales resueltos como su primer argumento y este objeto props es reactivo. También devolvemos un objeto que contiene la propiedad pageOffset para ser expuesto al contexto de render de la plantilla. Esta propiedad también se vuelve reactiva, pero sólo en el contexto del render. Podemos usarlo en la plantilla como de costumbre:

<div class="topbar" :class="{ open: pageOffset > 120 }">...</div>

Pero esta propiedad debería mutar en cada evento de desplazamiento. Para implementar esto, necesitamos agregar el listener de eventos de desplazamiento cuando el componente será montado y remover el listener - cuando sea desmontado. Para estos fines, existen funciones de API value, onMounted, onUnmounted:

// ...
<script>
import { value, onMounted, onUnmounted } from 'vue-function-api'
export default {
  setup(props) {
    const pageOffset = value(0)
    const update = () => {
        pageOffset.value = window.pageYOffset
    }
    
    onMounted(() => window.addEventListener('scroll', update))
    onUnmounted(() => window.removeEventListener('scroll', update))
    
    return {
      pageOffset
    }
  }
}
</script>
// ...

Tenga en cuenta que todos los gancho(hooks)s de ciclo de vida en la versión 2.x de Vue tienen una función equivalente onXXX que puede ser usada dentro de setup().

Probablemente también has notado que la variable pageOffset contiene una sola propiedad reactiva: .value. Necesitamos usar esta propiedad porque los valores primitivos en JavaScript como los números y las cadenas no se pasan por referencia. Las envolturas de valor proporcionan una forma de pasar referencias mutables y reactivas para tipos de valores arbitrarios.

Así es como se ve el objeto pageOffset:

El siguiente paso es implementar la obtención de datos del usuario. Al igual que cuando se utiliza la API basada en opciones, se pueden declarar valores calculados y observadores utilizando la API basada en funciones:

// ...
<script>
import {
    value,
    watch,
    computed,
    onMounted,
    onUnmounted
} from 'vue-function-api'
import { fetchUserPosts } from '@/api'
export default {
  setup(props) {
    const pageOffset = value(0)
    const isLoading = value(false)
    const posts = value([])
    const count = computed(() => posts.value.length)
    const update = () => {
      pageOffset.value = window.pageYOffset
    }
    
    onMounted(() => window.addEventListener('scroll', update))
    onUnmounted(() => window.removeEventListener('scroll', update))
    
    watch(
      () => props.id,
      async id => {
        isLoading.value = true
        posts.value = await fetchUserPosts(id)
        isLoading.value = false
      }
    )
    
    return {
      isLoading,
      pageOffset,
      posts,
      count
    }
  }
}
</script>
// ...

Un valor calculado se comporta como una propiedad calculada 2.x: rastrea sus dependencias y sólo reevalúa cuando éstas han cambiado. El primer argumento que se pasa a watch se llama "source", que puede ser uno de los siguientes:

  • una función getter
  • un valor wrapper
  • un array containing que contiene los dos tipos anteriores

El segundo argumento es una llamada de retorno que sólo será llamada cuando el valor devuelto por el getter o la envoltura de valor haya cambiado.

Acabamos de implementar el componente de destino utilizando una API basada en funciones.

El siguiente paso es hacer que toda esta lógica sea reutilizable..

Decomposición

Esta es la parte más interesante, para reutilizar código relacionado con un trozo de lógica sólo podemos extraerlo a lo que llamamos una “composition function” y devolverlo en estado reactivo:

// ...
<script>
import {
    value,
    watch,
    computed,
    onMounted,
    onUnmounted
} from 'vue-function-api'
import { fetchUserPosts } from '@/api'
function useScroll() {
    const pageOffset = value(0)
    const update = () => {
        pageOffset.value = window.pageYOffset
    }
    onMounted(() => window.addEventListener('scroll', update))
    onUnmounted(() => window.removeEventListener('scroll', update))
    return { pageOffset }
}
function useFetchPosts(props) {
    const isLoading = value(false)
    const posts = value([])
    watch(
        () => props.id,
        async id => {
            isLoading.value = true
            posts.value = await fetchUserPosts(id)
            isLoading.value = false
        }
    )
    return { isLoading, posts }
}
export default {
    props: {
        id: Number
    },
    setup(props) {
        const { isLoading, posts } = useFetchPosts(props)
        const count = computed(() => posts.value.length)
        return {
            ...useScroll(),
            isLoading,
            posts,
            count
        }
    }
}
</script>
// ...

Observe cómo usamos useFetchPosts y useScroll para devolver las propiedades reactivas. Estas funciones pueden almacenarse en archivos separados y utilizarse en cualquier otro componente. En comparación con la solución basada en opciones:

  • Las propiedades expuestas a la plantilla tienen fuentes claras ya que son valores devueltos por las funciones de composición;
  • Devuelve los valores de las funciones de composición nombradas arbitrariamente para que no haya colisión de espacios de nombres;
  • No hay instancias de componentes innecesarias creadas sólo con fines de reutilización lógica.

Hay muchos otros beneficios que se pueden encontrar en la página oficial de RFC.

Todos los ejemplos de código utilizados en este artículo se pueden encontrar aquí.

Ejemplo en vivo del componente que puede ver aquí.

Conclusión

Como puedes ver, la API basada en funciones de Vue presenta una forma limpia y flexible de componer la lógica dentro y entre componentes sin ninguna de las desventajas de la API basada en opciones. Imagína lo poderosas que podrían ser las funciones de composición para cualquier tipo de proyecto, desde aplicaciones web pequeñas hasta grandes y complejas.

Espero que este post haya sido de utilidad

 
by Luigi Nori Date: 25-10-2019 vue hooks javascript react mixin visitas : 582  
 
Luigi Nori

Luigi Nori

Lavora in Internet dal 1994 (praticamente una mummia), specializzato in tecnologie Web fa felici i suoi clienti smanettando con applicazioni su larga scala e ad alta disponibilità, frameworks php e js, disegno web, intercambio dati, sicurezza, e-commerce, amministrazione database e server, hacking etico. Convive felicemente con @salvietta150x40, nel (poco) tempo libero cerca di addomesticare un piccolo nano selvaggio appassionato di astri.

 
 
 

Artículos relacionados