Объявление компонентов

Однофайловый компонент (сокращенно — SFC) — наиболее распространённый

<template>
  <p class="demo">
    <button class="btn-primary" @click.prevent="handleClick">
      <slot></slot>(clicked - {{count}})
    </button>
  </p>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    handleClick() {
      this.count++;
      console.log('clicked', this.count);
    },
  },
};
</script>

<style scoped>
.btn-primary {
  display: inline-block;
  font-size: 1.2rem;
  color: #fff;
  background-color: #3eaf7c;
  padding: 0.8rem 1.6rem;
  border-radius: 4px;
  transition: background-color 0.1s ease;
  box-sizing: border-box;
  border-bottom: 1px solid #389d70;
}
</style>

Шаблонные строки (или литералы шаблонов в ES6)

Vue.component('my-btn', {
  template: `
    <button class="btn-primary" @click.prevent="handleClick">
      <slot></slot>(clicked - {{count}})
    </button>
  `,
  data() {
    return {
      text: 'Click me',
    };
  },
  methods: {
    handleClick() {
      this.count++;
      console.log('clicked', this.count);
    }
  }
});

Render-функция

Vue.component('my-btn', {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    handleClick() {
      this.count++;
      console.log('clicked', this.count);
    },
  },
  render(h) {
    return h(
      'button',
      {
        attrs: {
          class: 'btn-primary',
        },
        on: {
          click: this.handleClick,
        },
      },
      this.$slots.default
    );
  },
});

JSX

Vue.component('my-btn', {
  data() {
    return {
      text: 'Click me',
    };
  },
  methods: {
    handleClick() {
      console.log('clicked');
    },
  },
  render() {
    return (
      <button class="btn-primary" @click.prevent="handleClick">
        {this.$slots.default}(clicked - {{count}})
      </button>
    );
  },
});

vue-class-component

<template>
  <button class="btn-primary" @click.prevent="handleClick">
    <slot></slot>(clicked - {{count}})
  </button>
</template>

<script>
import Vue from 'vue';
import Component from 'vue-class-component';

@Component
export default MyBtn extends Vue {
  count = 0;

  handleClick() {
    this.count++;
    console.log('clicked', this.count);
  }
}
</script>

<style scoped>
.btn-primary {
  background-color: blue;
}
</style>

Ссылки:

Взаимодействие компонента

Входные параметры и события

В целом, компонент Vue следует однонаправленному потоку данных, то есть входные параметры передаются вниз (см. официальное руководство), а события — наверх. Входные параметры — это данные только для чтения, поэтому невозможно изменить входные параметры дочерних компонентов. При изменении входных параметров, дочерние компоненты будут автоматически повторно отрендерены (входные параметры являются реактивными источниками данных). Дочерние компоненты могут генерировать событие только к непосредственному родительскому компоненту, так что он может изменять data, сопоставляемые с props дочернего компонента.

<template>
  <button @click="$emit('click')">{{text}}</button>
</template>

<script>
export default {
  name: 'v-btn',
  props: {
    text: String,
  },
};
</script>
<template>
  <v-btn :text="buttonText" @click="handleClick"></v-btn>
</template>

<script>
export default {
  data() {
    return {
      clickCount: 0,
      buttonText: 'initial button text',
    };
  },
  methods: {
    handleClick() {
      this.buttonText = `Button clicked ${++this.clickCount}`;
      console.log('clicked', this.buttonText);
    },
  },
};
</script>

Ссылки:

Обработка событий компонента

Ссылки:

Условный рендеринг компонента

Директивы (v-if / v-else / v-else-if / v-show)

v-if

<h1 v-if="true">Рендеринг только, если условие v-if равняется true</h1>

Использование v-if и v-else

<h1 v-if="true">Рендеринг только, если условие v-if равняется true</h1>
<h1 v-else>Рендеринг только, если условие v-if равняется false</h1>

Использование v-else-if

<div v-if="type === 'A'">Рендеринг только, если `type` равняется `A`</div>
<div v-else-if="type === 'B'">Рендеринг только, если `type` равняется `B`</div>
<div v-else-if="type === 'C'">Рендеринг только, если `type` равняется `C`</div>
<div v-else>Рендеринг если `type` не равен ни `A`, ни `B`, ни `C`</div>

Использование v-show

<h1 v-show="true">Всегда рендерится, но виден только в том случае, если условия `v-show` равняются true</h1>

Если вы хотите по условию отобразить более одного элемента, вы можете использовать директивы (v-if / v-else / v-else-if /v-show) на элементе <template>. Обратите внимание, что элемент <template> фактические не будет отображаться в DOM. Это как невидимая обёртка.

<template v-if="true">
  <h1>Все элементы</h1>
  <p>будут отрендерены в DOM,</p>
  <p>за исключением элемента `template`</p>
</template>

Render-функция или JSX

Если вы используете JSX в своем Vue-приложении, то можете применять все техники, например использования выражения if else и switch case, а также тернарные и логические операторы.

Использование выражения if else

export default {
  data() {
    return {
      isTruthy: true,
    };
  },
  render(h) {
    if (this.isTruthy) {
      return <h1>Рендеринг, если значение равно true</h1>;
    } else {
      return <h1>Рендеринг, если значение равно false</h1>;
    }
  },
};

Использование выражения switch case

import Info from './Info';
import Warning from './Warning';
import Error from './Error';
import Success from './Success';

export default {
  data() {
    return {
      type: 'error',
    };
  },
  render(h) {
    switch (this.type) {
      case 'info':
        return <Info text={text} />;
      case 'warning':
        return <Warning text={text} />;
      case 'error':
        return <Error text={text} />;
      default:
        return <Success text={text} />;
    }
  },
};

Или можно использовать сопоставление с помощью объекта для упрощения выражений switch case

import Info from './Info';
import Warning from './Warning';
import Error from './Error';
import Success from './Success';

const COMPONENT_MAP = {
  info: Info,
  warning: Warning,
  error: Error,
  success: Success,
};

export default {
  data() {
    return {
      type: 'error',
    };
  },
  render(h) {
    const Comp = COMPONENT_MAP[this.type || 'success'];

    return <Comp />;
  },
};

Использование тернарного оператора

export default {
  data() {
    return {
      isTruthy: true,
    };
  },
  render(h) {
    return (
      <div>
        {this.isTruthy ? (
          <h1>Рендеринг, если значение равно true</h1>
        ) : (
          <h1>Рендеринг, если значение равно false</h1>
        )}
      </div>
    );
  },
};

Использование логического оператора

export default {
  data() {
    return {
      isLoading: true,
    };
  },
  render(h) {
    return <div>{this.isLoading && <h1>Загрузка ...</h1>}</div>;
  },
};

Ссылки

Динамический компонент

<component> с атрибутом is

<component :is="currentTabComponent"></component>

В приведённом выше примере отрендеренный компонент будет уничтожаться, если другой компонент должен будет рендериться в <component>. Если необходимо, чтобы компоненты сохраняли свои экземпляры без их уничтожения в теге <component>, можно обернуть <component> в тег <keep-alive>:

<keep-alive>
  <component :is="currentTabComponent"></component>
</keep-alive>

Ссылки

Композиция

Библиотека

Простой пример композиции

<template>
  <div class="component-b">
    <component-a></component-a>
  </div>
</template>

<script>
import ComponentA from './ComponentA';

export default {
  components: {
    ComponentA,
  },
};
</script>

Ссылки

Расширение компонента

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

<template>
  <button class="button-primary" @click.prevent="handleClick">
    {{buttonText}}
  </button>
</template>

<script>
import BaseButton from './BaseButton';

export default {
  extends: BaseButton,
  props: ['buttonText'],
};
</script>

Ссылки:

Примеси

// closableMixin.js
export default {
  props: {
    isOpen: {
      default: true
    }
  },
  data: function() {
    return {
      shown: this.isOpen
    }
  },
  methods: {
    hide: function() {
      this.shown = false;
    },
    show: function() {
      this.shown = true;
    },
    toggle: function() {
      this.shown = !this.shown;
    }
  }
}
<template>
  <div v-if="shown" class="alert alert-success" :class="'alert-' + type" role="alert">
    {{ text }}
    <i class="pull-right glyphicon glyphicon-remove" @click="hide"></i>
  </div>
</template>

<script>
import closableMixin from './mixins/closableMixin';

export default {
  mixins: [closableMixin],
  props: ['text']
};
</script>

Ссылки:

Слоты (по умолчанию)

<template>
  <button class="btn btn-primary">
    <slot></slot>
  </button>
</template>

<script>
export default {
  name: 'VBtn',
};
</script>
<template>
  <v-btn>
    <span class="fa fa-user"></span>
    Логин
  </v-btn>
</template>

<script>
import VBtn from './VBtn';

export default {
  components: {
    VBtn,
  }
};
</script>

Ссылки:

Именованные слоты

BaseLayout.vue

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

App.vue

<base-layout>
  <template slot="header">
    <h1>Здесь может быть заголовок страницы</h1>
  </template>

  <p>Абзац для основного контента.</p>
  <p>И еще один.</p>

  <template slot="footer">
    <p>Здесь некоторые контактные данные</p>
  </template>
</base-layout>

Ссылки

Слоты с ограниченной областью видимости

<template>
  <ul>
    <li
      v-for="todo in todos"
      v-bind:key="todo.id"
    >
      <!-- У нас есть слот для каждого todo, передавая его -->
      <!-- в объект `todo` в виде входного параметра для слота. -->
      <slot v-bind:todo="todo">
        {{ todo.text }}
      </slot>
    </li>
  </ul>
</template>

<script>
export default {
  name: 'TodoList',
  props: {
    todos: {
      type: Array,
      default: () => ([]),
    }
  },
};
</script>
<template>
  <todo-list v-bind:todos="todos">
      <template slot-scope="{ todo }">
        <span v-if="todo.isComplete"></span>
        {{ todo.text }}
      </template>
  </todo-list>
</template>

<script>
import TodoList from './TodoList';

export default {
  components: {
    TodoList,
  },
  data() {
    return {
      todos: [
        { todo: 'todo 1', isComplete: true },
        { todo: 'todo 2', isComplete: false },
        { todo: 'todo 3', isComplete: false },
        { todo: 'todo 4', isComplete: true },
      ];
    };
  },
};
</script>

Ссылки:

Render Props

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

С однофайловым компонентом SFC

<template>
  <div id="app">
    <Mouse :render="__render"/>
  </div>
</template>

<script>
import Mouse from './Mouse.js';
export default {
  name: 'app',
  components: {
    Mouse,
  },
  methods: {
    __render({ x, y }) {
      return (
        <h1>
          The mouse position is ({x}, {y})
        </h1>
      );
    },
  },
};
</script>
<style>
- {
  margin: 0;
  height: 100%;
  width: 100%;
}
</style>

С использованием JSX

const Mouse = {
  name: 'Mouse',
  props: {
    render: {
      type: Function,
      required: true,
    },
  },
  data() {
    return {
      x: 0,
      y: 0,
    };
  },
  methods: {
    handleMouseMove(event) {
      this.x = event.clientX;
      this.y = event.clientY;
    },
  },
  render(h) {
    return (
      <div style={{ height: '100%' }} onMousemove={this.handleMouseMove}>
        {this.$props.render(this)}
      </div>
    );
  },
};

export default Mouse;

Ссылки:

Передача входных параметров

Иногда вам может понадобиться передать входные параметры и обработчики дочернему компоненту, не объявляя всех входных параметров дочернего компонента. Вы можете привязать $attrs и $listeners в дочернем компоненте и установить inheritAttrs на false (в противном случае div и child-component получат атрибуты).

PassingProps.vue

<template>
  <div>
    <h1>{{title}}</h1>
    <passing-props-child v-bind="$attrs" v-on="$listeners"></passing-props-child>
  </div>
</template>

<script>
import PassingPropsChild from './PassingPropsChild';

export default {
  components: {
    PassingPropsChild,
  },
  inheritAttrs: false,
  props: {
    title: {
      type: String,
      default: 'Hello, Vue!',
    },
  },
};
</script>

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

PassedProps.vue

<template>
  <p class="demo">
    <passing-props
      title="This is from <passing-props />"
      childPropA="This is from <passing-props-child />"
      @click="handleClickPassingPropsChildComponent"
    >
    </passing-props>
  </p>
</template>

<script>
import PassingProps from './PassingProps';

export default {
  components: {
    PassingProps,
  },
  methods: {
    handleClickPassingPropsChildComponent() {
      console.log('This event comes from `<passing-props-child />`');
      alert('This event comes from `<passing-props-child />`');
    },
  },
};
</script>

Рабочий пример:

This is from <passing-props />

Ссылки:

Компоненты высшего порядка (они же HOC)

Ссылки:

Внедрение зависимостей

Vue поддерживает механизм предоставления и внедрения объекта во всех потомки, независимо от глубины иерархии компонентов, при условии, что компоненты находятся в одной и той же цепочке родителей. Обратите внимание, что привязки provide и inject *не являются- реактивными, пока вы не передадите наблюдаемый объект.

<parent-component>
  <child-component>
    <grand-child-component></grand-child-component>
  </child-component>
</parent-component>

С приведенной выше иерархией компонентов в качестве примера для получения данных из parent-component вам нужно передавать данные (объект) в качестве props компоненту child-component и компоненту grand-child-component. Однако, если parent-component предоставляет (provide) данные (объект), grand-child-component может просто определить свойство inject для получения объекта, предоставляемого parent-component.

Ссылки:

Provide / Inject

TIP

Вы также можете использовать @Provide, @Inject из vue-property-decorator

ThemeProvider.vue

<script>
export default {
  provide: {
    theme: {
      primaryColor: '#3eaf7c',
      secondaryColor: '#1FA2FF'
    },
  },
  render(h) {
    return this.$slots.default[0];
  },
};
</script>

ThemeButton.vue

<template>
  <button class="btn" :style="{ color: '#fff', backgroundColor: (primary && theme.primaryColor) || (secondary && theme.secondaryColor)  }">
    <slot></slot>
  </button>
</template>

<script>
export default {
  inject: {
    theme: {
      default: {},
    },
  },
  props: {
    primary: {
      type: Boolean,
      default: false,
    },
    secondary: {
      type: Boolean,
      default: false,
    },
  },
};
</script>
<theme-provider>
  <theme-button secondary>Themed Button</theme-button>
</theme-provider>

Working Example:

Обработка ошибок

Хук errorCaptured

ErrorBoundary.vue

<script>
export default {
  name: 'ErrorBoundary',
  data() {
    return {
      error: false,
      errorMessage: '',
    };
  },
  errorCaptured(err, vm, info) {
    this.error = true;
    this.errorMessage = `Sorry, error occured in ${info}`;

    return false;
  },
  render(h) {
    if (this.error) {
      return h('p', { class: 'demo bg-danger' }, this.errorMessage);
    }

    return this.$slots.default[0];
  },
};
</script>

ThrowError.vue

<template>
  <p class="demo">
    <button class="btn btn-danger" @click.prevent="throwError()">Error Thrown Button ({{count}})</button>
  </p>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  watch: {
    count() {
      throw new Error('error');
    },
  },
  methods: {
    throwError() {
      this.count++;
    },
  },
};
</script>
<error-boundary>
  <throw-error></throw-error>
</error-boundary>

Рабочий пример:

References

Советы по продуктивности

Наблюдение при создании

// don't
created() {
  this.fetchUserList();
},
watch: {
  searchText: 'fetchUserList',
}
// do
watch: {
  searchText: {
    handler: 'fetchUserList',
    immediate: true,
  }
}