Vue.js 3: майбутньо-орієнтоване програмування

13 хв. читання

Якщо ви цікавитесь Vue.js, то, імовірно, знаєте про третю версію фреймворку, яка зараз активно розробляється. Перелік фіч останньої версії ви знайдете в RFC за посиланням. Найпримітніша з них — function-api, вона може кардинально змінити стиль створення застосунків на Vue.

Ця стаття буде цікава саме тим розробникам, які мають за плечима певний досвід з JavaScript та Vue.

Що не так з поточним API

Краще показати все на практиці. Уявімо, що нам потрібно реалізувати компонент, який підвантажує деякі дані користувача, показує стан завантаження та верхню панель, якщо користувач прокручує сторінку. Все повинно мати такий вигляд:

Vue.js 3: майбутньо-орієнтоване програмування

Перевірити особисто можна за посиланням.

Гарною практикою вважається перевикористання логіки в декількох компонентах. З Vue 2.x API ми можемо використати деякі поширені та добре відомі шаблони для повторного використання коду, зокрема:

  • міксини (через властивість mixins);
  • компоненти вищого порядку (HOC).

Тож перемістимо логіку відстеження прокрутки в міксин, а логіку отримання даних — в компонент вищого порядку. Типову реалізацію у 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
        }
    }
}

Ми додаємо слухача події scroll, відстежуємо зсув сторінки та зберігаємо отримане значення у властивості 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 для завантаження.

Код цільового компонента:

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

Щоб отримати визначені props, його слід огорнути в створений HOC:

const PostsPage = withPostsHOC(PostsPage)

Весь компонент з шаблоном та стилями можна переглянути за посиланням.

Чудово! Ми щойно реалізували поставлене завдання з використанням міксина та 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 міксина. Те ж саме працює і для HOC. Якщо ви заміните поле fetchedPosts на posts:

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

...ви отримаєте подібні помилки:

Vue.js 3: майбутньо-орієнтоване програмування

Причина в тому, що в огорнутому компоненті вже визначено властивість posts.

2. Походження властивостей 📦

Якщо через деякий час ви вирішите використати інший міксин у вашому компоненті:


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

Чи зможете ви точно визначити з якого міксина було введено властивість PageOffset?

Або ж обидва міксина матимуть, наприклад, властивість yOffset, тому останній міксин перевизначить властивість попереднього. Це може спричинити неочікувані баги 😕.

3. Продуктивність ⏱

При роботі з HOC нам треба розділяти екземпляри компонентів, щоб використовувати їх логіку повторно. А такий підхід б'є по продуктивності.

Які є альтернативи

Наступний реліз Vue.js обіцяє порадувати функціональним API, який би враховував недоліки попередніх підходів.

Хоч реліз очікується лише в майбутньому, зараз створено плагін vue-function-api. Він передбачає функціональний API з Vue версій 3.x для Vue версій 2.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 як перший аргумент, котрий до того ж є реактивним. Ми також повертаємо об'єкт, котрий містить властивість pageOffset, яку необхідно передати в контекст рендерингу шаблону. Властивість також стає реактивною, але лише в контексті рендерингу. Тож ми можемо використовувати її в шаблоні, як зазвичай:

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

Однак нам треба, щоб властивість змінювалась при кожній прокрутці. Для цього додамо слухача події прокрутки, коли компонент буде вмонтований, та видалимо слухача, коли компонент буде від'єднаний. Новий 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>
// ...

Зверніть увагу, що всі хуки життєвого циклу у версіях 2.x Vue мають еквіваленті функції onXXX, які можуть бути використані всередині setup().

Можливо, ви також помітили, що змінна pageOffset містить єдину реактивну властивість .value. Нам необхідно огорнути властивість, оскільки примітиви в JavaScript (на зразок чисел та рядків) не передаються за посиланням. Огортання значень дає нам можливість передавати змінювані та реактивні посилання для довільних типів значень.

Об'єкт pageOffset матиме такий вигляд:

Vue.js 3: майбутньо-орієнтоване програмування

Далі нам необхідно реалізувати завантаження даних користувача. Подібно до того, як все працює в наявній версії, з новим 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>
// ...

Обчислювані значення не відрізняються за поведінкою в нововведенні: вони відстежують стан своїх залежностей та перераховують своє значення, лише якщо були певні зміни. Першим аргументом в watch передається «джерело», яким може бути:

  • функція-гетер;
  • обгортка значення;
  • масив, що містить обидва пункти вище.

Колбек передається як другий аргумент: він викликається, лише коли значення, повернене гетером чи обгорткою, змінилось.

Щойно ми реалізували цільовий компонент, використовуючи функціональне API.🎉 Далі нам необхідно зробити отриману логіку повторно використовуваною.

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

Ось ми і дійшли до найцікавішого. Щоб повторно використовувати код, котрий стосується певної логіки, ми можемо виокремити його у так звану «композиційну функцію» та повернути реактивний стан:

// ...
<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 для повернення реактивних властивостей. Їх можна розмістити в окремому файлі та використовувати в будь-якому іншому компоненті. У порівнянні з попередніми реалізаціями:

  • властивості, що передаються шаблону, мають зрозумілі джерела, оскільки їх значення повертаються з композиційних функцій;
  • ці значення довільно названі, тому немає колізій простору імен;
  • немає непотрібних екземплярів компонента, створених лише для повторного використання логіки.

Насправді, є ще безліч переваг, з якими можна ознайомитись на офіційній сторінці RFC.

Усі приклади коду зі статі можна знайти за посиланням, а протестувати компонент самостійно можна тут.

Висновок

Функціональний API Vue пропонує чистий та гнучкий спосіб організації логіки всередині та між компонентами, враховуючи недоліки попередніх підходів. Лише уявіть, якими потужними можуть бути композиційні функції для проєктів будь-якого масштабу.🚀

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 639
Приєднався: 1 рік тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід