Введение в Vue.js: Components, Props и Slots

20.04.2017

Это вторая часть серий статей о JavaScript фреймворке Vue.js. В этой части мы перейдем к components, props и slots. Это не будет полным руководством, а скорее обзор основ, чтобы вы смогли начать работать и понимали что этот фреймворк может вам предложить.

Серия статей

  1. Введение в Vue.js: Рендеринг, директивы и события
  2. Введение в Vue.js: Components, Props и Slots (Вы здесь)
  3. Vue-cli
  4. Vuex
  5. Анимации

Компоненты и передача данных

Если вы знакомы с React или Angular2, то идея компонентов и передачи состояния не будут для вас новыми. Если же нет, то давайте узнаем некоторые из основных понятий.

Сайты большие и маленькие, как правило, состоят из различных частей, и разделение их на более мелкие кусочки дает нам понятную структуру, простое обновление и более читаемый код. Вместо того, чтобы копаться в разметке длинной, многогранной страницы, мы могли бы разделить её на компоненты, как здесь:

<header></header>
<aside>
  <sidebar-item v-for="item in items"></sidebar-item>
</aside>
<main>
  <blogpost v-for="post in posts"></blogpost>
</main>
<footer></footer>

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

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

app.$mount('#app');

var app = new Vue({
  el: 'hello',
  template: '<h1>Привет, Мир!</h1>'
});

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

Это самый простой пример какой я смогла сделать, тут всё должно быть очевидно. Помните, что :text в HTML это сокращение для биндинга Vue. Мы рассматривали это в предыдущей статье. Забиндить можно что угодно, но в данном случае, это сдерживает нас от того, чтобы поместить состояние в фигурные скобки, как здесь {{ message }}.

В коде ниже, Vue.component является компонентом, а new Vue экземпляром. Можно иметь более одного экземпляра в приложении. Но мы, как правило, будем иметь один экземпляр и несколько компонентов, так как экземпляр является основным приложением.

Vue.component('child', {
  props: ['text'],
  template: `<div>{{ text }}<div>`
});

new Vue({
  el: "#app",
  data() {
    return {
      message: 'Привет, %username%!'
    }
  }
});
<div id="app">
  <child :text="message"></child>
</div>

See the Pen oWxBmP by FurFurFur (@FurFurFur) on CodePen.29134

Теперь мы можем использовать этот компонент сколько угодно раз в нашем приложении:

<div id="app">
  <child :text="message"></child>
  <child :text="message"></child>
</div>

See the Pen OmNWGJ by FurFurFur (@FurFurFur) on CodePen.29134

Мы можем также добавить проверку нашим props, что похоже на PropTypes в React. Это хорошо, потому что он сам проверится и вернет ошибку, если вышло что то не то, что мы ожидали. Но только в режиме разработки:

Vue.component('child', {
  props: {
    text: {
      type: String,
      required: true
    }
  },
  template: `<div>{{ text }}<div>`
});

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

Vue.component('child', {
  props: {
    text: {
      type: Boolean,
      required: true
    }
  },
  template: `<div>{{ text }}<div>`
});

See the Pen xdVgoz by FurFurFur (@FurFurFur) on CodePen.29134

Объекты должны быть возвращены как factory function и вы сможете даже передать валидатор как пользовательскую функцию. Это реально здорово, потому что вы сможете проверить значение несмотря на расчеты, ввод и другую логику. Хорошо рассказано о том как можно использовать каждый тип в этом руководстве.

Не обязательно передавать данные в props для наследования, у вас есть возможность использовать состояние или статическое значение, как вам захочется:

Vue.component('child', {
  props: { 
    count: {
      type: Number,
      required: true
    }
  },
  template: `<div class="num">{{ count }}</div>`
})

new Vue({
  el: '#app',
  data() {
    return {
      count: 0    
    }
  },
  methods: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    }
  }
})
<div id="app">
  <h3>
    <button @click="increment">+</button>
    Настройте значение
    <button @click="decrement">-</button>
  </h3>
  <h2>Это значение приложения: <span class="num">{{ count }}</span></h2>
  <hr>
  <h4><child count="1"></child></h4> 
  <p>Это счетчик наследователь, который использует статическое значение как props</p>
  <hr>
  <h4><child :count="count"></child></h4>
  <p>Это такой же счетчик, но он использует состояние как props</p>
</div>

See the Pen EmKWjL by FurFurFur (@FurFurFur) on CodePen.29134

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

Не используя состояние:

<child count="1"></child>

vs

Используя состояние:

<child :count="count"></child>

До сих пор мы создавали контент в нашем наследуемом компоненте со строкой, и, конечно, если вы используете babel и можете обрабатывать ES6 во всех браузерах (что я настоятельно рекомендую), вы можете использовать шаблон литерала, чтобы избежать потенциально трудно читаемых конкатенаций:

Vue.component('individual-comment', {
  template: 
  `<li> {{ commentpost }} </li>`,
  props: ['commentpost']
});

new Vue({
  el: '#app',
  data: {
    newComment: '',
    comments: [
      'Looks great Julianne!',
      'I love the sea',
      'Where are you at?'
    ]
  },
  methods: {
    addComment: function () {
      this.comments.push(this.newComment)
      this.newComment = ''
    }
  }
});
<ul>
    <li
      is="individual-comment"
      v-for="comment in comments"
      v-bind:commentpost="comment"
    ></li>
  </ul>
  <input
    v-model="newComment"
    v-on:keyup.enter="addComment"
    placeholder="Add a comment"
  >

See the Pen ybOMaz by FurFurFur (@FurFurFur) on CodePen.29134


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

Со всеми этими вещами в голове, давайте создадим шаблон. Мы оборнем некоторые теги HTML в скрипт и используем идентификатор, чтобы ссылаться на него для создания компонента. Заметьте как всё стало яснее, при том, что у нас всё ещё много текста и элементов:

<!-- This is the Individual Comment Component -->
<script type="text/x-template" id="comment-template">
<li> 
  <img class="post-img" :src="commentpost.authorImg" /> 
  <small>{{ commentpost.author }}</small>
  <p class="post-comment">"{{ commentpost.text }}"</p>
</li>
</script>

See the Pen MmypOQ by FurFurFur (@FurFurFur) on CodePen.29134

Слоты

Стало намного лучше. Но что произойдет, когда у нас будут два компонента, с небольшими вариациями в содержании или стилях? Мы могли бы пропустить стили и содержание вниз в компонент с props, и переключать все каждый раз, или мы могли бы разветвить компоненты и создать их разные версии. Но было бы очень хорошо, если бы мы могли повторно использовать компоненты, и заполнить их теми же данными или функционалом. Это тот момент, когда слоты становятся очень кстати.

Скажем, у нас есть основной экземпляр приложения использующий один и тот же <app-child> компонент дважды. Внутри каждого наследователя мы хотим немного такого и немного другого контента. В контенте мы хотим оставаться последовательными, мы будем использовать стандартный тег р , и для контента, который мы захотим переключить, будем ставить пустой тег <slot></slot>.

Затем, в экземпляре приложения, мы можем передать контент внутрь <app-child> теги компонента, и он будет автоматически заполнять слоты:

<div id="app">
  <h2>Мы можем использовать слоты для наполнения контентом</h2>
  <app-child>
    <h3>Это слот номер один</h3>
  </app-child>
  <app-child>
    <h3>Это слот номер два</h3>
    <small>Я могу поместить сюда еще больше контента!</small>
  </app-child>
</div>

See the Pen vmGxMG by FurFurFur (@FurFurFur) on CodePen.29134

Кроме того, вы можете иметь содержимое по умолчанию в слотах. Просто напишите содержимое внутри тегов, вместо <slot></slot> :

<slot>Я стандартный текст</slot>

Стандартный текст будет использоваться, пока вы не заполните слот другим материалом. Конечно же более полезным!

Еще можно дать слотам имена. Если вам нужно иметь два слота в компоненте, то, для их различия, добавьте атрибуту имя <slot name="headerinfo"></slot> . Чтобы получить доступ к этому конкретному слоту пишите:

<h1 slot="headerinfo">Я заполню слот headerinfo!</h1>

Это нереально полезно! Если у вас есть несколько слотов с именами, но к одному вы обратились не так, Vue поставит именованное содержание в именованные слоты, а оставшимся контентом заполнит безымянные слоты.

Вот пример того, что я имею в виду:

Это дочерний шаблон

<div id="post">
  <main>
    <slot name="header"></slot>
    <slot></slot>
  </main>
</div>

Это пример родителя

<app-post>
  <h1 slot="header">Это заголовок</h1>
  <p>А я пойду в безымянный слот!</p>
</app-post>

Выводимое содержимое

<main>
  <h1>Это заголовок</h1>
  <p>А я пойду в безымянный слот!</p>
</main>

Лично я если использую более одного слота в одно и тоже время, то называю их как можно яснее. Приятно, что Vue обеспечивает такой гибкий API.

Примеры слотов

С другой стороны, мы можем иметь конкретные стили, назначенные для разных компонентов, и сохранять весь контент внутри, а в результате — быстро и легко изменять внешний вид чего либо. Ниже, в редакторе этикетки для вина, одна из кнопок будут переключать компонент и цвета в зависимости от того, что выбирает пользователь. Фон бутылки, фон этикетки и текст будут переключаться, сохраняя при этом контент стабильным.

const app = new Vue({
  ...
  components: {
    'appBlack': {
      template: '#black'
    }
  }
});

Основное Vue приложение HTML:

 <component :is="selected">
    ...
    <path class="label" d="M12,295.9s56.5,5,137.6,0V409S78.1,423.6,12,409Z" transform="translate(-12 -13.8)" :style="{ fill: labelColor }"/>
    ...
  </component>

<h4>Color</h4>
  <button @click="selected ='appBlack', labelColor = '#000000'">Black Label</button>
  <button @click="selected ='appWhite', labelColor = '#ffffff'">White Label</button>
  <input type="color" v-model="labelColor" defaultValue="#ff0000">

Чистый компонент HTML:

<script type="text/x-template" id="white">
  <div class="white">
     <slot></slot>
  </div>
</script>

(Это больше демо, так что лучше поиграться с ним в отдельном окне/вкладке)

See the Pen wdGdBP by FurFurFur (@FurFurFur) on CodePen.29134


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

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

<!-- Основной экземпляр приложения -->
<app-comment>
  <p slot="comment">{{ comment.text }}</p>
</app-comment>
<!-- Индивидуальный компонент -->
<script type="text/x-template" id="comment-template">
  <div>
    <slot name="comment"></slot>
  </div>
</script>

Мы легко можем переключаться между разными компонентами с теми же ссылочными слотами, но что если мы захотим переключаться туда-сюда, оставляя индивидуальным состояние каждого компонента? Сейчас, когда мы переключаемся между черным и белым, шаблоны переключаются и содержание остается неизменным. Но, допустим, мы хотим, чтобы черная этикета полностью отличалась от белой. Есть специальный компонент — , в который вы можете обернуть то что вам нужно, тогда он будет сохранять состояние при переключении.

Проверьте это в примере выше — создайте черную метку, а затем смените цвет на белый и попереключайтесь между ними. Вы увидите, что состояние каждого сохраняется, но они отличаются друг от друга:

<keep-alive>
  <component :is="selected">
    ...
  </component>
</keep-alive>

See the Pen mmPmLq by FurFurFur (@FurFurFur) on CodePen.29134

Люблю эту особенность API.

Это все хорошо, но для простоты мы лепили все в один или два файла. Всё было бы гораздо лучше организовано во время создания сайта. Мы могли бы разделить компоненты в разные файлы и импортировать их когда понадобится. На самом деле, так разработка в Vue обычно и происходит. Так что давайте изучать дальше. Настройтесь на следующую часть, там мы поговорим о Vue-cli, build процессах и Vuex для управления структурой!

Серия статей

  1. Введение в Vue.js: Рендеринг, директивы и события
  2. Введение в Vue.js: Components, Props и Slots (Вы здесь)
  3. Vue-cli
  4. Vuex
  5. Анимации
Оригинальная статья: https://css-tricks.com/intro-to-vue-2-components-props-slots/

2 комментария

  1. @zerefus пишет:

    Добрый день. Непонятен один момент в этом коде:

    Что такое «is»? Просто не понимаю зачем он нужен, на что указывает

    • @rinamina пишет:

      Здравствуйте. «is» это специальный атрибут, который используется для модификации компонента, чтобы он не вызвал ошибок после рендера в DOM.
      Чтобы вы правильно поняли работу этого атрибута, очень рекомендую почитать о работе компонентов в документации Vue.js:
      https://ru.vue-js.org/components-vue
      Конкретный пример использования атрибута «is» можете сразу посмотреть здесь:
      https://ru.vuejs.org/v2/guide/components.html#Особенности-парсинга-DOM-шаблона

Добавить комментарий

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

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: