Vue.js 3: перспективы развития

Spread the love

В статье описано как новое API внедренное в Vue.js 3 на основе функций будет решать проблему повторного использования логики.

Оригинальная статья: Taras Batenkov Vue.js 3: Future-Oriented Programming

Если вы работаете с Vue.js, то вы, вероятно, уже слышали о 3-й версии этого фреймворка, которая будет выпущена в ближайшее время (если вы читаете эту статью в будущем, я надеюсь, что она по-прежнему актуальна 😉). Новая версия в настоящее время активно разрабатывается, но все возможные функции можно найти в отдельном репозитории RFC: https://github.com/vuejs/rfcs. Одна из них, function-api, может кардинально изменить стиль разработки приложений Vue.

Эта статья предназначена для людей, которые хоть немного знакомы с JavaScript и Vue.

Что не так с текущим API? 👀

Лучший способ – показать все на примере. Итак, давайте представим, что нам нужно реализовать компонент, который должен извлекать некоторые пользовательские данные, отображать состояние загрузки и верхнюю панель в зависимости от смещения прокрутки. Вот пример окончательного результата:

Живой пример вы можете посмотреть здесь.

Хорошей практикой является выделение некоторой логики для повторного использования в нескольких компонентах. В текущем API Vue 2.x существует ряд общих шаблонов, наиболее известными из которых являются:

  • Миксины (используя mixin) 🍹
  • Компоненты высшего порядка (HOCs – Higher-order components) 🎢

Итак, давайте переместим логику отслеживания прокрутки в миксин и извлечем логику в компонент более высокого порядка. Ниже будет типичная реализация с Vue этой логики.

Скрол миксин:

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

Здесь мы добавляем слушителя события прокрутки addEventListener, таким образом отслеживаем смещение страницы и сохраняем его в свойстве pageOffset.

Компонент высшего порядка будет выглядеть так:

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
            }
        })
    }
})

Здесь isLoading, posts инициализированы для отслеживания загрузки состояния и данных сообщений соответственно. Метод fetchPosts будет вызываться после создания экземпляра и каждый раз, когда изменяется props.id, для получения данных для нового id.

Это не полная реализация HOC, но для примера этого будет достаточно. Здесь мы просто обертываем целевой компонент и передаем оригинальный props  вместе с props, связанными с fetch.

Конечный компонент выглядит так:

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

Чтобы получить указанные props, их следует обернуть в созданный HOC:

const PostsPage = withPostsHOC(PostsPage)

Полный компонент с шаблоном и стилями можно найти здесь.

Супер! 🥳 Мы только что реализовали нашу задачу, используя mixin и HOC, теперь их можно использовать и в других компонентах. Но не все так радужно, есть несколько проблем с этими подходами.

1. Столкновение пространства имен ⚔️

Представьте, что нам нужно добавить метод update в наш компонент:

// ...
<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>
// ...

Если вы снова откроете страницу и прокрутите ее, верхняя панель больше не будет отображаться. Это связано с перезаписью метода update в mixin. То же самое работает для HOCs. Если вы измените поле данных fetchedPosts на posts:

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

… вы получите такие ошибки:

Причина этого заключается в том, что в обернутом компоненте уже указано свойство с именем posts.

2. Неясные источники 📦

Что если через некоторое время вы решили использовать другой миксин в своем компоненте:

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

Сможете ли вы сказать, из какого именно миксина было введено свойство pageOffset? Или в другом сценарии оба миксина могут иметь, например, yOffset, поэтому последний миксин будет переопределять свойство из предыдущего. Это не хорошо и может вызвать много неожиданных ошибок. 😕

3. Производительность ⏱

Другая проблема с HOC состоит в том, что отдельные экземпляры компонентов, созданные просто для целей повторного использования логики, приводят к снижению производительности.

Vue.js 3 🏗

Давайте посмотрим, какую альтернативу может предложить следующая версия Vue.js и как мы можем решить эту проблему, используя API на основе функций.

Поскольку Vue 3 еще не выпущен, был создан вспомогательный плагин – vue-function-api. Он предоставляет функцию api из Vue3.x для Vue2.x для разработки приложений Vue следующего поколения.

Во-первых, вам нужно установить его:

$ npm install vue-function-api

и явно подключить через Vue.use():

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

Vue.use(plugin)

Основным дополнением, основанным на API-функциях, является новая опция компонента – setup(). Как следует из названия, это место, где мы используем функции нового API для настройки логики нашего компонента. Итак, давайте реализуем функцию отображения верхней панели в зависимости от смещения прокрутки. Пример базового компонента:

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

Обратите внимание, что функция setup получает объект props в качестве первого аргумента, и этот объект props является реактивным. Мы также возвращаем объект, содержащий свойство pageOffset, чтобы он был доступным для контекста рендеринга шаблона. Это свойство также становится реактивным, но только в контексте визуализации. Мы можем использовать его в шаблоне как обычно:

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

Но это свойство должно быть изменено при каждом событии прокрутки. Чтобы реализовать это, нам нужно добавить слушитель события прокрутки (scroll event listener), когда компонент будет mounted, и удалить слушителя – когда unmounted. Для этого были созданы функции API value, onMounting, onUnmounting :

// ...
<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>
// ...

Обратите внимание, что все хуки жизненного цикла в версии Vue 2.x имеют эквивалентную функцию onXXX, которую можно использовать внутри setup().

Вы, вероятно, также заметили, что переменная pageOffset содержит единственное реактивное свойство: .value. Нам нужно использовать это упакованное свойство, потому что примитивные значения в JavaScript, такие как числа и строки, не передаются по ссылке. Оболочки значений обеспечивают способ передачи изменчивых и реактивных ссылок для произвольных типов значений.

Вот как выглядит объект pageOffset:

Следующим шагом является реализация выборки данных пользователя. Как и при использовании API на основе параметров, вы можете объявлять вычисленные значения (computed values) и watchers, используя API на основе функций:

// ...
<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>
// ...

Вычисленное значение ведет себя так же, как вычисляемое свойство 2.x: оно отслеживает свои зависимости и вычисляет заново только при изменении зависимостей. Первый аргумент, передаваемый для watch, называется «source», который может быть одним из следующих:

  • геттер (getter) функция
  • обертка значения
  • массив, содержащий два вышеуказанных типа

Второй аргумент – это обратный вызов, который вызывается только тогда, когда значение, возвращаемое из метода getter или оболочки значения, изменилось.

Мы только что реализовали целевой компонент, используя API на основе функций. 🎉 Следующий шаг – сделать всю эту логику многократно используемой.

Декомпозиция 🎻 ✂️

Это самая интересная часть: для повторного использования кода, связанного с фрагментом логики, мы просто можем извлечь его в так называемую “композиционную функцию” (“composition function”) и вернуть реактивное состояние:

// ...
<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>
// ...

Обратите внимание, как мы использовали функции useFetchPosts и useScroll для возврата реактивных свойств. Эти функции могут храниться в отдельных файлах и использоваться в любом другом компоненте. По сравнению с option-based решением:

  • Свойства, предоставляемые шаблону, имеют понятные источники (sources), поскольку они являются значениями, возвращаемыми из функций композиции;
  • Возвращенные значения из композиционных функций имеют произвольные имена, поэтому нет столкновений пространства имен;
  • Не существует ненужных экземпляров компонентов, созданных только для повторного использования логики.

Существует множество других преимуществ, которые можно найти на официальной странице RFC.

Все примеры кода, используемые в этой статье, вы можете найти здесь.

Живой пример компонента вы можете посмотреть здесь.

Заключение

Как вы можете видеть, API на основе функций Vue представляет собой простой и гибкий способ создания логики внутри и между компонентами без каких-либо недостатков API на основе параметров. Только представьте, какими мощными могут быть функции композиции для любого типа проекта – от маленьких до больших, сложных веб-приложений. 🚀

Я надеюсь, что этот пост был вам полезен 🎓. Если у вас есть какие-либо мысли или вопросы, пожалуйста, не стесняйтесь отвечать и комментировать ниже (в оригинальной статье автора)! Я буду рад ответить 🙂. Благодарю.


Spread the love

Добавить комментарий

Ваш e-mail не будет опубликован.