Повторное использование кода Vue.js

По мере усложнения приложения, в котором львиная доля кода использует Vue.js, возникает вопрос повторного использования кода. Когда возможности расширить тот или иной компонент с помощью параметров или слотов уже не удовлетворяют всех потребностей, можно прибегнуть в другим способам.

Популярный, но все всегда правильный, способ  использовать некоторую сущность добавив новые возможности — использовать наследование.

Mixin

У Vue.js для возможности наследования имеются mixin-ы. В mixin мы можем вынести данные, группу методов и даже hook-и жизненного цикла.

Например, создадим mixin-ы decrementMixin.js:

export default {
  data() {
    return {
      mixinData1: 1,
    }
  },
  created() {
    console.info(`Mixin's (1) hook "created"`);
  },
  methods: {
    decrement() {
      this.index--;
    }
  }
};

и incrementMixin.js

export default {
  data() {
    return {
      mixinData2: 2,
    }
  },
  created() {
    console.info(`Mixin's (2) hook "created"`);
  },
  methods: {
    increment() {
      this.index++;
    },
  }
};

Теперь можно создать компонент, который будет использовать эти mixin-ы

Mix.vue:

<template>
  <div class="component-border pl-1 mb-1 ms-1">
    <h1>
      {{ title }}
    </h1>
    <div>
      <p>mixinData1: {{ mixinData1 }}</p>
      <p>mixinData2: {{ mixinData2 }}</p>
      <p>Data: {{ index }}</p>
      <p>
        <button
          v-for="(action, index) in actions"
          v-bind:key="index"
          @click="action.handler"
          class="me-1">
          {{ action.text }}
        </button>
      </p>
    </div>
  </div>
</template>
import incrementMixin from "@/mixins/incrementMixin"
import decrementMixin from "@/mixins/decrementMixin";
export default {
  name: 'Mix',
  mixins: [ incrementMixin, decrementMixin ],
  data() {
    return {
      title: 'Mix',
      index: 0,
      actions: [
        { text: 'Increment', handler: this.increment },
        { text: 'Decrement', handler: this.decrement },
        { text: 'Clear', handler: this.clear },
      ]
    }
  },
  created() {
    console.info(`Mix component hook "created"`);
  },
  methods: {
    clear() {
      this.index = 0;
    },
  }
}

Данный компонент будет иметь методы из обоих миксинов. Обработчики created будут выполнены по очереди. Выглядеть на странице будет так:

В консоли при загрузке странице можно будет увидеть следующее:

# Mixin's (2) hook "created"
# Mixin's (1) hook "created"
# Mix component hook "created"

Можно видеть, что отрабатывают все hook-и жизненного цикла, что определены.

Шаблон для отображения используется, который описана в компоненте, так как в миксинах нет шаблонов. Таким образом с помощью мискинов как из частей можно создать компонент, который будет состоять из своих данных и методов и данных и методов полученных у миксинов.

Extends

Если же нужно создать потомка от существующего компонента, то для этого есть опция extends.

Допустим, у нас есть компонент Parent.vue:

<template>
  <div class="component-border pl-1 mb-1 ms-1">
    <h1>
      {{ title }}
    </h1>
    <div>
      <p>Param: {{ param }}</p>
      <p>Data: {{ index }}</p>
      <p>
        <button
          v-for="(action, index) in actions"
          v-bind:key="index"
          @click="action.handler"
          class="me-1">
          {{ action.text }}
        </button>
      </p>
    </div>
  </div>
</template>
export default {
  name: 'Parent',
  props: {
    param: { type: String, default: '' },
  },
  data() {
    return {
      title: 'Parent',
      index: 0,
      actions: [
        { text: 'Increment', handler: this.increment },
      ]
    }
  },
  created() {
    console.info(`Parent's hook "created" for component "${this.title}"`);
  },
  methods: {
    clear() {
      this.index = 0;
    },
    increment() {
      this.index++;
    },
  }
}

Унаследуем от него ChildExtend.vue:

import Parent from "./Parent"
export default {
  name: 'ChildExtend',
  extends: Parent,
  data() {
    return {
      title: 'ChildExtend',
      actions: [
        { text: 'Increment', handler: this.increment },
        { text: 'Decrement', handler: this.decrement },
        { text: 'Clear', handler: this.clear },
      ]
    }
  },
  created() {
    console.info(`Child's hook "created"`);
  },
  methods: {
    decrement() {
      this.index--;
    }
  }
}

Шаблона собственного у него нет. Он использует шаблон родителя. Ели определим в потомке шаблон, то он полностью заменит родительский.

Вместе родительский компонент и наследник выглядят на странице так:

По своей сути extends — тот же mixin, только применяется к компоненту и позволяет наследовать шаблон.

Higher-order components (HOC)

На этом способы повторного использования кода Vue.js не заканчиваются. В сети можно встретить описание подхода  Higher-order components (HOC). Он похож на паттерн «декоратор».

Принцип такой. У исходного компонента есть некое действие (метод). А компонент, который использует исходный вызывает исходное действие и добавляет свое поведение. 

Обычно в примерах HOC приводят логирование. Если вам довелось использовать такой подход в чем-то более полезном, пишите в комментариях. Я пока не вижу у данного подхода возможность широкого применения.

Итак, классический пример. Допустим у нас есть кнопка BaseButton.vue:

<template>
<button v-on:click="increment"><slot/> [<span>{{ index }}]</span></button>
</template>
<script>
export default {
  name: 'BaseButton',
  data() {
    return {
      index: 1,
    }
  },
  methods: {

    increment() {
      this.index++;
    }

  }
}
</script>

Используем ее так:

<base-button>Base button</base-button>

Можем создать  функцию, которая позволяет добавить поведение. Например, здесь добавлено логирование click-ов по кнопке.

export const withLoggerButton = button => {
  return {
    name: 'ExtendedButton',
    render(h) {
      const slots = Object.keys(this.$slots).map(key => this.$slots[key]);
      return h(
        button,
        {
          nativeOn: {
            click:() => {
              console.log('clicked')
            }
          }
        },
        slots
      );
    }
  }
}

Для лучшего понимания данного примера нужно быть знакомым с документацией по render функциям. Особенно подраздел «Подробно об объекте данных».

После того, как была написана  функция withLoggerButton мы можем где-либо создать компонент — ExtendedButton:

import BaseButton from "@/components/BaseButton";
import { withLoggerButton} from "@/withLoggerHOC";
const ExtendedButton = withLoggerButton(BaseButton);

И использовать его так:

<extended-button>Extended Button with logger</extended-button>

Расширенная кнопка будет работать точно также как и базовая, но отправлять при клике сообщение в консоль браузера.

К сожалению объект данных, передаваемый в функцию h (Сокращение createElement до h — распространённое соглашение в экосистеме Vue и обязательное для использования JSX. ) довольно ограниченный. В связи с этим в большой пользе от такого способа повторного использования кода я сильно сомневаюсь.

На этом статью завершу. Все примеры можно посмотреть и запустить склонировав код с github репозитория.

Но также я планирую продолжить изучение способов  повторного использования кода в Vue.js и скоро вернусь с новой статьей.

 

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *