Основы Vue 3: SFC, шаблоны, директивы, компоненты
Vue 3 — это прогрессивный JavaScript-фреймворк для построения пользовательских интерфейсов. Он сочетает декларативный подход к описанию UI, реактивную систему на основе Proxy и удобный компонентный подход через Single File Components (SFC). В отличие от React, где UI описывается в JSX как JavaScript-код, Vue использует HTML-подобные шаблоны с директивами, что многим разработчикам кажется более близким к классической вёрстке.
Эта статья — фундамент раздела docs/vue/. Мы разберём SFC-файлы, основные директивы, компоненты, передачу данных через props, события через emits, двустороннее связывание v-model, условный рендеринг и работу со списками. Основной стиль кода — Composition API с <script setup>, как того требует современная разработка на Vue 3. Options API будет показан как legacy-вариант, который всё ещё полезно уметь читать.
Содержание
- Что такое Vue и зачем он нужен
- Создание проекта через Vite
- Single File Component
- Шаблоны и синтаксис
- Реактивное состояние: ref и reactive
- Директивы
- Компоненты, props и emits
- Условный рендеринг
- Рендеринг списков
- Вычисляемые свойства и watchers
- Подъём состояния и передача данных вверх
- Options API как legacy
- Сравнение с React
- Лучшие практики и антипаттерны
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Что такое Vue и зачем он нужен
Прогрессивный фреймворк
Vue называют прогрессивным фреймворком, потому что его можно внедрять постепенно. Можно подключить Vue через CDN для оживления небольшого фрагмента страницы, а можно построить на нём полноценное SPA с роутингом, состоянием и SSR. Это отличает Vue от React, который позиционируется как библиотека для UI и обычно требует сборщика и экосистемы, и от Angular, который является монолитным фреймворком со своим мнением обо всём.
<!-- Подключение Vue через CDN -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<div id="app">{{ message }}</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const message = ref('Hello from Vue CDN!');
return { message };
}
}).mount('#app');
</script>
Декларативный UI через шаблоны
Как и React, Vue использует декларативный подход: вы описываете, как UI должен выглядеть при каждом состоянии, а фреймворк сам обновляет DOM. Но если React делает это через JSX-функции, Vue использует HTML-шаблоны с директивами:
<template>
<button @click="increment">Count: {{ count }}</button>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => count.value++;
</script>
Здесь {{ count }} — интерполяция, а @click — сокращение для директивы v-on:click. Vue компилирует этот шаблон в оптимизированные render-функции, которые отслеживают зависимости и обновляют DOM точечно.
Ключевые идеи Vue
1. Реактивность из коробки
Vue предоставляет ref, reactive, computed, watch — весь стек реактивности встроен во фреймворк. В React ту же задачу решают useState, useEffect, useMemo, useCallback, которые являются примитивами библиотеки.
2. SFC — единый файл компонента
Логика, шаблон и стили компонента живут в одном .vue-файле. Это упрощает навигацию по проекту и делает компонент самодостаточным.
3. Директивы для типовых задач
Условный рендеринг, циклы, связывание атрибутов и событий решаются через директивы v-if, v-for, v-bind, v-on, v-model. Это меньше boilerplate, чем в React, где те же задачи пишутся через JavaScript-выражения в JSX.
Создание проекта через Vite
Рекомендуемый способ
Для новых Vue 3 проектов рекомендуется официальный scaffolding-инструмент, основанный на Vite:
# Создать проект
npm create vue@latest my-vue-app
# Перейти в директорию
cd my-vue-app
# Установить зависимости
npm install
# Запустить dev-сервер
npm run dev
Интерактивный скрипт предложит выбрать TypeScript, JSX, Vue Router, Pinia, Vitest, ESLint, Prettier и другие опции.
Альтернатива: Vite напрямую
npm create vite@latest my-vue-app -- --template vue-ts
cd my-vue-app
npm install
npm run dev
Структура проекта
my-vue-app/
├── public/ # Статические файлы
├── src/
│ ├── assets/ # Изображения, шрифты, стили
│ ├── components/ # Переиспользуемые компоненты
│ ├── App.vue # Корневой компонент
│ └── main.ts # Точка входа
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts
Точка входа
// src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
Функция createApp создаёт экземпляр приложения. Ему можно подключать плагины, глобальные компоненты, директивы и middleware для ошибок.
Single File Component
Из чего состоит .vue-файл
SFC-файл состоит из трёх необязательных блоков:
<script setup>
// Логика компонента
import { ref } from 'vue';
const count = ref(0);
</script>
<template>
<!-- Разметка -->
<button @click="count++">Count: {{ count }}</button>
</template>
<style scoped>
/* Стили, ограниченные этим компонентом */
button {
color: #42b883;
}
</style>
<script setup> — синтаксический сахар для Composition API. Все top-level переменные и функции автоматически становятся доступны в шаблоне. Не нужно возвращать объект из setup().
<template> — HTML-подобный шаблон с директивами и интерполяцией.
<style scoped> — стили применяются только к элементам текущего компонента. Vue добавляет уникальный data-атрибут к элементам и селекторам.
Сравнение с React
В React логика и разметка смешаны в одном JSX-файле:
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}
В Vue они разделены на <script setup>, <template> и <style> в одном файле. Это не «разделение технологий», а «разделение ответственности»: каждый блок отвечает за свою задачу.
Порядок блоков
Официальный стиль: <script setup>, затем <template>, затем <style>. Некоторые команды предпочитают <template> первым. Главное — единообразие в проекте.
Шаблоны и синтаксис
Интерполяция текста
Для вывода текста используются двойные фигурные скобки:
<template>
<p>Hello, {{ user.name }}!</p>
<p>Next year you will be {{ user.age + 1 }}.</p>
</template>
<script setup>
const user = { name: 'Alice', age: 25 };
</script>
Внутри {{ }} можно использовать любые JavaScript-выражения, но не инструкции (if, for).
Привязка атрибутов: v-bind
Для динамических атрибутов используется v-bind, сокращённо ::
<template>
<img :src="imageUrl" :alt="description">
<a :href="link">Read more</a>
<button :disabled="isLoading">Submit</button>
</template>
<script setup>
const imageUrl = '/logo.png';
const description = 'Company logo';
const link = 'https://vuejs.org';
const isLoading = true;
</script>
Булевы атрибуты (disabled, hidden) при значении false удаляются из DOM.
Привязка классов и стилей
Vue предоставляет удобный синтаксис для классов и стилей:
<template>
<div :class="{ active: isActive, 'text-danger': hasError }">
Dynamic classes
</div>
<div :style="{ color: textColor, fontSize: size + 'px' }">
Dynamic styles
</div>
</template>
<script setup>
const isActive = true;
const hasError = false;
const textColor = 'red';
const size = 16;
</script>
Можно передавать массив, объект или строку. Это удобнее, чем писать className с clsx в React.
Обработка событий: v-on
Директива v-on сокращается до @:
<template>
<button @click="handleClick">Click me</button>
<input @input="handleInput" @focus="handleFocus">
</template>
<script setup>
const handleClick = () => console.log('clicked');
const handleInput = (event) => console.log(event.target.value);
const handleFocus = () => console.log('focused');
</script>
Модификаторы событий
Vue предлагает модификаторы для частых случаев:
<template>
<form @submit.prevent="submitForm">
<input @keydown.enter="submit">
<a @click.stop="doSomething">Click</a>
</form>
</template>
.prevent— вызываетevent.preventDefault().stop— вызываетevent.stopPropagation().once— обработчик сработает только один раз.enter,.esc,.space— срабатывают только на нужной клавише.capture— слушает событие на фазе захвата
В React ту же логику пишут вручную внутри обработчика.
Реактивное состояние: ref и reactive
ref
ref создаёт реактивную ссылку на примитивное или объектное значение:
<template>
<p>Count: {{ count }}</p>
<button @click="increment">+1</button>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => {
count.value++;
};
</script>
В шаблоне Vue автоматически разворачивает .value, поэтому пишем {{ count }}, а не {{ count.value }}. Внутри <script setup> доступ к значению идёт через count.value.
reactive
reactive создаёт реактивный прокси-объект:
<template>
<p>{{ user.name }} — {{ user.age }} years old</p>
<button @click="user.age++">Have birthday</button>
</template>
<script setup>
import { reactive } from 'vue';
const user = reactive({
name: 'Alice',
age: 25
});
</script>
С reactive не нужен .value, но есть ограничения: деструктуризация теряет реактивность, замена всего объекта требует Object.assign.
Сравнение с React
| Vue | React |
|---|---|
ref(0) | useState(0) |
count.value++ | setCount(c => c + 1) |
reactive({...}) | useState({...}) с иммутабельным обновлением |
Подробнее реактивность разбирается в статье Реактивность Vue.
Директивы
Директивы — это специальные атрибуты с префиксом v-, которые добавляют шаблонам реактивное поведение.
v-model
v-model создаёт двустороннее связывание между состоянием и элементом формы:
<template>
<input v-model="message">
<p>You typed: {{ message }}</p>
</template>
<script setup>
import { ref } from 'vue';
const message = ref('');
</script>
Под капотом v-model — это сахар для:
<template>
<input
:value="message"
@input="message = $event.target.value"
>
</template>
Модификаторы v-model
<template>
<!-- Убирает пробелы по краям -->
<input v-model.trim="name">
<!-- Преобразует в число -->
<input v-model.number="age" type="number">
<!-- Ленивая синхронизация по событию change -->
<input v-model.lazy="description">
</template>
v-if, v-else-if, v-else
Условный рендеринг: элементы действительно добавляются и удаляются из DOM:
<template>
<div v-if="user.role === 'admin'">
Admin panel
</div>
<div v-else-if="user.role === 'moderator'">
Moderator tools
</div>
<div v-else>
User dashboard
</div>
</template>
v-show
v-show только переключает CSS-свойство display:
<template>
<p v-show="isVisible">This text is toggled via display</p>
</template>
Когда что использовать:
v-if— если элемент редко переключается или тяжёлый при инициализацииv-show— если элемент часто переключается, и его инициализация дорога
v-for
Для рендеринга списков:
<template>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
</li>
</ul>
</template>
<script setup>
const todos = [
{ id: 1, text: 'Learn Vue' },
{ id: 2, text: 'Build app' },
{ id: 3, text: 'Deploy' }
];
</script>
Можно получить индекс:
<li v-for="(todo, index) in todos" :key="todo.id">
{{ index + 1 }}. {{ todo.text }}
</li>
v-html
Позволяет вставить HTML из строки:
<template>
<div v-html="rawHtml"></div>
</template>
<script setup>
const rawHtml = '<strong>Bold text</strong>';
</script>
⚠️ Опасно для XSS. Используйте только для доверенного контента. Vue по умолчанию экранирует интерполяцию {{ }}, но v-html отключает эту защиту.
v-text
Аналог интерполяции, но через директиву:
<span v-text="message"></span>
<!-- эквивалентно -->
<span>{{ message }}</span>
v-once
Рендерит элемент один раз и пропускает его при последующих обновлениях:
<template>
<p v-once>This will never update: {{ message }}</p>
</template>
Полезно для статичного контента, который не должен участвовать в реактивном обновлении.
Компоненты, props и emits
Определение компонента
Компонент — это переиспользуемый блок UI с собственным состоянием, шаблоном и стилями:
<!-- BaseButton.vue -->
<template>
<button class="base-button" @click="handleClick">
<slot>Default text</slot>
</button>
</template>
<script setup>
const emit = defineEmits(['click']);
const handleClick = () => {
emit('click');
};
</script>
<style scoped>
.base-button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
Props
Props — это входные данные компонента. Они передаются сверху вниз и не должны мутироваться дочерним компонентом:
<!-- UserCard.vue -->
<template>
<div class="user-card">
<h2>{{ name }}</h2>
<p>Age: {{ age }}</p>
<span v-if="isActive">Active</span>
</div>
</template>
<script setup>
const props = defineProps({
name: {
type: String,
required: true
},
age: {
type: Number,
default: 18
},
isActive: {
type: Boolean,
default: false
}
});
</script>
Использование:
<template>
<UserCard name="Alice" :age="25" :is-active="true" />
</template>
<script setup>
import UserCard from './UserCard.vue';
</script>
Важно: defineProps — это макрос, доступный только внутри <script setup>. Его не нужно импортировать.
Emits
Дочерний компонент сообщает родителю о событиях через emit:
<!-- CounterButton.vue -->
<template>
<button @click="emit('increment', 1)">+1</button>
</template>
<script setup>
const emit = defineEmits(['increment']);
</script>
Родитель слушает событие:
<template>
<CounterButton @increment="handleIncrement" />
<p>Count: {{ count }}</p>
</template>
<script setup>
import { ref } from 'vue';
import CounterButton from './CounterButton.vue';
const count = ref(0);
const handleIncrement = (step) => {
count.value += step;
};
</script>
Валидация emits
<script setup>
const emit = defineEmits({
increment: (step) => {
if (typeof step !== 'number') {
console.warn('increment payload must be a number');
return false;
}
return true;
}
});
</script>
Слоты
Слоты позволяют передавать разметку внутрь компонента:
<!-- Card.vue -->
<template>
<div class="card">
<h2><slot name="title">Default title</slot></h2>
<div class="content"><slot>Default content</slot></div>
</div>
</template>
<template>
<Card>
<template #title>Custom Title</template>
<p>This is the main content.</p>
</Card>
</template>
Слоты в Vue похожи на children в React, но более мощные: есть именованные слоты, scoped slots и fallback-контент.
Условный рендеринг
Тернарный оператор и v-if
В шаблоне Vue можно использовать как директивы, так и JavaScript-выражения:
<template>
<p>{{ isLoggedIn ? 'Welcome back' : 'Please sign in' }}</p>
<div v-if="isAdmin">
<AdminPanel />
</div>
<div v-else-if="isModerator">
<ModeratorPanel />
</div>
<div v-else>
<UserPanel />
</div>
</template>
Условная группировка с template
Если нужно применить v-if к нескольким элементам без обёртки:
<template>
<template v-if="isLoaded">
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</template>
<p v-else>Loading...</p>
</template>
Элемент <template> в Vue — это логическая обёртка, которая не рендерится в DOM, аналогично React Fragment <>...</>.
v-if vs v-show
| v-if | v-show | |
|---|---|---|
| Работа с DOM | Монтирует/размонтирует элемент | Переключает display: none |
| Начальная стоимость | Выше, если изначально false | Выше, если изначально false, но элемент всегда монтируется |
| Переключения | Дороже | Дешевле |
| Использование | Редкие переключения, тяжёлые компоненты | Частые переключения, простые элементы |
Рендеринг списков
Базовый список
<template>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
<script setup>
const items = [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' }
];
</script>
Итерация по объекту
<template>
<ul>
<li v-for="(value, key, index) in user" :key="key">
{{ index + 1 }}. {{ key }}: {{ value }}
</li>
</ul>
</template>
<script setup>
const user = { name: 'Alice', age: 25, role: 'admin' };
</script>
v-for и v-if
Нельзя использовать v-if и v-for на одном элементе — v-if будет иметь более высокий приоритет во Vue 3. Рекомендуется вынести фильтрацию в вычисляемое свойство или обернуть во <template>:
<template>
<ul>
<template v-for="todo in visibleTodos" :key="todo.id">
<li v-if="!todo.done">{{ todo.text }}</li>
</template>
</ul>
</template>
<script setup>
import { ref, computed } from 'vue';
const todos = ref([
{ id: 1, text: 'Learn Vue', done: false },
{ id: 2, text: 'Write tests', done: true }
]);
const visibleTodos = computed(() => todos.value.filter(t => !t.done));
</script>
Зачем нужен :key
Атрибут :key помогает Vue отследить идентичность элементов в списке. Без него Vue использует in-place patch стратегию, что может приводить к багам с состоянием при добавлении, удалении или сортировке элементов.
<!-- ❌ Плохо: индекс как ключ -->
<li v-for="(item, index) in items" :key="index">
<!-- ✅ Хорошо: уникальный идентификатор -->
<li v-for="item in items" :key="item.id">
Почему индекс плох:
Если список изменяется, индексы элементов сдвигаются. Vue может переиспользовать DOM-элементы неправильно, что ломает фокус, анимации и внутреннее состояние input-ов.
Вычисляемые свойства и watchers
computed
computed создаёт кэшируемое производное состояние:
<template>
<p>Full name: {{ fullName }}</p>
<input v-model="firstName">
<input v-model="lastName">
</template>
<script setup>
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
</script>
fullName пересчитывается только при изменении firstName или lastName. В React аналог — useMemo.
watch
watch следит за изменением реактивного значения и выполняет side-эффекты:
<template>
<input v-model="searchQuery">
</template>
<script setup>
import { ref, watch } from 'vue';
const searchQuery = ref('');
watch(searchQuery, (newValue, oldValue) => {
console.log('Search changed:', oldValue, '->', newValue);
});
</script>
В React аналог — useEffect с зависимостью.
Подъём состояния и передача данных вверх
Проблема общего состояния
Когда два компонента нуждаются в одних данных, состояние поднимается к ближайшему общему родителю:
<!-- Parent.vue -->
<template>
<TemperatureInput v-model="temperature" />
<TemperatureDisplay :value="temperature" />
</template>
<script setup>
import { ref } from 'vue';
import TemperatureInput from './TemperatureInput.vue';
import TemperatureDisplay from './TemperatureDisplay.vue';
const temperature = ref(20);
</script>
<!-- TemperatureInput.vue -->
<template>
<input
type="number"
:value="modelValue"
@input="emit('update:modelValue', Number($event.target.value))"
>
</template>
<script setup>
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
</script>
<!-- TemperatureDisplay.vue -->
<template>
<p>Temperature: {{ value }}°C</p>
</template>
<script setup>
const props = defineProps(['value']);
</script>
v-model для компонентов
Предыдущий пример можно записать короче:
<!-- TemperatureInput.vue -->
<template>
<input type="number" v-model="value">
</template>
<script setup>
const value = defineModel({ type: Number, default: 0 });
</script>
defineModel — макрос Vue 3.4+, который автоматически создаёт пропс modelValue и событие update:modelValue.
Options API как legacy
До Vue 3 компоненты писали через Options API:
<template>
<button @click="increment">Count: {{ count }}</button>
</template>
<script>
export default {
data() {
return {
count: 0
};
},
methods: {
increment() {
this.count++;
}
}
};
</script>
В Options API логика разбита по опциям: data, methods, computed, watch, props, emits. Это удобно для небольших компонентов, но плохо масштабируется: связанная логика размазывается по разным секциям.
Когда встречается Options API:
- Старые проекты на Vue 2
- Библиотеки, которые ещё не перешли на Composition API
- Код, который нужно прочитать при миграции
Современный подход: всегда используйте <script setup> + Composition API для новых компонентов.
Сравнение с React
| Аспект | Vue 3 | React |
|---|---|---|
| Синтаксис UI | HTML-шаблоны + директивы | JSX — JavaScript-выражения |
| Состояние | ref, reactive | useState, useReducer |
| Производное состояние | computed | useMemo |
| Side-эффекты | watch, watchEffect | useEffect |
| Компоненты | SFC: <script>, <template>, <style> | JSX-функции в одном файле |
| События | emits + v-on | Callback-пропсы + onEvent |
| Двустороннее связывание | v-model | Контролируемый компонент вручную |
| Условный рендеринг | v-if, v-show | &&, ? :, if в JSX |
| Списки | v-for + :key | .map() + key |
| Стили | <style scoped> | CSS Modules, CSS-in-JS, Tailwind |
Vue и React решают схожие задачи, но с разными акцентами. Vue делает типовые паттерны более лаконичными за счёт директив и встроенной реактивности. React предоставляет больше гибкости за счёт того, что UI — это чистый JavaScript.
Лучшие практики и антипаттерны
✅ Делайте
- Используйте
<script setup>и Composition API для новых компонентов. - Всегда указывайте
:keyприv-forс уникальным идентификатором. - Декомпозируйте логику в composables, начиная с 200–300 строк в компоненте.
- Валидируйте
propsи документируйте их типы. - Используйте
v-showдля частых переключений,v-ifдля редких. - Предпочитайте
computedвместо сложных выражений в шаблоне.
❌ Не делайте
- Не мутируйте
propsв дочернем компоненте. - Не используйте
v-htmlдля пользовательского контента без санитизации. - Не ставьте
v-ifиv-forна один элемент. - Не используйте индекс массива как
:keyдля изменяемых списков. - Не смешивайте Options API и Composition API без необходимости.
Ключевые тезисы для интервью
- Vue 3 — прогрессивный фреймворк с декларативными шаблонами, встроенной реактивностью и SFC. Основной API для новых проектов — Composition API с
<script setup>. - SFC-файл состоит из
<script setup>,<template>и<style scoped>. Логика, разметка и стили компонента живут в одном файле, но разделены по блокам. refсоздаёт реактивную ссылку с доступом через.valueв скрипте и автоматическим разворачиванием в шаблоне;reactiveсоздаёт реактивный прокси-объект.- Директивы
v-bind(:),v-on(@),v-model,v-if/v-else,v-for,v-showрешают типовые задачи шаблонизации с минимумом boilerplate. v-model— синтаксический сахар для пропсаmodelValue+ событияupdate:modelValue; в компонентах его можно использовать черезdefineModel.propsпередаются сверху вниз и иммутабельны; дочерний компонент сообщает родителю о событиях черезdefineEmitsиemit.v-ifдобавляет/удаляет элемент из DOM,v-showтолько переключаетdisplay.v-ifдороже при переключении,v-showдороже при первой инициализации, если элемент скрыт.:keyвv-forнужен для корректного отслеживания элементов при изменении списка; индекс массива подходит только для статичных списков.computedкэширует производное состояние;watchвыполняет side-эффекты при изменении зависимости.- Options API — legacy-подход, который всё ещё встречается в Vue 2 и старых проектах, но новый код пишется на Composition API.
Заключение
Vue 3 предлагает мощный и лаконичный способ строить пользовательские интерфейсы. В этой статье мы разобрали фундамент: SFC, шаблоны, основные директивы, компоненты, props, emits, v-model, условный рендеринг и работу со списками. Это база, без которой невозможно двигаться к продвинутым темам: реактивности, Composition API, роутингу, Suspense и TypeScript.
В следующих статьях раздела мы углубимся в реактивную систему Vue, жизненный цикл компонентов, Vue Router, обработку ошибок и оптимизацию производительности. Но понимание основ, описанных здесь, — обязательный минимум для уверенного прохождения собеседований по Vue.
Создайте тестовый проект через npm create vue@latest, поэкспериментируйте с директивами и компонентами. Практика закрепляет теорию гораздо лучше, чем одно чтение.
Полезные ссылки
- Vue.js Documentation — официальная документация Vue 3
- Vue.js Tutorial — интерактивный туториал от авторов фреймворка
- Vue SFC Playground — онлайн-песочница для Vue SFC
- Create Vue — официальный scaffolding-инструмент
- Vite — быстрый сборщик, используемый под капотом create-vue
- Vue Style Guide — официальные рекомендации по стилю кода