Swimmers jumping at the start of the race - allegory of starting Vue.js app

Jak zacząć przygodę z Vue.js?

Technologie frontendowe są niezwykle dynamicznie rozwijającym się polem branży programistycznej. Coraz częściej zaczynając nowe projekty, programiści Drupala decydują się na porzucenie czystego JavaScript i jQuery na rzecz nowoczesnego frameworka do budowania warstwy klienta aplikacji. Pojawia się wtedy dylemat, którego z nich użyć. Pod uwagę brane są zazwyczaj trzy pozycje: Vue, React i Angular. W niniejszym artykule omówimy podstawy pierwszego z nich w możliwie prosty i przyjazny sposób.

Czym jest Vue.js?

Vue.js, podobnie jak React.js czy Angular, należy do rodziny najbardziej popularnych frameworków JavaScript służących do budowania interfejsów użytkownika. Pozwala na tworzenie zarówno prostych komponentów, jak i zaawansowanych i skalowalnych aplikacji typu SPA (Single-Page Application) przy wykorzystaniu dodatkowych narzędzi i bibliotek.

Instancja Vue i szablonowanie

W pierwszym etapie naszych rozważań musimy oswoić się ze sposobem powiązania szablonu z warstwą logiki JavaScript. W tym celu omówimy tworzenie instancji Vue() oraz jej podstawowe właściwości. Najpierw jednak, aby móc z niej korzystać, do naszego dokumentu musimy dołączyć kod biblioteki udostępnianej na przykład przez CDN.

HTML

<body>
  <div id="app">
    <p>{{ message }}</p>
    <p>{{ capitalize() }}</p>
    <p>{{ reversedMessage }}</p>
  </div>
</body>

 JavaScript

const app = new Vue({
  el: '#app',
  data: {
    message: 'Hello world',
  },
  methods: {
    capitalize () {
      return this.message.toUpperCase()
    }
  },
  computed: {
    reversedMessage () {
      return this.message.split('').reverse().join('')
    }
  }
})

Zasadniczą czynnością, jaką musimy wykonać na samym początku jest powiązanie obiektu Vue z konkretnym elementem DOM w naszym dokumencie. Instancja Vue musi wiedzieć, w jakim zakresie ma działać i trzeba to wyraźnie zdefiniować. Robimy to poprzez podanie nazwy selectora właściwości el obiektu, którego przekazujemy jako argument konstruktora Vue(). Dzięki temu każde dziecko zdefiniowanego elementu będzie obserwowane i przetwarzane jako wirtualne drzewo DOM (Virtual DOM).

Właściwość data zawiera pola, których będziemy używać do przechowywania naszych danych, methods przetrzymuje definicje metod dostępnych w zasięgu instancji. Metody kryjące się w computed to wartości obliczane dynamicznie. Definiujemy je jako metody, jednakże w momencie ich użycia korzystamy z nich jak ze zwykłego pola (Vue oblicza je automatycznie). Jest to niezwykle przydatne na przykład w sytuacjach, kiedy chcemy zmodyfikować dowolną wartość, ale niewygodne jest wykorzystanie zwykłej metody (na przykład ze względu na dużą ilość zależności).

Ważną kwestią jest sposób odwoływania się do jednej z powyższych właściwości, który odbywa się pośrednictwem referencji this. Jest to możliwe dzięki temu, że Vue łączy wszystkie właściwości z główną instancją obiektu, co daje do nich bezpośredni dostęp.

Warto zwrócić również uwagę na sposób wykorzystania pól i metod w naszym szablonie. Wszystko, co ma zostać wyświetlone w dokumencie, umieszczamy w podwójnych nawiasach klamrowych {{ }}. Trzeba mieć jedynie na uwadze to, że takiej składni możemy używać tylko i wyłącznie w zagnieżdżonych elementach #app.

Dyrektywy

Dyrektywy są specjalnymi atrybutami znajdującymi swoje zastosowanie w szablonach. Poprzedzane są prefiksem v-, na przykład: v-if, v-else, v-for, v-model, v-bind. Warto pamiętać, że wiele z nich posiada aliasy, które mogą być używane zamiennie.

v-if

Pozwala na warunkowe renderowanie elementu DOM, do którego przypisana jest dyrektywa.

HTML

<div id="app">
  <p v-if="isVisible">
    Some paragraph
  </p>
  <button @click="toggle">
    Toggle me!
  </button>
</div>

JavaScript

const app = new Vue({
  el: '#app',
  data: {
    isVisible: true,
  },
  methods: {
    toggle () {
      this.isVisible = !this.isVisible
    }
  },
})

v-on

Pozwala na wiązanie event listenerów do wybranych elementów DOM.

Alias

v-on:click=”someMethod”  ->  @click=”someMethod”

HTML wersja 1 

<div id="app">
  <p v-if="isVisible">Some paragraph</p>
  <button @click="toggle">Toggle me!</button>
</div>

HTML wersja 2

<div id="app">
  <p v-if="isVisible">Some paragraph</p>
  <button v-on:click="isVisible = !isVisible">
    Toggle me!
  </button>
</div>

JavaScript

const app = new Vue({
  el: '#app',
  data: {
    isVisible: true,
  },
  methods: {
    toggle () {
      this.isVisible = !this.isVisible
    }
  },
})

v-for

Pozwala na wykonanie pętli na powiązanym elemencie DOM.

HTML

<div id="app">
  <p v-for="person in people">
    {{ person.name }} is {{ person.age }} years old.
  </p>
</div>

JavaScript

const app = new Vue({
  el: '#app',
  data: {
    people: [
      { name: 'John', age: 10 },
      { name: 'Mark', age: 20 },
      { name: 'Jeff', age: 30 },
    ]
  },
})

Rezultat

v-bind

Pozwala na wiązanie wyrażeń do atrybutów elementów DOM.

Alias

v-bind:title=”someTitle”  ->  :title=”someTitle”

HTML wersja 1 

<div id="app">
  <p v-bind:class="className">Some paragraph</p>
  <button @click="toggle">Toggle color</button>
</div>

HTML wersja 2

<div id="app">
  <p :class="className">Some paragraph</p>
  <button @click="toggle">Toggle color</button>
</div>

JavaScript

const app = new Vue({
  el: '#app',
  data: {
    className: 'red',
  },
  methods: {
    toggle () {
      this.className = this.className == 'red' 
        ? 'blue' 
        : 'red'
    }
  }
})

v-model

Pozwala na wiązanie elementów DOM takich jak na przykład <input> do pól obiektu data.

HTML

<div id="app">
  <input type="text" v-model="inputValue" name="exampleInput">
  <p>{{ inputValue }}</p>
</div>

JavaScript

const app = new Vue({
  el: '#app',
  data: {
    inputValue: ''
  }
})

Komponenty

Komponenty są to instancje Vue, dzięki którym możemy wielokrotnie wykorzystywać te same części kodu w uniwersalny sposób. Unikamy w ten sposób powtarzania się, czyli przestrzegamy zasady DRY (Don’t Repeat Yourself). Dzięki temu aplikacja wygląda czysto i przejrzyście, a w przypadku błędów nie musimy poprawiać ich w wielu miejscach.

HTML

<div id="components-demo">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>

JavaScript 

Vue.component('button-counter', {
  data () {
    return {
      count: 0
    }
  },
  template: `
    <button v-on:click="count++">
      You clicked me {{ count }} times.
    </button>
  `
})

Rezultat

Komponent definiujemy za pomocą Vue.component. Pierwszym argumentem metody jest nazwa elementu DOM, z którego będziemy korzystać w szablonach. Drugi parametr to obiekt, który w tym przypadku posiada dwie właściwości. Najważniejszą z nich jest template, który w efekcie będzie podmieniany w realnym drzewie DOM za każdym razem, kiedy użyjemy komponentu. Drugą właściwością obiektu jest metoda data (metoda, nie obiekt!) zwracająca obiekt z danymi, które będą dostępne w zakresie pojedynczego komponentu.

Komponenty można zagnieżdżać wewnątrz siebie budując jednocześnie bardziej zaawansowane struktury szablonów. W przykładzie poniżej stworzono dwa proste przykłady: parent i child. Wykorzystane tu zostały specjalne tagi <slot></slot>, które stanowią placeholder. W ich miejscu renderowane jest to, co zostało umieszczone w elemencie rodzica.

HTML

<div id="app">
  <parent>
    <child></child>
    <child></child>
    <child></child>
  </parent>
</div>

JavaScript

Vue.component('parent', {
  template: '<div><slot></slot></div>',
})
Vue.component('child', {
  template: `<p>I'm a child!</p>`
})
const app = new Vue({
  el: '#app',
})

Rezultat

Zdarzenia

Z obsługą zdarzeń mieliśmy do czynienia podczas omawiania dyrektywy v-on. Za jej pomocą dodajemy event listener, który będzie nasłuchiwał wystąpienia zdefiniowanego zdarzenia, a po jego wyzwoleniu wykonywał konkretną akcję (na przykład wykonywał metodę). Pójdźmy jednak o krok dalej i zastanówmy się, jak wygląda komunikacja pomiędzy komponentami w kontekście zdarzeń. Możemy wyobrazić sobie prostą sytuację, w której mamy drzewo DOM składające się z elementu rodzica i zagnieżdżonego w nim dziecka. Pytanie brzmi: jak poinformować rodzica z poziomu elementu zagnieżdżonego, że zostało wyzwolone jakieś zdarzenie? Przeanalizujmy poniższy przykład.

HTML

<div id="app">
  <div>
    <example @execute="onExecute"></example>
  </div>
</div>

 JavaScript

Vue.component('example', {
  template: `
    <button @click="execute">
      Execute!
    </button>
  `,
  methods: {
    execute () {
      this.$emit('execute')
    }
  }
})
const app = new Vue({
  el: '#app',
  methods: {
    onExecute () {
      alert('Doing some fancy stuff...')
    }
  }
})

W pierwszej kolejności zwróćmy uwagę na komponent example, którego szablon zawiera zwykły button. Na akcję jego kliknięcia wyzwalana jest metoda execute, która wykonuje kluczową czynność. Na rzecz obiektu komponentu wywoływana jest metoda $emit emitująca zdarzenie, które następnie może być przechwycone przez komponent wyższej warstwy. Pierwszym argumentem metody jest nazwa zdarzenia rodzica @execute. W drugim (lecz w tym przykładzie nieobecnym) możemy podać dane, które chcemy przekazać metodzie wywołanej przez zdarzenie rodzica.

Powyższy sposób jednak nie do końca sprawdza się w momencie, kiedy mamy wiele zagnieżdżonych komponentów, ponieważ chcąc poinformować pierwszy z nich, musimy do każdego dziecka zastosować zabieg przekazywania i emitowania zdarzeń. Łatwo sobie wyobrazić, jak negatywny wpływ będzie miało takie podejście na utrzymanie aplikacji. Jednym z rozwiązań tego problemu jest stworzenie nowej instancji Vue, powiązanie z obiektem globalnym window, a następnie (bazując na mechanizmie łączenia przez Vue wszystkich właściwości obiektu) wykorzystanie metod takich jak $emit (emitujemy zdarzenie) i $on (nasłuchujemy zdarzenia i w funkcji zwrotnej wykonujemy akcję). W ten sposób, nieważne jak bardzo zagnieżdżone są komponenty, możemy obsługiwać zdarzenia w zakresie całej aplikacji.

HTML

<div id="app">
  <div>
    <example></example>
  </div>
</div>

JavaScript 

window.Event = new Vue()
Vue.component('example', {
  template: '<button @click="execute">Execute!</button>',
  methods: {
    execute () {
      Event.$emit('execute')
    }
  }
})
const app = new Vue({
  el: '#app',
  created () {
    Event.$on('execute', () => alert('Doing some fancy stuff...'))
  }
})

Cykl życia

Vue, podobnie jak React czy Angular, posiada cykl życia komponentu i zdarzenia, na które można reagować. Istnieje kilka metod, o których warto pamiętać, ze względu na to, że mają niezwykle przydatne zastosowanie w wielu powszechnych sytuacjach. Przykładem może być ładowanie danych z zewnętrznego API, za pomocą żądań AJAX zaraz po utworzeniu komponentu przez Vue. Najczęściej używanymi zdarzeniami obiektu są: created, mounted, updated.

Zastosowanie w średnich i dużych aplikacjach

Mając omówione podstawowe mechanizmy, możemy poruszyć kwestie związane z rzeczywistymi metodykami budowania aplikacji w Vue. W tym celu zbudujemy prostą aplikację. Z pomocą przyjdzie nam vue-cli - aplikacja konsolowa oparta o node.js, dzięki której będziemy w stanie zarządzać naszą aplikacją z poziomu terminala. Aby zainstalować vue-cli musimy posiadać node.js oraz npm/yarn, a następnie uruchomić jedno z poleceń:

npm install -g @vue/cli

lub

yarn global add @vue/cli

Do stworzenia aplikacji używamy komendy:

vue create [nazwa-projektu]

Polecenie to stworzy projekt z następującym drzewem katalogów i plików:

├── babel.config.js
├── package.json
├── package-lock.json
├── public
│   ├── favicon.ico
│   └── index.html
├── README.md
└── src
    ├── App.vue
    ├── assets
    │   └── logo.png
    ├── components
    │   └── HelloWorld.vue
    └── main.js

Na potrzeby tego wpisu skupimy się tylko na czterech plikach, które będą mieć widoczny wpływ na naszą aplikację (reszta z nich w większości to pliki konfiguracyjne, których nie będziemy omawiać). Zostały one nieco zmienione, aby móc zaprezentować odpowiednie podejście. Są nimi:

  • public/index.html
  • src/main.js
  • src/App.vue
  • src/components/HelloWorld.vue

public/index.html

W domyślnym podejściu każdą aplikację webową zaczynamy od pliku index.* - w tej kwestii nic się nie zmienia. Jedyną rzeczą, na którą musimy zwrócić uwagę jest określenie naszego rdzennego elementu DOM, z którym będziemy wiązać naszą główną instancję Vue.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title>my-awesome-project</title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but my-awesome-project doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

src/main.js

Punkt wyjściowy w kontekście logiki JavaScript. Dzieją się tu trzy zasadnicze rzeczy, na które warto zwrócić uwagę:

  1. Tworzona jest instancja Vue.
  2. Renderowanie komponentu najwyższego poziomu przeniesione zostaje do App.vue.
  3. Instancja Vue zostaje powiązana z elementem #app znajdującym się w pliku index.html.
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App)
}).$mount('#app')

src/App.vue

Aby zrozumieć jak działa kod tego pliku, musimy omówić strukturę tzw. komponentów Single File Components. Według tej koncepcji, jak sama nazwa wskazuje, każdy komponent musi znajdować się w osobnym pliku i posiadać rozszerzenie *.vue. Spójrzmy na poniższy przykład.

<template>
  <div id="app-container">
    <hello-world message="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
  import HelloWorld from './components/HelloWorld.vue'

  export default {
    components: {
      HelloWorld
    }
  }
</script>

<style>
  #app-container {
    text-align: center;
    color: #2c3e50;
    margin-top: 60px;
  }
</style>

Możemy wyróżnić trzy zasadnicze części zawartości pliku.

template
Szablon komponentu. Umieszczamy tu kod, który do tej pory znajdował się we właściwości template.

script
Logika komponentu. Jak widać, nie korzystamy tutaj z Vue.component (jest to definicja komponentu globalnego). Należy pamiętać, że nie możemy się do niego odnosić bez wcześniejszego zaimportowania. Dodatkowo nazwę importowanego komponentu musimy umieścić w polu components obiektu eksportowanego, po to, aby framework odpowiednio przetworzył szablon na rzeczywistą składnię HTML.

style
Style odnoszące się do komponentu. Możemy tutaj korzystać z czystego języka CSS lub użyć jednego ze znanych preprocesorów: Sass, Less czy Stylus. Podczas budowania aplikacji taki kod zostanie odpowiednio skompilowany na przyjazny dla przeglądarki CSS.

src/HelloWorld.vue

Drugi w naszej aplikacji Single File Component. Zwróćmy uwagę na sposób przekazywania właściwości. W App.vue znajduje się własny atrybut message, który dalej w komponencie podrzędnym dodawany jest do tablicy props. Rzeczą godną uwagi jest także atrybut scope w tagu <style>. Zapewnia on hermetyzację styli w obrębie jednego komponentu. W odniesieniu do powyższego przykładu oznacza to, że tylko w komponencie HelloWorld paragrafy będą mieć kolor czerwony.

<template>
  <div class="hello">
    <p>{{ message }}</p>
  </div>
</template>

<script>
  export default {
    props: ['message']
  }
</script>

<style scoped>
  p {
    color: red;
  }
</style>

Dalszy etap tworzenia aplikacji wygląda analogicznie. Tworzymy rozbudowane struktury komponentów tak, aby w efekcie uzyskać pożądany rezultat w postaci pełnej i funkcjonalnej aplikacji. Dostępna jest szeroka gama bibliotek i dodatków dedykowanych dla Vue. Możemy wzbogacić aplikację o Routing (Vue-Router), Vuex (odpowiednik Redux), gotowe komponenty z frameworków CSS (np. Bootstrap, Foundation), biblioteki użytkowe (np. Lodash, Axios) i wiele innych. Możliwości są niemalże nieograniczone.

Co dalej?

W artykule omówiliśmy podstawy tworzenia prostych aplikacji w Vue.js. Poznaliśmy fundamentalne mechanizmy jakie zostały zaimplementowane w frameworku. Wiemy już, jak działa instancja Vue, czym są dyrektywy oraz jak budować komponenty wielokrotnego użytku. Wiemy też, jak podejść do tworzenia aplikacji z wykorzystaniem npm i vue-cli. Zakres omówionych wiadomości jest jednak podstawowy i warto zapoznać się z bardziej zaawansowanymi technikami (zobacz sekcję "Polecane źródła"). Stoi za tym szereg korzyści. Dobra znajomość jednego z nowoczesnych frameworków JavaScript przyspiesza tworzenie warstwy klienta, kod jest lepiej zorganizowany i mniej podatny na błędy, w efekcie czego uzyskujemy atrakcyjne i rozbudowane aplikacje frontendowe.

Polecane źródła

Ogromne źródło wiedzy związane z Vue.js:
https://github.com/vuejs/awesome-vue
Darmowa seria tutoriali będąca doskonałym uzupełnieniem niniejszego artykułu:
https://laracasts.com/series/learn-vue-2-step-by-step
Płatny kurs na Udemy:
https://www.udemy.com/vuejs-2-the-complete-guide

3. Najlepsze praktyki zespołów programistycznych