Vue — прогрессивный JavaScript-фреймворк для создания пользовательских интерфейсов. Вы описываете, как интерфейс должен выглядеть при текущих данных, а Vue сам обновляет страницу, когда данные меняются. Его можно подключить к готовому сайту одним тегом <script> или построить на нём большое одностраничное приложение (SPA).

Проблема: ручная работа с DOM

Возьмём счётчик: число кликов, подсказку, которая меняется после пятого клика, и кнопку «Reset», неактивную, пока счётчик равен нулю. На чистом JavaScript:

html
<p>Clicks: <span id="count">0</span></p>
<p id="hint">Click the button</p>
<button id="inc">+1</button>
<button id="reset" disabled>Reset</button>

<script>
  let count = 0

  const countEl = document.querySelector('#count')
  const hintEl = document.querySelector('#hint')
  const resetBtn = document.querySelector('#reset')

  // после каждого изменения данных вручную обновляем все зависимые места
  function render() {
    countEl.textContent = count
    hintEl.textContent = count >= 5 ? 'Enough clicking!' : 'Click the button'
    resetBtn.disabled = count === 0
  }

  document.querySelector('#inc').addEventListener('click', () => {
    count++
    render()
  })

  resetBtn.addEventListener('click', () => {
    count = 0
    render()
  })
</script>

Код работает, но в нём много служебной работы: найти элементы, повесить обработчики, после каждого изменения вызвать render() и обновить в нём все зависимые места. Это императивный подход: вы командуете браузеру, как изменить страницу. Когда элементов сотни, а данные меняются из многих мест, легко забыть обновить один из них — и интерфейс покажет неправду.

Декларативный UI: тот же счётчик на Vue

Vue предлагает декларативный подход: вы один раз описываете, что должно быть на экране в зависимости от данных, а синхронизацию берёт на себя фреймворк. Коротко: UI = f(state).

html
<div id="app">
  <p>Clicks: {{ count }}</p>
  <p>{{ count >= 5 ? 'Enough clicking!' : 'Click the button' }}</p>
  <button @click="count++">+1</button>
  <button :disabled="count === 0" @click="count = 0">Reset</button>
</div>

<script type="module">
  import { createApp, ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'

  createApp({
    setup() {
      const count = ref(0) // реактивное значение: Vue следит за его изменениями
      return { count }     // делаем count доступным в шаблоне
    }
  }).mount('#app')         // Vue управляет содержимым <div id="app">
</script>

Разметка внутри #app стала шаблоном: {{ count }} выводит значение, @click вешает обработчик, :disabled привязывает атрибут к выражению (подробно — в разделе о шаблонах). ref(0) создаёт реактивное значение: когда count меняется, Vue сам обновляет зависящие места. После пяти кликов DOM выглядит так:

html
<div id="app" data-v-app="">
  <p>Clicks: 5</p>
  <p>Enough clicking!</p>
  <button>+1</button>
  <button>Reset</button>
</div>

Кнопка потеряла disabled, подсказка сменилась — без отдельного кода (data-v-app — метка контейнера приложения).

💡 Сохраните пример в index.html и откройте в браузере — установка не нужна. Онлайн-песочница: Vue Playground.

«Прогрессивный» фреймворк

«Прогрессивный» значит, что Vue внедряют постепенно, в нужном проекту объёме:

УровеньКак подключаетсяКогда подходит
Виджет на странице<script type="module"> с CDNкалькулятор или фильтр на готовом сайте (Laravel, WordPress)
SPAпроект на Vite, файлы .vueличный кабинет, админ-панель
SPA + экосистема+ Vue Router, Piniaмного страниц и общего состояния
Nuxtмета-фреймворк поверх VueSEO, серверный рендеринг

Первый уровень: Vue «оживляет» один блок на обычной странице, которую сгенерировал сервер.

html
<main>
  <h1>Delivery</h1>
  <!-- обычный HTML страницы: Vue его не трогает -->
  <p>Free delivery for orders over 50 AZN.</p>

  <div id="delivery-calc">
    <label>Weight: <input v-model.number="weight" type="number" min="0"> kg</label>
    <p>Price: {{ price }} AZN</p>
  </div>
</main>

<script type="importmap">
  { "imports": { "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js" } }
</script>

<script type="module">
  import { createApp, ref, computed } from 'vue'

  createApp({
    setup() {
      const weight = ref(1)
      const price = computed(() => 5 + weight.value * 2) // 5 AZN + 2 AZN за кг
      return { weight, price }
    }
  }).mount('#delivery-calc')
</script>

Vue управляет только #delivery-calc: введите вес 3 — и цена станет 11 AZN. Import map позволяет писать from 'vue' вместо полного URL, .prod.js — production-сборка. v-model и computed изучим позже.

В SPA код переезжает в однофайловые компоненты — файлы .vue, где шаблон, логика и стили лежат вместе:

vue
<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <p>Clicks: {{ count }}</p>
  <button @click="count++">+1</button>
  <button :disabled="count === 0" @click="count = 0">Reset</button>
</template>

Шаблон тот же, а <script setup> избавляет от createApp и return: всё объявленное в скрипте доступно в шаблоне. Файлы .vue компилирует Vite — так мы будем писать весь курс.

Vue среди других фреймворков

Все четыре решения строят декларативный UI из компонентов, но с разной философией:

VueReactAngularSvelte
Что этопрогрессивный фреймворкбиблиотека для UIплатформа «всё включено»фреймворк-компилятор
РазметкаHTML-шаблоны (JSX по желанию)JSXHTML-шаблоныHTML-шаблоны
Реактивностьref/reactive на ProxyuseState, перерисовка компонентаSignalsруны $state
Роутинг и состояниеофициальные Vue Router и Piniaсторонние библиотекивстроеныSvelteKit
Мета-фреймворкNuxtNext.jsAngular SSRSvelteKit

Сильные стороны Vue — шаблоны на привычном HTML, точечная реактивность, отличная документация и официальные инструменты. Он популярен в экосистеме Laravel, на нём написан интерфейс GitLab. Знания переносимы: компоненты и реактивность устроены похоже во всех четырёх фреймворках.

Экосистема и актуальная версия

ИнструментДля чегоГде в курсе
Vitedev-сервер и сборкаследующая тема
Vue Routerстраницы и навигация в SPAраздел «Маршрутизация»
Piniaобщее хранилище состоянияраздел «Pinia»
VueUseсотни готовых composablesраздел «Composables»
NuxtSSR, SEO, файловые маршрутыпоследний раздел

Актуальная ветка — Vue 3.5 (на момент написания 3.5.43); версия 3.6 с режимом рендеринга Vapor Mode — в статусе Release Candidate. Vue 2 не поддерживается с 31 декабря 2023 года, но примеров для него в сети много.

Типичные ошибки

Менять DOM вручную внутри Vue-приложения. document.querySelector('#count').textContent = 10 сработает лишь до следующего обновления — Vue перерисует элемент по своим данным. Меняйте данные (count.value = 10), а не DOM.

Забыть type="module". Обычный скрипт не понимает import, и браузер выдаст Uncaught SyntaxError: Cannot use import statement outside a module.

html
<script>import { createApp } from 'vue'</script>               <!-- ❌ -->
<script type="module">import { createApp } from 'vue'</script> <!-- ✅ -->

Копировать примеры для Vue 2. new Vue({ el, data }), Vue.component(), фильтры {{ price | currency }} и $on в Vue 3 не работают. Ищите в примерах createApp.

Оставить dev-сборку на рабочем сайте. vue.esm-browser.js тяжелее и пишет в консоль «You are running a development build of Vue». В продакшне нужен vue.esm-browser.prod.js, а для приложения — проект на Vite.

Итог

  • Vue — фреймворк для декларативного UI: вы описываете результат по данным, а DOM обновляет Vue.
  • ref() создаёт реактивное значение, шаблон с {{ }}, @click и :disabled описывает разметку, createApp().mount() запускает приложение.
  • «Прогрессивный» — значит внедряемый постепенно: от виджета через CDN до SPA на Vite и Nuxt.
  • У Vue HTML-шаблоны, точечная реактивность и официальная экосистема: Vite, Vue Router, Pinia, VueUse, Nuxt.
  • Актуальная версия — Vue 3.5; код с new Vue() относится к устаревшему Vue 2.

Практика

  1. Откройте Vue Playground, измените в примере выводимое значение и посмотрите, как обновился результат справа.
  2. Сохраните счётчик на Vue в index.html и откройте в браузере. Добавьте кнопку «-1», неактивную при нуле.
  3. Добавьте в обе версии счётчика (JS и Vue) строку «Even»/«Odd» — чётное или нечётное число кликов. Сравните, сколько строк пришлось изменить в каждой.
  4. Сделайте виджет «Калькулятор чаевых» для страницы кафе: поле суммы, поле процента и строка «Tip: … AZN, Total: … AZN». Vue управляет только блоком #tip-calc.

💡 В задании 4 используйте import map и computed, как в примере с доставкой, и v-model.number, иначе значения из полей придут строками.