Мы в команде 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 принимает три аргумента:
- Элемент который нужно создать. Это может быть не только тег HTML, но и имя компонента. Этот аргумент обязательный;
- Объект с данными. Может содержать список классов, стилей, входных параметров для компонента, методы для обработки событий и т.д. Более подробно — в документации. Опциональный аргумент;
- Дочерние виртуальный узлы. Это может быть как строка, так и массив. В примере выше это — 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’).
Автор: FeelGood