Первая программа на Vue.js
Первая программа на Vue.js
Где применяют Vue
Vue — фреймворк для интерфейса в браузере. Вы описываете страницу почти обычным HTML, а Vue сам обновляет экран, когда меняются данные. Ручной поиск элементов в DOM и textContent = … для каждой кнопки не нужен — это и есть реактивность.
В React разметка чаще живёт в JS (JSX). Во Vue привычнее шаблон <template> с директивами: v-model, v-if, v-for, @click. После HTML/CSS многим так проще войти в SPA.
Рекомендуемый порядок
Если вы только заходите во Vue, держитесь последовательности:
refи простые кнопки.v-modelиv-if.v-forсо списком.onMounted+fetch.
Так вы по шагам закрываете весь базовый цикл клиентского приложения.
В этой статье одно приложение разберём по шагам:
- Счётчик и имя —
ref, директивы, стили в.vue. - Список заметок с Node API —
onMountedиfetch. - Дочерний компонент — props и события вверх.
- Options API — как пишут в legacy-проектах.
Обзор экосистемы: Vue.js. Склейка с API: Fullstack 264. Сравнение: React 272 · Angular 292.
Что получится
| Часть | Результат |
|---|---|
| Проект | create vue + Vite → http://localhost:5173 |
| UI | Приветствие, счётчик, (опционально) список с сервера |
| Файлы | App.vue и при желании components/Counter.vue |
| API | GET http://127.0.0.1:3000/notes, если Node запущен |
Создание проекта
Нужен Node.js (LTS с nodejs.org):
node -v
npm -v
Разбор:
node -vпоказывает установленную версию Node.js и проверяет, что среда выполнения JavaScript доступна в системе.npm -vвыводит версию пакетного менеджера npm, через который устанавливаются зависимости Vue-проекта.- Команды помогают сразу выявить проблему окружения до создания проекта, а не в середине установки.
- Если одна из команд не находится, нужно добавить Node.js в
PATHили переустановить LTS-версию.
npm create vue@latest my-first-vue-app
cd my-first-vue-app
npm install
npm run dev
Разбор:
npm create vue@latest my-first-vue-appгенерирует каркас приложения Vue 3 с актуальными шаблонами.cd my-first-vue-appоткрывает контекст проекта, чтобы установка и запуск выполнялись по нужным путям.npm installподтягивает все библиотеки, включаяvue,viteи служебные dev-зависимости.npm run devзапускает локальный сервер разработки; адрес обычно выводится в терминале.- На этом шаге формируется полный цикл локальной разработки: создать, установить, запустить.
| Вопрос мастера | На первый раз |
|---|---|
| TypeScript | по желанию (ниже — JS) |
| JSX | No |
| Vue Router | Yes |
| Pinia | No |
| Vitest / Cypress | No |
После npm run dev откройте адрес из терминала (обычно 5173). Сохранили файл — страница обновилась (HMR).
src/App.vue — корень. src/components/ — ваши блоки UI. src/main.js монтирует приложение в #app.
Файл .vue — три блока
<script setup> ← логика
<template> ← разметка + директивы
<style scoped> ← стили только этого компонента
Разбор:
<script setup>хранит реактивное состояние, функции и импорты компонента.<template>описывает структуру интерфейса и связывает её с данными через интерполяцию и директивы.<style scoped>ограничивает CSS текущим компонентом, чтобы стили не протекали в соседние части приложения.- Такое разделение делает файл
.vueсамодостаточным: логика, разметка и стиль живут рядом и проще поддерживаются.
Composition API и <script setup>
Полный src/App.vue:
<script setup>
import { ref } from 'vue'
const count = ref(0)
const name = ref('')
const increment = () => { count.value++ }
const decrement = () => { count.value-- }
const reset = () => { count.value = 0 }
</script>
<template>
<div class="app">
<h1>Моя первая программа на Vue.js</h1>
<section class="greeting">
<input v-model="name" type="text" placeholder="Введите ваше имя" />
<h2 v-if="name">Привет, {{ name }}!</h2>
</section>
<section class="counter">
<h2>Счётчик: {{ count }}</h2>
<div class="buttons">
<button type="button" @click="decrement">−</button>
<button type="button" @click="reset">Сброс</button>
<button type="button" @click="increment">+</button>
</div>
</section>
</div>
</template>
<style scoped>
.app { text-align: center; padding: 2rem; font-family: system-ui, sans-serif; }
.greeting { margin: 1.5rem 0; padding: 1.25rem; background: #f0f0f0; border-radius: 10px; }
.counter { margin: 1.5rem 0; padding: 1.25rem; background: #e8f4f8; border-radius: 10px; }
input { padding: 0.5rem 0.75rem; font-size: 1rem; border: 2px solid #ddd; border-radius: 6px; }
button {
margin: 0 4px; padding: 0.5rem 1rem; font-size: 1rem;
background: #42b883; color: #fff; border: none; border-radius: 6px; cursor: pointer;
}
button:hover { background: #3aa876; }
</style>
Разбор — скрипт
| Код | Смысл |
|---|---|
ref(0) | Реактивная ячейка; в script меняем через .value |
\{\{ count \}\} в template | Vue разворачивает ref сам |
() => count.value++ | Обработчик клика — функция, не вызов сразу |
Разбор — шаблон
| Синтаксис | Роль |
|---|---|
\{\{ name \}\} | Подстановка текста |
v-model="name" | Двусторонняя связь с input |
v-if="name" | Показать блок, если имя не пустое |
@click="increment" | Событие клика (v-on:click) |
.valueВ <script setup> пишут count.value++. В шаблоне — просто {'{{ count }}'}.
computed
Производное значение без ручной синхронизации:
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
</script>
<template>
<p>Счётчик: {{ count }}, удвоенный: {{ doubled }}</p>
</template>
Разбор:
computed(() => count.value * 2)создаёт производное состояние, которое кешируется и пересчитывается только при измененииcount.- В шаблоне
{{ doubled }}используется как обычная переменная, хотя в скрипте это вычисляемый ref. - Подход избавляет от ручной синхронизации между
countи его производными значениями. - Шаблон остаётся декларативным: он только отображает состояние, а формулы находятся в
script.
computed пересчитывается только когда меняются зависимости.
onMounted и заметки с API
- API "Заметки" на порту 3000.
- CORS или прокси — 263, 264.
<script setup>
import { ref, onMounted } from 'vue'
const notes = ref([])
const error = ref('')
const loading = ref(true)
onMounted(async () => {
try {
const res = await fetch('http://127.0.0.1:3000/notes')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
notes.value = await res.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
})
</script>
<template>
<section>
<h2>Заметки с сервера</h2>
<p v-if="loading">Загрузка…</p>
<p v-else-if="error">Ошибка: {{ error }}</p>
<ul v-else>
<li v-for="n in notes" :key="n.id">{{ n.text }}</li>
</ul>
</section>
</template>
Разбор:
onMounted(async () => { ... })запускает асинхронный код после монтирования компонента и готовности UI.fetch('http://127.0.0.1:3000/notes')запрашивает данные API;res.okпроверяет успешность HTTP-ответа.notes.value = await res.json()сохраняет полученный массив в реактивное состояние для отрисовки списка.- Блок
try/catch/finallyразделяет успешный сценарий, ошибку и финальное выключениеloading. - В шаблоне
v-if / v-else-if / v-elseуправляет тремя состояниями интерфейса: загрузка, ошибка, данные.
| Элемент | Зачем |
|---|---|
onMounted | Код после появления компонента на странице |
v-for + :key | Список со стабильными id |
v-if / v-else-if | Состояния загрузки и ошибки |
Дочерний компонент
src/components/Counter.vue:
<script setup>
defineProps({ value: { type: Number, required: true } })
const emit = defineEmits(['increment', 'decrement', 'reset'])
</script>
<template>
<section class="counter">
<h2>Счётчик: {{ value }}</h2>
<button type="button" @click="emit('decrement')">−</button>
<button type="button" @click="emit('reset')">Сброс</button>
<button type="button" @click="emit('increment')">+</button>
</section>
</template>
Разбор:
defineProps(...)объявляет входные данные компонента и валидирует обязательностьvalue.defineEmits(['increment', 'decrement', 'reset'])фиксирует контракт событий, которые компонент отправляет родителю.emit('...')в обработчике кнопки передаёт намерение действия наверх, не меняя состояние напрямую.- Компонент остаётся переиспользуемым: он отображает значение и генерирует события, а бизнес-логика хранится у родителя.
App.vue:
<script setup>
import { ref } from 'vue'
import Counter from './components/Counter.vue'
const count = ref(0)
</script>
<template>
<Counter
:value="count"
@increment="count++"
@decrement="count--"
@reset="count = 0"
/>
</template>
Разбор:
import Counter from './components/Counter.vue'подключает дочерний компонент в родительскийApp.vue.:value="count"передаёт реактивное значение вниз черезprops.@increment,@decrement,@resetподписывают родитель на события ребёнка и изменяютcountв одном источнике истины.- Паттерн формирует предсказуемую архитектуру: данные вниз, события вверх.
Props вниз, события вверх — как в React.
Options API — устаревший стиль
<script>
export default {
data() {
return { count: 0, name: '' }
},
methods: {
increment() { this.count++ },
decrement() { this.count-- },
reset() { this.count = 0 },
},
}
</script>
<template>
<h2>Счётчик: {{ count }}</h2>
<input v-model="name" />
<button type="button" @click="increment">+</button>
</template>
Разбор:
data()возвращает начальное состояние компонента в стиле Options API.- В
methodsразмещаются обработчики, которые вызываются из шаблона через@click. this.count++иthis.count--меняют реактивное поле экземпляра компонента.- Пример иллюстрирует legacy-стиль Vue 2/раннего Vue 3, который часто встречается в существующих кодовых базах.
| Options API | Composition API |
|---|---|
data() | ref() / reactive() |
this.count++ | count.value++ |
| Учебные и старые кодовые базы | Новые проекты, composables |
Стили scoped
<style scoped> ограничивает CSS этим компонентом. Цвет #42b883 — фирменный зелёный Vue.
Краткий обзор концепций Vue
Реактивность связывает данные и DOM. Шаблон описывает, что показать. Директивы v-* добавляют поведение. Компоненты делят UI на части.
Практика — что добавить самому
- Кнопка ×2:
count.value *= 2. - Todo на
ref([])иv-for. - Таймер:
setIntervalвonMounted,clearIntervalвonUnmounted. - POST заметки:
fetchсmethod: 'POST'иJSON.stringify— шаблоны с разбором в Fetch / axios — типовые запросы.
Запуск
- Node.js LTS.
- Установка и dev-сервер (
npm create vue@latest→npm install→npm run dev):
npm create vue@latest
npm install
npm run dev
Разбор:
- Первая команда создаёт новый проект Vue через официальный генератор.
- Вторая устанавливает зависимости, необходимые для сборки и запуска.
- Третья поднимает dev-сервер с наблюдением за файлами.
- Три команды покрывают базовую инициализацию проекта с нуля.
- Вставить код в
App.vue. - (Опционально) второй терминал — API 262.
- Production:
npm run build→ каталогdist/.
npm run build
Разбор:
- Команда запускает production-сборку и генерирует оптимизированные файлы в
dist/. - На этапе сборки выполняются минификация, разделение чанков и удаление неиспользуемого кода.
- Результат готов к публикации на статический хостинг или интеграции в CI/CD-пайплайн.
- Ошибки сборки на этом шаге обычно сигнализируют о проблемах импорта, типов или конфигурации.
Частые ошибки
| Симптом | Причина |
|---|---|
| Счётчик не двигается | Нет .value в script |
| CORS | 264 |
Список без key | Предупреждения Vue при v-for |
Что попробовать
- React 272 на том же API — галерея компонентов для сравнения синтаксиса.
- Vue Router — страница
/about. - Next 2731 для SSR.
Связанные материалы
| Тема | Материал |
|---|---|
| Обзор | 28.md |
| Галерея примеров | Vue и Svelte — лаборатория |
| Fullstack | 264.md |
| Чек-лист | 999.md |
Vue удобен тем, что первая программа похожа на вёрстку, а дальше тот же проект растёт до SPA с API и тестами.
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.