Vytvořte svou první aplikaci s Vue.js

Dnes si procvičíme své dovednosti Vue.js vytvořením jednoduché aplikace pro procházení příspěvků na redditu. Celou věc zkonstruujeme od začátku, abychom ukázali, jak snadné je vytvářet uživatelská rozhraní s rámcem, jako je Vue.

Tento tutoriál vyžaduje, abyste měli alespoň nějaké základní znalosti JavaScriptu a Vue.js. Pokud vůbec neznáte Vue.js, doporučujeme vám projít si náš článek 5 praktických příkladů pro výuku Vue.js, kde ukazujeme mnoho základních konceptů s praktickými úryvky kódu.

Aplikace

To, co od naší aplikace chceme, je jednoduše načíst zdroj z řady subbredditů a zobrazit je. Zde je, jak bude konečný výsledek vypadat:

Budeme mít šest samostatných kanálů subreddit, z nichž každý bude obsahovat pět příspěvků. Příspěvky obsahují odkazy na obsah a diskusi na redditu a také některé další podrobnosti. Pro jednoduchost jsme vynechali funkce, jako je přidávání/odebírání subredditů a vyhledávání, ale lze je snadno přidat nad stávající aplikaci.

Nastavení pracovního prostoru

Úplný zdrojový kód aplikace prohlížeče reddit si můžete stáhnout z Stáhnout tlačítko v horní části článku. Než se skutečně podíváme na kód, ujistěte se, že je vše správně nastaveno. Zde je přehled struktury souborů:

Jak vidíte, je to docela jednoduché:máme jen jeden soubor HTML, jeden soubor CSS, script.js obsahující náš JavaScript kód. Přidali jsme také místní kopie knihoven Vue.js a Vue-resource, ale pokud chcete, můžete použít CDN.

Naštěstí Vue.js nevyžaduje žádnou speciální konfiguraci, takže by měl fungovat hned po vybalení. Ke spuštění aplikace stačí vytvořit globální instanci Vue:

new Vue({
    el: '#main'
});

Jediné, co nyní zbývá udělat, je spustit místní webový server, který umožní cross-origin požadavky AJAX na reddit API. Nejjednodušší způsob, jak to udělat v OS X/Ubuntu, je spustit následující příkaz z adresáře projektu:

python -m SimpleHTTPServer 8080

Pokud je vše provedeno správně, náš projekt by měl být dostupný na localhost:8080.

Vytváření vlastních komponent

Naše aplikace bude potřebovat dvě opakovaně použitelné součásti – jednu pro Příspěvky a další pro Subreddits . Tyto dvě komponenty budou ve vztahu Child-Parent, což znamená, že komponenta Subreddit bude mít vnořeno více příspěvků.

Začněme komponentou Subreddit a konkrétněji je to JavaScript:

// Parent | Subreddit component containing a list of 'post' components. 
var subreddit = Vue.component('subreddit',{
    template: '#subreddit',
    props: ['name'],

    data: function () {
        return { posts: [] }
    },

    created: function(){
        this.$http.get("https://www.reddit.com/r/"+ this.name +"/top.json?limit=5")
        .then(function(resp){
            if(typeof resp.data == 'string') {
               resp.data = JSON.parse(resp.data);
            }
            this.posts=resp.data.data.children;
        });
    }
});

Zde definujeme novou komponentu pod názvem subreddit . V props poskytujeme pole se všemi parametry, které může naše komponenta obdržet - v tomto případě je to pouze název subbredditu, který chceme procházet. Nyní, pokud chceme přidat blok subreddit do HTML, použijeme toto označení:

<subreddit name="food"></subreddit>

data vlastnost definuje, jaké proměnné jsou potřebné pro každou instanci komponenty a jejich výchozí hodnoty. Začneme prázdným posts pole a naplňte jej do created metoda. Když <subreddit> je vytvořen tag, Vue si vezme jeho name vlastnost, zavolejte reddit API, abyste načetli prvních 5 příspěvků ze subredditu s tímto názvem a uložili je do this.posts . Pro požadavky HTTP jsme místo jQuery použili knihovnu vue-resource, protože je mnohem menší a automaticky váže správný kontext pro this .

Poté, co v modelu získáme vše, co potřebujeme, Vue.js automaticky vykreslí naše komponenty Subreddit. Skutečný pohled, který uživatel vidí, je definován v šabloně v index.html :

<template id="subreddit">

    <div class="subreddit">
        <h2>{{ name | uppercase }}</h2>

        <ul class="item-list">
            <li v-for="obj in posts">
                <post :item="obj"></post>
            </li>
        </ul>
    </div>

</template>

Osobně se mi líbí zabalit všechny prvky komponenty do div kontejner. Díky tomu je snazší stylizovat a také působí sémanticky (alespoň mně). Uvnitř tohoto kontejneru máme nadpis, který prochází filtrem velkých písmen (filtrům se budeme věnovat později v článku) a neuspořádaným seznamem iterujícím přes prvky vrácené z volání reddit API.

Pokud se podíváte pozorně na HTML, také si všimnete, že používáme <post> štítek. Toto není nějaký nový efektní HTML prvek – je to naše podřízená komponenta!

// Child | Componenet represiting a single post.
var post = Vue.component('post', {
    template: "#post",
    props: ['item'],
    methods: {
        getImageBackgroundCSS: function(img) {
            if(img && img!='self' && img!='nsfw') {
                return 'background-image: url(' + img + ')';    
            }
            else {
                return 'background-image: url(assets/img/placeholder.png)'; 
            }
        }       
    }
});

Komponenty příspěvku budou očekávat objekt nazvaný item obsahující všechny informace o jediném příspěvku na redditu – věci jako název, adresy URL, počet komentářů atd. Jak jsme viděli dříve, provádí se to v v-for smyčka uvnitř komponenty Subreddit (rodič):

<li v-for="obj in posts">
    <post :item="obj"></post>
</li>

Dvojtečka předpona :item="obj" je velmi důležité. Říká Vue, že dokazujeme objekt JavaScript s názvem obj (na rozdíl od řetězce "obj" ), což nám umožňuje předávat data z v-for .

Nyní, když máme všechny potřebné vlastnosti pro příspěvek, můžeme je zobrazit.

<template id="post">

    <div class="post">
        <a   :href="item.data.url" :style="getImageBackgroundCSS(item.data.thumbnail)" 
             target="_blank" class="thumbnail"></a>

        <div class="details">
            <a :href="item.data.url" :title="item.data.title" target="_blank" class="title">
                {{ item.data.title | truncate}}
            </a>          

            <div class="action-buttons">
                <a href="http://reddit.com{{ item.data.permalink }}" title="Vote">
                    <i class="material-icons">thumbs_up_down</i>
                    {{item.data.score}}
                </a>

                <a href="http://reddit.com{{ item.data.permalink }}" title="Go to discussion">
                    <i class="material-icons">forum</i>
                    {{item.data.num_comments}}
                </a>
            </div>
        </div>
    </div>

</template>

Výše uvedená šablona vypadá zpočátku děsivě, ale ve skutečnosti není. Prostě vezmeme vlastnosti objektu post a zobrazíme je.

Vytváření vlastních filtrů

Definování filtrů je poměrně snadné. Vue.filter() metoda nám poskytuje příchozí data řetězce, která můžeme transformovat, jak chceme, a pak je jednoduše vrátit.

uppercase filtr, který jsme zmínili dříve v šabloně subreddit, je jedním z nejjednodušších možných filtrů. Ve skutečnosti byl vestavěný do předchozí verze Vue, ale byl odstraněn ve verzi 2 spolu se všemi ostatními textovými filtry.

Vezme jeden parametr řetězce, převede jej na velká písmena a vrátí výsledek.

Vue.filter('uppercase', function(value) {
    return value.toUpperCase();
});

Náš další filtr vezme řetězce a zkrátí je, pokud jsou příliš dlouhé. To se týká názvů příspěvků, které jsou často příliš dlouhé pro návrh, který jsme měli na mysli.

Vue.filter('truncate', function(value) {
    var length = 60;

    if(value.length <= length) {
        return value;
    }
    else {
        return value.substring(0, length) + '...';            
    }
});

Úplný kód

Níže uvádíme všechny soubory pro aplikaci, abyste si mohli prohlédnout celý kód a získat lepší představu, jak celá věc funguje.

/*-----------------
    Components 
-----------------*/

// Parent | Subreddit component containing a list of 'post' components. 
var subreddit = Vue.component('subreddit',{
    template: '#subreddit',
    props: ['name'],

    data: function () {
        return { posts: [] }
    },

    created: function(){
        this.$http.get("https://www.reddit.com/r/"+ this.name +"/top.json?limit=5")
        .then(function(resp){
            if(typeof resp.data == 'string') {
               resp.data = JSON.parse(resp.data);
            }
            this.posts=resp.data.data.children;
        });
    }
});

// Child | Componenet represiting a single post.
var post = Vue.component('post', {
    template: "#post",
    props: ['item'],
    methods: {
        getImageBackgroundCSS: function(img) {
            if(img && img!='self' && img!='nsfw') {
                return 'background-image: url(' + img + ')';    
            }
            else {
                return 'background-image: url(assets/img/placeholder.png)';   
            }
        }       
    }
});

/*-----------------
   Custom filters 
-----------------*/

// Filter that transform text to uppercase.
Vue.filter('uppercase', function(value) {
    return value.toUpperCase();
});

// Filter for cutting off strings that are too long.
Vue.filter('truncate', function(value) {
    var length = 60;

    if(value.length <= length) {
        return value;
    }
    else {
        return value.substring(0, length) + '...';            
    }
});

/*-----------------
   Initialize app 
-----------------*/

new Vue({
    el: '#main'
});
<!DOCTYPE html>
<html>
<head>
    <title>Vue</title>

    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    <link rel="stylesheet" type="text/css" href="assets/css/styles.css">
</head>
<body>

    <header>
        <div class="header-limiter">
            <h1><a href="https://tutorialzine.com/2016/08/building-your-first-app-with-vue-js/">Building Your First App With <span>Vue.js</span></a></h1>
            <nav>
                <a href="https://tutorialzine.com/2016/08/building-your-first-app-with-vue-js/">Download</a>
            </nav>
        </div>
    </header>

    <div id="main">

        <div class="container">
            <subreddit name="aww"></subreddit>
            <subreddit name="space"></subreddit>
            <subreddit name="gifs"></subreddit>
            <subreddit name="food"></subreddit>
            <subreddit name="comics"></subreddit>
            <subreddit name="sports"></subreddit>
        </div>

    </div>

    <template id="subreddit">

        <div class="subreddit">
            <h2>{{ name | uppercase }}</h2>

            <ul class="item-list">
                <li v-for="obj in posts">
                    <post :item="obj"></post>
                </li>
            </ul>
        </div>

    </template>

    <template id="post">

        <div class="post">
            <a :href="item.data.url" :style="getImageBackgroundCSS(item.data.thumbnail)" target="_blank" class="thumbnail"></a>

            <div class="details">

                <a :href="item.data.url" :title="item.data.title" target="_blank" class="title">
                    {{ item.data.title | truncate}}
                </a>            

                <div class="action-buttons">
                    <a :href="'http://reddit.com' + item.data.permalink " title="Vote">
                        <i class="material-icons">thumbs_up_down</i>
                        {{item.data.score}}
                    </a>

                    <a :href="'http://reddit.com' + item.data.permalink " title="Go to discussion">
                        <i class="material-icons">forum</i>
                        {{item.data.num_comments}}
                    </a>
                </div>

            </div>
        </div>

    </template>

    <script src="assets/js/vue.js"></script>
    <script src="assets/js/vue-resource.min.js"></script>
    <script src="assets/js/script.js"></script>

    <!-- Demo ads. Please ignore and remove. -->
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script src="https://tutorialzine.com/misc/enhance/v3.js" async></script>
</body>
</html>
*{
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

a{
    text-decoration: none;
}

a:hover{
    text-decoration: underline;
}

html{
    font: normal 16px sans-serif;
    color: #333;
    background-color: #f9f9f9;
}

.container{
    padding: 27px 20px;
    margin: 30px auto 50px;
    max-width: 1250px;
    display: flex;
    flex-wrap: wrap;
    flex-direction: row;
    background-color: #fff;
    box-shadow: 0 0 1px #ccc;
}

/* Subreddit component */

.subreddit{
    flex: 0 0 33%;
    min-width: 400px;
    padding: 20px 42px;
}

.subreddit h2{
    font-size: 18px;
    margin-bottom: 10px;
}

.subreddit .item-list{
    border-top: 1px solid #bec9d0;
    padding-top: 20px;
    list-style: none;
}

.subreddit .item-list li{
    margin-bottom: 17px;
}

/* Post component */

.post{
    display: flex;
}

.post .thumbnail{
    display: block;
    flex: 0 0 60px;
    height: 60px;
    background-repeat: no-repeat;
    background-size: cover;
    background-position: center;
    margin-right: 10px;
    border-radius: 4px;
    margin-right: 12px;
}

.post .details{
    display: flex;
    flex-direction: column;
}

.post .details .title{
    font-size: 15px;
    margin-bottom: 3px;
    color: #04477b;
}

.post .details .title:visited{
    color: purple;
}

.post .details .action-buttons a{
    font-size: 11px;
    margin-right: 4px;
    display: inline-block;
    color: #666;
}

.post .details .action-buttons i{
    font-size: 10px;
    margin-right: 1px;
}

@media(max-width: 1250px){

    .container{
        justify-content: center;
        margin: 30px 30px 50px 30px;
    }
}

@media(max-width: 500px){

    .subreddit{
        min-width: 300px;
        padding: 20px 15px;
    }
}

Všimněte si, že po vytvoření našich dvou komponent se celé rozhraní aplikace sníží na:

<div class="container">
    <subreddit name="aww"></subreddit>
    <subreddit name="space"></subreddit>
    <subreddit name="gifs"></subreddit>
    <subreddit name="food"></subreddit>
    <subreddit name="comics"></subreddit>
    <subreddit name="sports"></subreddit>
</div>

Soubor JavaScriptu také není příliš velký a to je jedna z mých oblíbených věcí na Vue. Dělá za nás tolik práce, že nám nakonec zbude velmi čistý a komplexní kus kódu.

Další čtení

Hlavním cílem tohoto tutoriálu bylo ukázat proces vytváření jednoduché aplikace Vue.js. Abychom to zkrátili, nezastavili jsme se, abychom vysvětlili každou drobnou zvláštnost syntaxe, ale nebojte se! Existuje mnoho úžasných zdrojů, kde se můžete naučit základy:

  • Oficiální průvodce Vue.js a dokumenty – zde.
  • Vynikající série videí od Laracasts – zde.
  • Náš vlastní článek:5 praktických příkladů pro výuku Vue.js – zde.

Tímto končí náš výukový program Vue.js! Doufáme, že jste si u toho užili spoustu legrace a že jste se něco nebo dvě naučili. Pokud máte nějaké návrhy nebo dotazy, neváhejte zanechat zprávu v sekci komentářů níže :)