Към съдържанието
wannacode

Първият компонент

Накратко: Vue компонентът е шаблон с данни: описва как изглежда екранът според тях, а Vue сменя в страницата само разликата.

Проблем

Полето губи фокуса

Модулът „JavaScript в страницата“ свърши с кошница, която се рисува от данните с една render(). Тук всяка промяна на полето за брой вика render(). Купувачът иска 15 млека: щраква в полето, трие 2 и пише 15. Опитай го.

HTML, CSS, JS

След триенето полето е празно и вече не е на фокус: 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').
Vue

Смени цената или наличността в обекта 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 />.

Пробвай

Намери грешката

Клик на „Добави“ не пише нищо в конзолата.

Избери реда с грешката.

Пробвай

Попълни

Екранният четец трябва да прочете името на продукта като описание на картинката.

<img :src="product.image" ="product.name" />

Идеята

Компоненти от компоненти

Страницата на магазина има части: горната част с менюто, картата на продукта, кошницата. Всяка част става компонент в свой файл, а по-големият компонент ги ползва като тагове. Така App не е една дълга функция, а няколко реда, които казват какво има на страницата.

ShopHeader.vue
<template>
<header>
<a href="/">Магазинът</a>
<nav>…</nav>
</header>
</template>
App.vue
<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 картата на продукта.

Vue

Идеята

Само разликата

Ето кошницата от проблема като компонент.

ref('2') е паметта на компонента: стойност, която Vue следи. Когато тя се смени, 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>
Стъпка по стъпка, с думи
  1. Купувачът е изтрил 2 и е написал 1: данните казват milk = "1". По средата е последното описание, което компонентът е върнал, а вдясно е DOM-ът с фокуса в полето за мляко.
  2. Купувачът пише 5. @input записва "15" в milk: данните се сменят и Vue знае, че шаблонът чете milk.
  3. Vue е компилирал шаблона в render функция. Понеже milk се смени, той я пуска наново и тя връща ново описание: дърво от обикновени обекти.
  4. Библиотеката сравнява новото описание с предишното. Същите тагове в същия ред и две разлики: стойността на полето и текстът на сумата.
  5. В DOM-а влизат само двете разлики. <input> е същият елемент като преди, затова фокусът и курсорът остават в него.
  6. Ти описваш целия екран за дадените данни, както с 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 картата.

Vue

Тест

Провери се

1. Компонентът се рисува втори път със същите данни. Какво показва?
2. Кой ред в шаблона показва името на продукта като alt на картинката?
3. Защо в компонента фокусът остава в полето, а с render() от предишния модул се губи?
4. App.vue ползва <ShopHeader />, а конзолата казва Failed to resolve component: ShopHeader. Какво липсва?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·