Първият компонент
Накратко: Vue компонентът е шаблон с данни: описва как изглежда екранът според тях, а Vue сменя в страницата само разликата.
Проблем
Полето губи фокуса
Модулът „JavaScript в страницата“ свърши с кошница, която се рисува от данните с една render(). Тук всяка промяна на полето за брой вика render(). Купувачът иска 15 млека: щраква в полето, трие 2 и пише 15. Опитай го.
Конзола
След триенето полето е празно и вече не е на фокус: 1 и 5 не стигат до никъде. render() е сменила полето с ново още при първия клавиш. Новото поле изглежда същото, но е друг елемент, а фокусът беше у стария. Ръчното пипане на отделни места забравя места. Рисуването на всичко наново губи фокуса. Има трети път.
Идеята
Интерфейсът като функция на данните
render() имаше добра идея: екранът се строи само от данните. При едни и същи данни екранът е един и същ, каквото и да е имало на него преди. Грешката беше в последната стъпка: render() сама маха и прави елементите.
Затова работата се дели на две:
- Ти пишеш функция, която казва как изглежда екранът при тези данни. Тя не пипа страницата, а връща описание: тук има
<main>, в него<h1>с текст „Кошницата“, после поле със стойност 2. - Библиотеката държи истинския DOM. Първия път го строи от описанието. При всяка промяна на данните взима новото описание, сравнява го със старото и сменя в страницата само разликата.
Такава функция се казва компонент. Накратко: екранът е функция на данните, а компонентът е тази функция. React и Vue правят това по различен начин. Идеята е една и съща, а изборът между тях е въпрос на екип, не на „по-добра“ библиотека.
Виж го
Първият компонент
Във Vue компонентът е файл .vue (Single File Component) с до три части: <script setup> с данните и функциите, <template> с описанието на екрана и по желание <style>.
<script setup>const product = { name: 'Прясно мляко', price: 1.6, stock: 35 };</script>
<template> <main> <h1>{{ product.name }}</h1> <p>Цена: {{ money(product.price) }}</p> </main></template>- Всичко, което
<script setup>обявява (променливи, функции, внесени компоненти), шаблонът вижда. {{ product.name }}е израз на JavaScript в двойни фигурни скоби. Резултатът му става текст в страницата.- Името на компонента е името на файла:
App.vueе компонентътApp. - Пясъчникът сам слага
Appв страницата. В истински проект това прави един ред вmain.js:createApp(App).mount('#app').
Конзола
Смени цената или наличността в обекта product и натисни „Изпълни“. Описанието на екрана не го пипаш: екранът следва данните.
Идеята
Правилата на шаблона
Шаблонът е HTML, но Vue го компилира до JavaScript и чете в него свои знаци. Затова има свои правила.
{{ израз }}в текста:<h1>{{ product.name }}</h1>.:пред атрибут значи „стойността е израз“:<img :src="product.image" :alt="product.name" />.:altе съкращение наv-bind:alt. Без:стойността е текстът „product.name“, буква по буква.{{ }}в атрибут не работи:alt="{{ product.name }}"е текстът{{ product.name }}.@пред събитие:@click="add"(съкращение наv-on:click). Може и кратък израз:@click="count++"или@click="add(product)". Vue го обвива във функция, която тръгва при клик.classиforси оставатclassиfor, както в HTML.- Няколко корена са позволени: шаблонът може да има
<header>и<main>един до друг. - Таговете без съдържание може да са затворени с
/>:<input />,<ProductCard />.
Пробвай
Намери грешката
Клик на „Добави“ не пише нищо в конзолата.
Избери реда с грешката.
onclick="add()" е обикновен HTML атрибут. Vue го слага в страницата такъв, какъвто е, и при клик браузърът търси глобална функция add. Такава няма, затова конзолата казва „ReferenceError: add is not defined“. add от <script setup> се вика с @click="add".Пробвай
Попълни
Екранният четец трябва да прочете името на продукта като описание на картинката.
<img :src="product.image" ="product.name" />:alt="product.name": двоеточието казва, че стойността е израз. Пълният запис е v-bind:alt.Идеята
Компоненти от компоненти
Страницата на магазина има части: горната част с менюто, картата на продукта, кошницата. Всяка част става компонент в свой файл, а по-големият компонент ги ползва като тагове. Така App не е една дълга функция, а няколко реда, които казват какво има на страницата.
<template> <header> <a href="/">Магазинът</a> <nav>…</nav> </header></template><script setup>import ShopHeader from './ShopHeader.vue';import ProductCard from './ProductCard.vue';</script>
<template> <ShopHeader /> <main> <ProductCard /> </main></template>Внесеният компонент шаблонът вижда директно. Без import Vue не знае кой е ShopHeader: в конзолата пише „[Vue warn]: Failed to resolve component: ShopHeader“, а на страницата остава непознат таг. Името е с главна буква по навик (PascalCase), за да се различава от HTML таговете. Vue приема и <shop-header />.
Пробвай
Сглоби страницата
ShopHeader и ProductCard са готови в своите файлове. Сглоби App: горе ShopHeader, а под заглавието „Млечни“ в main картата на продукта.
Конзола
Решение
App.vue<script setup>
import ShopHeader from './ShopHeader.vue';
import ProductCard from './ProductCard.vue';
</script>
<template>
<ShopHeader />
<main>
<h1>Млечни</h1>
<ProductCard />
</main>
</template>
ShopHeader.vue<template>
<header>
<a href="/">Магазинът</a>
<nav>
<a href="/bread">Хляб</a>
<a href="/milk">Мляко</a>
<a href="/cart">Кошница</a>
</nav>
</header>
</template>
ProductCard.vue<script setup>
const product = { name: 'Прясно мляко', price: 1.6, stock: 35 };
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
</script>
<template>
<article>
<h2>{{ product.name }}</h2>
<p>Цена: {{ money(product.price) }}</p>
</article>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; background: #f3efe6; padding: 8px 16px; }
nav a { margin-left: 12px; }
main { padding: 0 16px; }
h1 { font-size: 22px; }
label { display: block; margin: 8px 0; }
input { width: 4em; font: inherit; margin-left: 8px; }
article { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 12px 12px; max-width: 320px; }
button { font: inherit; }
<script setup> внася двата компонента, а шаблонът ги ползва като тагове: <ShopHeader /> преди <main>, <ProductCard /> в <main>. Шаблонът може да има два корена един до друг. Проверката търси горната част по роля: <header> вътре в <main> вече не е горната част на страницата, а без import няма никакъв <header>.
Идеята
Само разликата
Ето кошницата от проблема като компонент.
ref('2') е паметта на компонента: стойност, която Vue следи. Когато тя се смени, Vue рисува наново компонентите, които я четат. Тя е темата на модула „Състояние“. Тук е само за да има какво да се сменя.
Конзола
Направи същото като в проблема: щракни в полето за мляко, изтрий 2 и напиши 15. Полето пази фокуса и написаното, а сумата става 25,20 €. Компонентът описва целия екран при всеки клавиш, както render(). Разликата е, че страницата не се строи наново. Библиотеката вижда, че в новото описание полето е на същото място, със същия таг, и сменя само неговата стойност и текста на сумата.
Виж го
Виж сравнението
<script setup>import { ref } from 'vue';const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const milk = ref('2');const bread = ref('1');</script>
<template> <main> <h1>Кошницата</h1> <label> Прясно мляко <input type="number" :value="milk" @input="milk = $event.target.value" /> </label> <label> Бял хляб <input type="number" :value="bread" @input="bread = $event.target.value" /> </label> <p>Общо: {{ money(Number(milk) * 1.6 + Number(bread) * 1.2) }}</p> </main></template>Стъпка по стъпка, с думи
- Купувачът е изтрил 2 и е написал 1: данните казват milk = "1". По средата е последното описание, което компонентът е върнал, а вдясно е DOM-ът с фокуса в полето за мляко.
- Купувачът пише 5. @input записва "15" в milk: данните се сменят и Vue знае, че шаблонът чете milk.
- Vue е компилирал шаблона в render функция. Понеже milk се смени, той я пуска наново и тя връща ново описание: дърво от обикновени обекти.
- Библиотеката сравнява новото описание с предишното. Същите тагове в същия ред и две разлики: стойността на полето и текстът на сумата.
- В DOM-а влизат само двете разлики. <input> е същият елемент като преди, затова фокусът и курсорът остават в него.
- Ти описваш целия екран за дадените данни, както с render(). Библиотеката намира какво се е сменило и пипа само него. Двете библиотеки правят това, всяка по своя начин.
Описанието е евтино: то е дърво от обикновени обекти и компонентът го прави наново всеки път. Скъпото е DOM-ът, затова библиотеката пипа в него само разликите. Така ти пишеш като с render(): целия екран от данните. Цената на render() (изгубен фокус, хиляди нови елементи) не се плаща.
По-точно, за любопитните
Виртуален DOM. Описанието често се казва виртуален DOM (virtual DOM), а сравнението му със старото се казва reconciliation (React) или patch (Vue). Възел от описанието е обект като { type: 'h1', props: { children: 'Кошницата' } }.
Двата начина. Шаблонът се компилира до render функция, която връща възлите на описанието. Компилаторът отбелязва кои части никога не се сменят и сравнението ги прескача. Vue помни кои данни чете всеки компонент (реактивност) и пуска наново само компонентите, чиито данни са се сменили. И в двата случая накрая има ново описание, сравнение и малко промени в DOM-а.
Текстът е текст. {{ product.name }} слага текст, както textContent. Име като <img src=x onerror=alert(1)> се вижда като знаци, а не става таг. HTML от низ се слага само с v-html, и името казва да внимаваш.
Options API. В по-стар код компонентът е обект: export default { data() { … }, methods: { … } }. Работи и днес, но новият код е с <script setup> (Composition API).
v-model. Двойката :value и @input има съкращение, v-model. То е от урока за формулярите.
Упражнение
Картата на продукта
В ProductCard е обектът product (Сирене). Направи картата: името като заглавие от второ ниво, „Цена: 7,90 €“ (с money), „В наличност: 12 бр.“ и бутон „Добави“ с име за екранния четец „Добави Сирене“. Клик на бутона пише в конзолата „Добавен: Сирене“ (функцията add е готова). После сглоби App: ShopHeader горе, а под заглавието „Млечни“ в main картата.
Конзола
Решение
ProductCard.vue<script setup>
const product = { name: 'Сирене', price: 7.9, stock: 12 };
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
function add() {
console.log(`Добавен: ${product.name}`);
}
</script>
<template>
<article>
<h2>{{ product.name }}</h2>
<p>Цена: {{ money(product.price) }}</p>
<p>В наличност: {{ product.stock }} бр.</p>
<button type="button" :aria-label="`Добави ${product.name}`" @click="add">Добави</button>
</article>
</template>
App.vue<script setup>
import ShopHeader from './ShopHeader.vue';
import ProductCard from './ProductCard.vue';
</script>
<template>
<ShopHeader />
<main>
<h1>Млечни</h1>
<ProductCard />
</main>
</template>
ShopHeader.vue<template>
<header>
<a href="/">Магазинът</a>
<nav>
<a href="/bread">Хляб</a>
<a href="/milk">Мляко</a>
<a href="/cart">Кошница</a>
</nav>
</header>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; background: #f3efe6; padding: 8px 16px; }
nav a { margin-left: 12px; }
main { padding: 0 16px; }
h1 { font-size: 22px; }
label { display: block; margin: 8px 0; }
input { width: 4em; font: inherit; margin-left: 8px; }
article { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 12px 12px; max-width: 320px; }
button { font: inherit; }
Текстът от данните е в {{ … }}: {{ product.name }}, {{ money(product.price) }}, {{ product.stock }}. Името за екранния четец е атрибут от израз, с двоеточие: :aria-label="`Добави ${product.name}`". Без двоеточие бутонът се казва буквално „Добави ${product.name}“ и проверката не го намира. Бутонът вика функцията с @click="add".
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Достъпност · DOM: страницата от JavaScript · Събития · Списък от данни
- Продължава в: Props: данни отгоре · Списъци и ключове · Условия: празно, грешка, зарежда · Състояние
- Задачи по темата: Компоненти