Мы в команде Ptah решили пойти чуть дальше привычных SPA и попробовали использовать Vue для конструктора лендингов. И теперь хотим поделиться частью нашего опыта.

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

Render-функции


Шаблоны компонентов — одна из тех вещей, за которые разработчики любят Vue. Они просты и логичны, благодаря им фреймворк имеет низкий порог входа. Синтаксиса шаблонов хватает в 90% случаев, чтобы написать логичный и красивый код. Но, что делать если вы попали в оставшиеся 10%, и написать компактный компонент не получается? Render-функция вам поможет.

Давайте разберемся, что это такое на примере из документации:

Vue.component('anchored-heading', {
 render: function (createElement) {
   return createElement(
     'h' + this.level,   // имя тега
     this.$slots.default // массив дочерних элементов
   )
 },
 props: {
   level: {
     type: Number,
     required: true
   }
 }
})

Компонент anchored-heading принимает свойство level и отрисовывает тег заголовка. Таким образом, запись

<anchored-heading :level="1">Привет, мир!</anchored-heading>

Будет преобразована в

<h1>Привет, мир!</h1>

Если бы этот компонент был описан с помощью стандартного шаблона, то содержал бы в себе до 6 условий v-if, описывающих разные уровни заголовков:

<h1 v-if="level === 1">
 <slot></slot>
</h1>
<h2 v-if="level === 2">
 <slot></slot>
</h2>
<h3 v-if="level === 3">
 <slot></slot>
</h3>
<h4 v-if="level === 4">
 <slot></slot>
</h4>
<h5 v-if="level === 5">
 <slot></slot>
</h5>
<h6 v-if="level === 6">
 <slot></slot>
</h6>

Как это работает?


Метод render принимает два аргумента. Первый аргумент createElement — функция описывающая то, какой элемент Vue должен создать. В сообществе принято сокращать createElement до одной буквы — h. Второй аргумент — context, для доступа к контекстным данным.

createElement принимает три аргумента:

  1. Элемент который нужно создать. Это может быть не только тег HTML, но и имя компонента. Этот аргумент обязательный;
  2. Объект с данными. Может содержать список классов, стилей, входных параметров для компонента, методы для обработки событий и т.д. Более подробно — в документации. Опциональный аргумент;
  3. Дочерние виртуальный узлы. Это может быть как строка, так и массив. В примере выше это — this.$slots.default.

Render-функции могут помочь в самых неожиданных ситуациях. Например в Ptah, нам часто требуется использовать тег style внутри страницы для правильной работы некоторых элементов конструктора. Однако Vue запрещает использование этого тега внутри template компонента. Это ограничение с легкостью обходится благодаря небольшой обертке:

Vue.component('v-style', {
 render: function (h) {
   return h('style', this.$slots.default)
 }
})

Теперь внутри шаблонов вместо тега style можно использовать v-style.

Итак, как только вам начинает казаться, что стандартных возможностей шаблонов Vue недостаточно — вспомните о Render-функциях. Они выглядят сложными лишь на первый взгляд, но в них можно использовать все возможности которые дает JS.

Mixins


У вас появилось несколько похожих компонентов с повторяющимся кодом? Mixins или примеси, помогут соблюсти принцип DRY — функционал из миксинов может быть использован в нескольких компонентах сразу.

Разберем на примере. Допустим у нас есть 2 компонента с похожей логикой:

export default  {
  name: 'TextElement',

  data () {
    return {
      elementName: 'Text',
      showEditor: false,
      editor: null
    }
  },
  
  methods: {
    initEditor () {
      this.showEditor = true
      this.editor = new Editor(this.elementName)
    }
  }
}

export default  {
  name: 'ButtonElement',

  data () {
    return {
      elementName: 'Button',
      showEditor: false,
      editor: null
    }
  },
  
  methods: {
    initEditor () {
      this.showEditor = true
      this.editor = new Editor(this.elementName)
    }
  }
}

Компоненты разные, но имеют одинаковую логику. Чтобы вынести ее потребуется создать обычный js файл. Логичным будет разместить его в директории mixins рядом с компонентами.

// mixin.js
export default  {
  data () {
    return {
      showEditor: false,
      editor: null
    }
  },
  
  methods: {
    initEditor () {
      this.showEditor = true
      this.editor = new Editor(this.elementName)
    }
  }
}

// TextElement.vue
import mixin from './mixins/mixin'

export default  {
  name: 'TextElement',

  mixins: [mixin]  // используем примесь

  data () {
    return {
      elementName: 'Text',
    }
  },
}

// ButtonElement.vue
import mixin from './mixins/mixin'

export default  {
  name: 'ButtonElement',

  mixins: [mixin]

  data () {
    return {
      elementName: 'Button'
    }
  }
}

Как видно из примера практически вся логика перекочевала в миксин. При использовании примеси внутри компонентов все их опции сливаются. И в компоненте можно свободно вызвать метод initEditor(), и, наоборот, в примеси здесь используется elementName из компонента. При этом объекты data будут слиты рекурсивно, и свойства из компонента будут иметь приоритет.

Итак, польза примесей очевидна — это повторное использование кода. Но есть и минус. Этот пример синтетический, всего в пару строчек. Реальные компоненты, например те, что используются в Ptah могут быть расписаны на пару сотен строк кода. Человеку, который не писал этот код, не всегда будет ясно как он работает, особенно если он упустит из виду добавление mixins в компонент. К сожалению, полностью избавиться от этого минуса не получится. Порекомендовать я могу две вещи: описывать работу компонента в JSDoc и использовать для свойств из примеси особые имена (например, можно добавлять префикс, о котором вы заранее договоритесь с командой).

Provide / Inject


Эта пара опций всегда используется вместе и позволяет передать данные от компонента родителя во всю иерархию его потомков. Эти опции прежде всего используют для написания плагинов, официальная документация не рекомендует использовать их в приложениях. В приложениях общение между компонентами отлично строится на Vuex. Однако этот функционал все-же заслуживает внимания.

Как это работает?


Для начала нам нужно определить данные в компоненте родителе, которые мы будем передавать его потомкам.

// Parent.vue определяем данные которые хотим передать вниз по иерархии
export default {
  provide: {
   device: 'is-desktop'
  }
}

Теперь переданные данные нужно внедрить в дочерний компонент.

// Child.vue внедряем данные переданные родителем
export default {
  inject: ['device'],

  created () {
   console.log(this.device) // => "is-desktop"
  }
}

Как видно из примера всё довольно просто. Но следует отметить один существенный минус — данные из связки provide/inject по умолчанию не реактивны! Однако этот недостаток легко обойти используя Object.defineProperty:

provide () {
 let device = {}
 Object.defineProperty(device, 'type', { enumerable: true, get: () => this.device })
 return { device }
},

data () {
 return {
   device: 'is-desktop'
 }
}

Теперь изменение this.device в родителе изменит его и в потомках.

Мета-компонент Component


Бывают ситуации, когда заранее неизвестно какой компонент будет использован в коде. Рассмотрим пример из нашего редактора. Задача следующая: в условной секции FirstScreen показать элементы Text, Logo, Button, затем к этим элементам добавить SocialIcons.

Итак, очевидно, что у нас будет компонент секции который будет служить контейнером для элементов и 4 компонента для самих элементов. Структура будет примерно следующая:

/ sections
 -- FirstScreen.vue
/ elements
 -- Text.vue
 -- Logo.vue
 -- Button.vue
 -- SocialIcons.vue

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

<component :is="%componentName%"/>

Элемент component с атрибутом :is в который просто записывается имя компонента. И наша задача благодаря нему решается просто элементарно:

<script>
export default  {
  name: 'FirstScreen',

  data () {
    return {
      elements: [
        'Text',
        'Logo',
        'Button',
      ],
    }
  }
}
</script>

<template>
  <div class="first-screen">
    <component v-for="element in elements" :is="element"/>
  </div>
</template>

В массив elements мы записали имена компонентов и затем просто выводим эти компоненты в цикле внутри шаблона FirstScreen. Теперь для того чтобы добавить в нашу секцию элемент с иконками соцсетей, нам нужно всего лишь выполнить this.elements.push(‘SocialIcons’).