Компоненти вищого порядку у Vue.js

14 хв. читання

Як описано в документації React, компонент вищого порядку (HOC) це функція, що приймає компонент в ролі аргументу й повертає наново створений компонент. Компонент, що повертається, як правило, доповнюється особливостями, наданими компонентом вищого порядку. Сам же компонент вищого порядку не є частиною програмного забезпечення, яку можна взяти й встановити. Це техніка, яка може бути корисною в написанні повторно використовуваного та підтримуваного коду.

Крок 1: Налаштування компонентів

Штучний застосунок, створений для цілей цієї статті, складається з двох компонентів: CommentsList та BlogPost. Вони обидва відображені всередині App, головного компоненту застосунку.

# App.vue

<template>
  <div id="app">
    <blog-post/>
    <comments-list/>
  </div>
</template>

<script>
import CommentsList from './components/CommentsList'
import BlogPost from './components/BlogPost'

export default {
  name: 'app',
  components: {
    'blog-post': BlogPost,
    'comments-list': CommentsList
  }
}
</script>

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

# components/CommentsList.vue

<template>
    <ul>
        <li
          v-for="(comment, index) in comments"
          :key="index"
        >{{comment}}</li>
    </ul>
</template>

<script>
    import DataSource from '../store/source.js'

    export default {
        name: 'comments-list',
        data() {
            return {
                comments: DataSource.getComments()
            }
        },
        methods: {
            handleChange() {
                this.comments = DataSource.getComments()
            }
        },
        mounted() {
            DataSource.addChangeListener(this.handleChange)
        },
        beforeDestroy() {
            DataSource.removeChangeListener(this.handleChange)
        }
    }
</script>

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

# components/BlogPost.vue

<template>
    <div>
        {{blogPost}}
    </div>
</template>

<script>
    import DataSource from '../store/source.js'

    export default {
        data() {
            return {
                blogPost: DataSource.getBlogPost()
            }
        },
        methods: {
            handleChange() {
                this.blogPost = DataSource.getBlogPost()
            }
        },
        mounted() {
            DataSource.addChangeListener(this.handleChange)
        },
        beforeDestroy() {
            DataSource.removeChangeListener(this.handleChange)
        }
    }
</script>

Компоненти BlogPost та CommentsList поділяють чотири функціональності:

  1. Беруть дані зі зовнішнього джерела даних (у цьому випадку з DataSource) всередині хуку mounted.
  2. Оновлюють data при кожному оновлені в зовнішньому джерелі даних.
  3. Додають прослуховувач змін до джерела даних.
  4. Прибирають прослуховувач змін із джерела даних.

Щоб уникнути повторень коду, загальна логіка між BlogPost та CommentsList може бути витягнута у компонент вищого порядку.

Крок 2: Компонент вищого порядку

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

Компонент вищого порядку це функція, що бере компонент в ролі аргументу й повертає новий компонент. Напишімо його на Vue:

# hocs/withSubscription.js

import Vue from 'vue'
import CommentsList from '~/components/CommentsList.vue'

const withSubscription = (component) => {
  return Vue.component('withSubscription', {
    render(createElement) {
      return createElement(component)
    } 
  }
}

const CommentsListWithSubscription = withSubscription(CommentsList)

На даному етапі компонент вищого порядку робить небагато. Він просто бере компонент й створює новий, який рендерить прийнятий компонент.

Наступний крок: реалізувати в ньому загальну логіку. Потрібно додати хуки mounted й beforeDestroy, та метод handleChange, який буде викликатися при кожному оновлені.

# hocs/withSubscription.js

import DataSource from '../store/source'
import Vue from 'vue'


const withSubscription = (component) => {
    return Vue.component('withSubscription', {
        render(createElement) {
            return createElement(component)
        },
        methods: {
            handleChange() {
            }
        },
        mounted() {
            DataSource.addChangeListener(this.handleChange)
        },
        beforeDestroy() {
            DataSource.removeChangeListener(this.handleChange)
        }
    })
}

export default withSubscription

Тепер новий компонент, що повертається за допомогою компонента вищого порядку, має необхідні хуки життєвого циклу. Метод handleChange залишається порожнім. Обидва компоненти містять метод handleChange, однак він має дещо різну реалізацію в кожному з них.

Компонент вищого порядку може приймати більше одного аргументу. Зараз withSubscription в ролі аргументу приймає тільки компонент. Щоб викликати кастомну логіку всередині handleChange, потрібен другий аргумент. Ним буде метод, що має викликатися при кожній зміні джерела даних. Метод, що передається, повертає оновлені дані, які повинні бути передані наново створеному компоненту в ролі вхідного параметра.

# hocs/withSubscription.js

import DataSource from '../store/source'
import Vue from 'vue'


const withSubscription = (component, selectData) => {

return Vue.component('withSubscription', {
        render(createElement, context) {
            return createElement(component, {
               props: {
                  content: this.fetchedData
               }
            })
        },
        data() {
            return {
                fetchedData: null
            }
        },
        methods: {
            handleChange() {
                this.fetchedData = selectData(DataSource)
            }
        },
        mounted() {
            DataSource.addChangeListener(this.handleChange)
        },
        beforeDestroy() {
            DataSource.removeChangeListener(this.handleChange)
        }
    })
}

export default withSubscription

Використання компоненту вищого порядку всередині App.vue виглядає наступним чином:

# App.vue

<template>
  <div id="app">
    <blog-post/>
    <comments-list/>
  </div>
</template>

<script>
import CommentsList from './components/CommentsList'
import BlogPost from './components/BlogPost'
import withSubscription from './hocs/withSubscription'

const BlogPostWithSubscription = withSubscription(BlogPost, (DataSource) => {
  return DataSource.getBlogPost()
})
const CommentsListWithSubscription = withSubscription(CommentsList, (DataSource) => DataSource.getComments())

export default {
  name: 'app',
  components: {
    'blog-post': BlogPostWithSubscription,
    'comments-list': CommentsListWithSubscription
  }
}
</script>

А тут код BlogPost та CommentsList:

# components/BlogPost.vue

<template>
    <div>
        {{content}}
    </div>
</template>

<script>
    export default {
        props: ['content']
    }
</script>

----

# components/CommentsList.vue

<template>
    <ul>
        <li v-for="(comment, index) in content" :key="index">{{comment}}</li>
    </ul>
</template>

<script>
    export default {
        name: 'comments-list',
        props: ['content']
    }
</script>

Все це виглядає дуже добре, але є одна відсутня частина. Що, якщо мені потрібно передати ID у BlogPost? Або, що, якщо я хочу згенерувати подію з BlogPost у компонент App? З поточною реалізацією це не спрацює.

Крок 3: Обробка вхідних параметрів та подій у компоненті вищого порядку

По-перше, трохи змінимо реалізацію методу getBlogPost у DataSource. Щоб дізнатися, який пост був отриманий та повернутий, в ролі другого аргументу потрібно взяти id посту. Оскільки фактичний виклик getBlogPost відбувається всередині компоненту BlogPost, має сенс передавати в ролі вхідного параметра бажаний id посту блогу й використовувати його при виклику методу getBlogPost. Для цього мені потрібно зробити дві речі: перенести вхідний параметр id з компонента App у компонент BlogPost та змінити функцію, яку я передаю у компонент вищого порядку, так, щоб вона приймала другий аргумент — вхідні параметри, які вона має передати далі у BlogPost.

# App.vue

<template>
  <div id="app">
    <blog-post :id="1"/>
  </div>
</template>

<script>

import BlogPost from './components/BlogPost'
import withSubscription from './hocs/withSubscription'

const BlogPostWithSubscription = withSubscription(BlogPost, (DataSource, props) => {
  return DataSource.getBlogPost(props.id)
})

export default {
  name: 'app',
  components: {
    'blog-post': BlogPostWithSubscription
  }
}
</script>

---

# components/BlogPost.vue

<template>
    <div>
        {{content}}
    </div>
</template>

<script>
    export default {
        props: ['content', 'id']
    }
</script>

Тепер мені треба оновити компонент вищого порядку, щоб він знав, як передавати вхідні параметри у компонент, який він відображає.

# hocs/withSubscription.js

import DataSource from '../store/source'
import Vue from 'vue'


const withSubscription = (component, selectData) => {
    const originalProps = component.props || [];

    return Vue.component('withSubscription', {
        render(createElement) {
            return createElement(component, {
                props: {
                    ...originalProps,
                    content: this.fetchedData
                }
            })
        },
        props: [...originalProps],
        data() {
            return {
                fetchedData: null
            }
        },
        methods: {
            handleChange() {
                this.fetchedData = selectData(DataSource, this.$props)
            }
        },
        mounted() {
            DataSource.addChangeListener(this.handleChange)
        },
        beforeDestroy() {
            DataSource.removeChangeListener(this.handleChange)
        }
    })
}

export default withSubscription

Перше, що додається у компонент вищого порядку — читання оригінальних вхідних параметрів з компоненту, який він відображає. Ці параметри зберігаються в константі originalProps. У Vue компонент має визначити, які вхідні параметри він приймає. withSubscription повинна приймати ті самі вхідні параметри, що і компонент, який вона відображає, щоб мати можливість пізніше передати їх в нього. Це робиться за допомогою наступного рядка коду:

return Vue.component('withSubscription', {
    ...
    props: [...originalProps], # <= цей рядок
    ...
}

Остання частина, що була оновлена, — виклик функції selectData всередині методу handleChange. Був доданий другий аргумент — вхідні параметри компоненту вищого порядку — this.$props. Властивість $props є властивістю екземпляру компоненту Vue, доступної з версії Vue 2.2.

Я охопив передачу вхідних параметрів у дочірній компонент. Останньою відсутньою частиною є генерація подій від дочірнього компонента до предка.

Додамо прослуховувач подій в компонент App.vue, та щось, що генеруватиме подію в BlogPost.vue.

# App.vue

<template>
  <div id="app">
    <blog-post :id="1" @click="onClick"/>
  </div>
</template>

---

# components/BlogPost.vue

<template>
    <div>
        <button @click="$emit('click', 'aloha')">CLICK ME!</button>
        {{data}}
    </div>
</template>

<script>
    export default {
        props: ['data', 'id']
    }
</script>

Слід пам'ятати, що BlogPost не виноситься всередину App, бо є посередник — компонент вищого порядку withSubscription.

Щоб передати прослуховувачі подій у компонент, який виноситься, мені потрібно додати один рядок коду в компонент вищого порядку.

# hocs/withSubscription.js

return Vue.component('withSubscription', {
    ...
    on: {...this.$listeners} # <= цей рядок,
})

Точно так само, як і у this.$props, тут є властивість екземпляра $listener, яка містить прослуховувачі подій v-on області видимості предка.

Повний застосунок зі статті можна знайти за посиланням: https://github.com/bognix/vue-hoc

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

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

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

Вхід