- 1. Основы Vue.js 3
- 2. Установка и настройка
- 3. Composition API
- 4. Options API
- 5. Компоненты
- 6. Директивы
- 7. Маршрутизация (Vue Router)
- 8. Управление состоянием
- 9. Жизненный цикл компонентов
- 10. Работа с формами
- 11. Анимации и переходы
- 12. Тестирование
- 13. Полезные ресурсы
Vue.js — это прогрессивный JavaScript-фреймворк для создания пользовательских интерфейсов. В отличие от монолитных фреймворков, Vue спроектирован с возможностью постепенного внедрения.
- Реактивность — автоматическое обновление DOM при изменении данных
- Декларативный рендеринг — описание HTML с использованием шаблонов
- Компонентная архитектура — разделение UI на независимые компоненты
- Composition API — новый подход к организации логики компонентов
- Virtual DOM — эффективное обновление DOM
npm install -g @vue/cli
vue create my-project
my-project/
├── node_modules/
├── public/
│ ├── favicon.ico
│ └── index.html
├── src/
│ ├── assets/
│ ├── components/
│ ├── App.vue
│ └── main.js
├── package.json
└── vue.config.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
Composition API — это новый способ организации логики компонентов в Vue.js 3.
Создает реактивную ссылку на примитивное значение:
import { ref } from 'vue'
const count = ref(0)
// Доступ к значению через .value
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
Создает реактивный объект:
import { reactive } from 'vue'
const state = reactive({
count: 0,
name: 'Vue'
})
// Доступ напрямую (без .value)
console.log(state.count) // 0
state.count++
Создает вычисляемое свойство:
import { ref, computed } from 'vue'
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
Отслеживание изменений:
import { ref, watch, watchEffect } from 'vue'
const count = ref(0)
// Явное отслеживание
watch(count, (newValue, oldValue) => {
console.log(`count изменился с ${oldValue} на ${newValue}`)
})
// Автоматическое отслеживание зависимостей
watchEffect(() => {
console.log(`count сейчас: ${count.value}`)
})
Хуки жизненного цикла:
import { onMounted, onUpdated, onUnmounted } from 'vue'
export default {
setup() {
onMounted(() => {
console.log('Компонент смонтирован')
})
onUpdated(() => {
console.log('Компонент обновлен')
})
onUnmounted(() => {
console.log('Компонент удален')
})
}
}
Передача данных между компонентами:
// Родительский компонент
import { provide, ref } from 'vue'
export default {
setup() {
const theme = ref('dark')
provide('theme', theme)
}
}
// Дочерний компонент
import { inject } from 'vue'
export default {
setup() {
const theme = inject('theme')
return { theme }
}
}
Options API — традиционный способ создания компонентов в Vue.
export default {
data() {
return {
count: 0,
message: 'Hello Vue!'
}
}
}
export default {
data() {
return { count: 0 }
},
methods: {
increment() {
this.count++
}
}
}
export default {
data() {
return { count: 0 }
},
computed: {
doubleCount() {
return this.count * 2
}
}
}
export default {
data() {
return { count: 0 }
},
watch: {
count(newValue, oldValue) {
console.log(`count изменился с ${oldValue} на ${newValue}`)
}
}
}
<template>
<div>
<h1>{{ title }}</h1>
<button @click="increment">Счетчик: {{ count }}</button>
</div>
</template>
<script>
export default {
data() {
return {
title: 'Мой компонент',
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
<style scoped>
h1 {
color: blue;
}
</style>
<template>
<div>
<h1>{{ title }}</h1>
<button @click="increment">Счетчик: {{ count }}</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('Мой компонент')
const count = ref(0)
function increment() {
count.value++
}
</script>
<style scoped>
h1 {
color: blue;
}
</style>
import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './components/MyComponent.vue'
const app = createApp(App)
app.component('MyComponent', MyComponent)
app.mount('#app')
import MyComponent from './components/MyComponent.vue'
export default {
components: {
MyComponent
}
}
<!-- Родительский компонент -->
<template>
<ChildComponent :message="parentMessage" />
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const parentMessage = ref('Сообщение от родителя')
</script>
<!-- Дочерний компонент (ChildComponent.vue) -->
<template>
<div>{{ message }}</div>
</template>
<script setup>
const props = defineProps({
message: {
type: String,
required: true
}
})
</script>
<!-- Дочерний компонент -->
<template>
<button @click="emitEvent">Отправить событие</button>
</template>
<script setup>
const emit = defineEmits(['custom-event'])
function emitEvent() {
emit('custom-event', 'Данные события')
}
</script>
<!-- Родительский компонент -->
<template>
<ChildComponent @custom-event="handleEvent" />
</template>
<script setup>
import ChildComponent from './ChildComponent.vue'
function handleEvent(data) {
console.log('Получено событие с данными:', data)
}
</script>
Связывает атрибут с выражением:
<img v-bind:src="imageUrl">
<!-- Сокращенная запись -->
<img :src="imageUrl">
Привязывает обработчик события:
<button v-on:click="handleClick">Нажми меня</button>
<!-- Сокращенная запись -->
<button @click="handleClick">Нажми меня</button>
Двустороннее связывание:
<input v-model="message">
Условный рендеринг:
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else>Не A/B</div>
Условное отображение (через CSS):
<div v-show="isVisible">Видимый элемент</div>
Рендеринг списков:
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index }}: {{ item.name }}
</li>
</ul>
Слоты для контента:
<!-- Базовый слот -->
<template v-slot:default>
Содержимое слота
</template>
<!-- Именованный слот (сокращенно) -->
<template #header>
Заголовок
</template>
// Глобальная регистрация
app.directive('focus', {
mounted(el) {
el.focus()
}
})
// Использование
<input v-focus>
npm install vue-router@4
import { createRouter, createWebHistory } from 'vue-router'
import Home from './views/Home.vue'
import About from './views/About.vue'
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
{
path: '/user/:id',
component: User,
props: true
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// Подключение к приложению
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.use(router)
app.mount('#app')
<template>
<div>
<router-link to="/">Главная</router-link>
<router-link to="/about">О нас</router-link>
<!-- Отображение активного маршрута -->
<router-view></router-view>
</div>
</template>
<script setup>
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
// Программная навигация
function goToHome() {
router.push('/')
}
// Доступ к параметрам маршрута
console.log(route.params.id)
</script>
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login')
} else {
next()
}
})
npm install pinia
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// state
state: () => ({
count: 0
}),
// getters
getters: {
doubleCount: (state) => state.count * 2
},
// actions
actions: {
increment() {
this.count++
}
}
})
// Вариант с Composition API
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
<template>
<div>
<p>Счетчик: {{ counterStore.count }}</p>
<p>Удвоенный: {{ counterStore.doubleCount }}</p>
<button @click="counterStore.increment">Увеличить</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
</script>
npm install vuex@next
import { createStore } from 'vuex'
const store = createStore({
state() {
return {
count: 0
}
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
},
getters: {
doubleCount: (state) => state.count * 2
}
})
// Подключение к приложению
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.use(store)
app.mount('#app')
export default {
beforeCreate() {
// Перед инициализацией
},
created() {
// Компонент инициализирован
},
beforeMount() {
// Перед монтированием DOM
},
mounted() {
// DOM смонтирован
},
beforeUpdate() {
// Перед обновлением DOM
},
updated() {
// После обновления DOM
},
beforeUnmount() {
// Перед удалением компонента
},
unmounted() {
// Компонент удален
}
}
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
export default {
setup() {
onBeforeMount(() => {
// Перед монтированием DOM
})
onMounted(() => {
// DOM смонтирован
})
onBeforeUpdate(() => {
// Перед обновлением DOM
})
onUpdated(() => {
// После обновления DOM
})
onBeforeUnmount(() => {
// Перед удалением компонента
})
onUnmounted(() => {
// Компонент удален
})
}
}
<template>
<form @submit.prevent="submitForm">
<div>
<label for="name">Имя:</label>
<input id="name" v-model="form.name">
</div>
<div>
<label for="email">Email:</label>
<input id="email" v-model="form.email" type="email">
</div>
<div>
<label>Пол:</label>
<input type="radio" v-model="form.gender" value="male"> Мужской
<input type="radio" v-model="form.gender" value="female"> Женский
</div>
<div>
<label>Интересы:</label>
<input type="checkbox" v-model="form.interests" value="sports"> Спорт
<input type="checkbox" v-model="form.interests" value="music"> Музыка
<input type="checkbox" v-model="form.interests" value="reading"> Чтение
</div>
<div>
<label for="country">Страна:</label>
<select id="country" v-model="form.country">
<option value="">Выберите страну</option>
<option value="ru">Россия</option>
<option value="us">США</option>
<option value="uk">Великобритания</option>
</select>
</div>
<div>
<label for="message">Сообщение:</label>
<textarea id="message" v-model="form.message"></textarea>
</div>
<button type="submit">Отправить</button>
</form>
</template>
<script setup>
import { reactive } from 'vue'
const form = reactive({
name: '',
email: '',
gender: '',
interests: [],
country: '',
message: ''
})
function submitForm() {
console.log('Форма отправлена:', form)
// Здесь логика отправки формы
}
</script>
<template>
<form @submit.prevent="submitForm">
<div>
<label for="name">Имя:</label>
<input id="name" v-model="form.name" @blur="validateName">
<span v-if="errors.name" class="error">{{ errors.name }}</span>
</div>
<div>
<label for="email">Email:</label>
<input id="email" v-model="form.email" type="email" @blur="validateEmail">
<span v-if="errors.email" class="error">{{ errors.email }}</span>
</div>
<button type="submit" :disabled="!isFormValid">Отправить</button>
</form>
</template>
<script setup>
import { reactive, computed } from 'vue'
const form = reactive({
name: '',
email: ''
})
const errors = reactive({
name: '',
email: ''
})
function validateName() {
if (!form.name) {
errors.name = 'Имя обязательно для заполнения'
} else if (form.name.length < 3) {
errors.name = 'Имя должно содержать не менее 3 символов'
} else {
errors.name = ''
}
}
function validateEmail() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!form.email) {
errors.email = 'Email обязателен для заполнения'
} else if (!emailRegex.test(form.email)) {
errors.email = 'Введите корректный email'
} else {
errors.email = ''
}
}
const isFormValid = computed(() => {
return form.name && form.email && !errors.name && !errors.email
})
function submitForm() {
validateName()
validateEmail()
if (isFormValid.value) {
console.log('Форма отправлена:', form)
// Здесь логика отправки формы
}
}
</script>
<style scoped>
.error {
color: red;
font-size: 0.8em;
}
</style>
<template>
<button @click="show = !show">Переключить</button>
<Transition name="fade">
<p v-if="show">Привет, мир!</p>
</Transition>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(true)
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
<template>
<button @click="addItem">Добавить</button>
<button @click="removeItem">Удалить</button>
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</TransitionGroup>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, text: 'Элемент 1' },
{ id: 2, text: 'Элемент 2' },
{ id: 3, text: 'Элемент 3' }
])
let nextId = 4
function addItem() {
items.value.push({
id: nextId++,
text: `Элемент ${nextId - 1}`
})
}
function removeItem() {
if (items.value.length > 0) {
items.value.splice(Math.floor(Math.random() * items.value.length), 1)
}
}
</script>
<style>
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-move {
transition: transform 0.5s ease;
}
</style>
npm install --save-dev @vue/test-utils vitest
// Counter.spec.js
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from '../Counter.vue'
describe('Counter.vue', () => {
it('увеличивает счетчик при нажатии на кнопку', async () => {
const wrapper = mount(Counter)
// Проверяем начальное состояние
expect(wrapper.text()).toContain('Счетчик: 0')
// Нажимаем на кнопку
await wrapper.find('button').trigger('click')
// Проверяем, что счетчик увеличился
expect(wrapper.text()).toContain('Счетчик: 1')
})
})
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import LifecycleComponent from '../LifecycleComponent.vue'
describe('LifecycleComponent.vue', () => {
it('вызывает метод при монтировании', () => {
// Создаем шпиона для console.log
const spy = vi.spyOn(console, 'log')
// Монтируем компонент
mount(LifecycleComponent)
// Проверяем, что console.log был вызван с ожидаемым сообщением
expect(spy).toHaveBeenCalledWith('Компонент смонтирован')
// Восстанавливаем оригинальную функцию
spy.mockRestore()
})
})
- Vue DevTools
- Vite - быстрый инструмент сборки
- Nuxt.js - фреймворк для создания SSR приложений на Vue
- Vueuse - коллекция утилит для Composition API
- Vue Test Utils - утилиты для тестирования
- Quasar - фреймворк UI компонентов
- Vuetify - Material Design компоненты