Объявление компонентов
Однофайловый компонент (сокращенно — 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>
Ссылки:
- 🇷🇺 Официальная документация — Однофайловые компоненты
- 🇷🇺 Официальная документация — Render-функции и JSX
- 🇺🇸 7 способов определения шаблона компонента в VueJS
Взаимодействие компонента
Входные параметры и события
В целом, компонент 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>
Ссылки:
- 🇷🇺 Официальная документация — Входные параметры
- 🇺🇸 Паттерны взаимодействия компонента Vue.js
- 🇺🇸 Создание пользовательских полей ввода с помощью Vue.js
- 🇺🇸 Взаимодействие дочерних компонентов Vue
- 🇺🇸 Управление состоянием во Vue.js
- 🇺🇸 Взаимодействие во Vue.js, часть 2: родительский и дочерний компоненты
Обработка событий компонента
Ссылки:
- 🇷🇺 Официальная документация — Пользовательские события
- 🇺🇸 Использование событий Vue для сокращения объявлений входных параметров
- 🇺🇸 Хуки компонента Vue.js как события
- 🇺🇸 Создание глобальной шины событий с помощью Vue.js
- 🇺🇸 Шина событий Vue.js + Промисы
Условный рендеринг компонента
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>
Ссылки
- 🇷🇺 Официальная документация — Динамические компоненты
- 🇷🇺 Официальная документация — Динамические и асинхронные компоненты
- 🇺🇸 Шаблоны динамических компонентов с Vue.js
Композиция
Библиотека
Простой пример композиции
<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>
Ссылки:
- 🇷🇺 Официальная документация — Содержимое слотов
- 🇺🇸 Понимание слотов компонентов с помощью Vue.js
- 🇺🇸 Составление пользовательских элементов с помощью слотов и именованными слотами
- 🇺🇸 Написание абстрактных компонентов во Vue.js (🇷🇺 перевод)
Именованные слоты
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>
Ссылки:
- 🇷🇺 Официальная документация — Слоты с ограниченной областью видимости
- 🇺🇸 Разбираемся со слотами с ограниченной областью видимости
- 🇺🇸 Понимание слотов с ограниченной областью видимости во Vue.js
- 🇺🇸 Слоты с ограниченной областью видимости компонента во Vue.js
- 🇺🇸 Трюк к пониманию слотов с ограниченной областью видимости во Vue.js
- 🇺🇸 Мощность слотов в Vue
- 🇺🇸 Создание компонента, управляемого с клавиатуры, списка с помощью Vue.js и слотов с ограниченной областью видимости
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;
Ссылки:
- 🇷🇺 Официальная документация — Render Functions & JSX
- 🇺🇸 Использование рендеринга входных параметров в Vue
- 🇺🇸 Использование рендеринга входных параметров Vue.js!
Передача входных параметров
Иногда вам может понадобиться передать входные параметры и обработчики дочернему компоненту, не объявляя всех входных параметров дочернего компонента. Вы можете привязать $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.js
- 🇺🇸 Нужны ли нам компоненты высшего порядка порядка во Vue.js?
- 🇺🇸 Компоненты высшего порядка во Vue.js
Внедрение зависимостей
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
.
Ссылки:
- 🇷🇺 Официальный API
- 🇷🇺 Официальное руководство
- 🇺🇸 Взаимодействие компонента
- 🇺🇸 Внедрение зависимостей в приложении Vue.js с TypeScript
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,
}
}