Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 6 - Jednoduchý redakční systém v AngularJS - API článků

V minulé lekci, Jednoduchý redakční systém v AngularJS - Struktura projektu, jsme si připravili projektovou strukturu pro jednoduchý redakční systém.

V dnešní lekci se pustíme do programování CMS modulu, konkrétněji API modelu článků.

Práce s API v AngularJS

Jelikož aplikace v AngularJS je vlastně single-page aplikace v podobě tlustého klienta, stará se v podstatě o vše v rámci své MVC architektury. Snad jedinou výjimku tvoří data. Veškerá data se standardně zpracovávají na straně serveru (kvůli sdílení dat mezi klienty a validaci bez zásahu klienta). Server pro účely získání nebo vložení/úpravy dat vystavuje nějaké API (Application Programming Interface).

V AngularJS poté existuje hned několik možností jak s tímto API komunikovat. Pro naši aplikaci jsem zvolil API v podobě tradičního REST (Representational State Transfer) s komunikačním formátem JSON (JavaScript Object Notation). Pokud tyto zkratky snad neznáte, doporučuji si o této problematice něco více nastudovat, např. v místním povedeném článku Stopařův průvodce REST API :)

Nyní tedy k možnostem komunikace. Základní možností, snad jako všude, je volat přímo jednotlivé HTTP metody daného API. K tomu slouží AngularJS služba $http, která je součástí přímo jeho jádra. Pokud si ale řekneme, že naše API bude víceméně splňovat standard RESTful, můžeme využít službu $resource, jež je součástí modulu ngResource, která nám vše velice usnadní. Vydáme se tedy touto cestou nejmenšího odporu :)

Model

Vytvoříme si službu pro obsluhu API článků pomocí $resource.

app/services/ar­ticles.factory­.js

Naše služba bude vypadat následovně:

'use strict';

/** Model pro práci s články přes API. */
app.factory('Articles', function ($resource) {
    return $resource('/api/articles/:url', {url: '@url'});
});

A to je vše, jdeme domů :) Vážně, nyní umíme obsluhovat celé RESTful API pro články, pomocí standardizovaných metod v rámci $resource. Ale dobrá, pojďme si to ještě trochu vysvětlit.

V první řadě, proč jsme použili factory místo service a jaký je v tom rozdíl? Obojí definuje službu. Ovšem klasická service je již přímo funkcí dané služby, když to factory vytváří a vrací nějaký nový objekt, který onu službu definuje. Ovšem nenechte se zmást, obojí tvoří pouze jednu instanci dané služby, která bude k dispozici přes DI.

V tomto případě tedy konstrukce $resource nad danou URL adresou API vytváří nový objekt, který když vrátíme z factory, bude definován jako služba v naší aplikaci. Chytré, že?

Jinak URL adresa API zde má jeden dynamický parametr, což bude URL adresa článku. Tento formát je právě předepsaný normou RESTful API.

Dobrá, máme tedy vysvětleno, ale pořád tu zbývá jeden problém... To API přeci neexistuje! :-`

Zde začíná ta těžká část, protože ho musíte implementovat. Můžete si v podstatě vybrat kterýkoliv programovací jazyk, nastavit webový server a hurá do práce!

Pokud se vám ale nechce teď odcházet k serverové implementaci, existuje ještě jedna varianta, podporovaná přímo v AngularJS. Můžeme si serverové API naší aplikace nasimulovat v rámci lokální aplikace! Co to znamená? API budeme muset stále implementovat, ale uděláme to jako součást naší klientské aplikace, kde následně využijeme modul ngMockE2E, který nám umožní přemapovat API dotazy na volání lokální JS implementace. Této metodiky se v praxi využívá hlavně pro testování, ale občas i pokud např. API není ještě hotové, ale vy s ním v aplikaci potřebujete pracovat. Tak jdeme na to! :)

Simulace API

Jak jsem popsal výše, musíme v podstatě vytvořit implementaci podobnou té serverové, akorát v JS a následně ji přemapovat v rámci AngularJS. Tato implementace samotná tedy nemá s AngularJS potažmo nic společného, takže pokud nemáte úplně rádi čistý JS, připomínám, že ji můžete klidně udělat přímo na serveru ve vašem oblíbeném jazyce.

Já tedy budu předpokládat, že JS umíte dobře a nebudu se zdržovat zbytečným vysvětlováním. Případně si můžete odskočit k místním kurzům JavaScriptu.

Adresářová struktura API

Nejdříve si vytvoříme bokem adresářovou strukturu pro naše API:

  • api/
    • entities/
      • article.js - Entita reprezentující článek.
    • model/
      • article-model.js - Model pro práci s články.
    • api.js - Zde umístíme mapování API v rámci naší AngularJS aplikace.

Nezapomeňte pak všechno hezky nalinkovat do index.html:

...
<!-- Simulace serverového API. -->
<script src="api/api.js"></script>
<script src="api/entities/article.js"></script>
<script src="api/models/article-model.js"></script>
...

api/entities/ar­ticle.js

Logicky jsem si vybral OOP implementační přístup, tj. pomocí entit reprezentujících data a jejich ukládání do paměti. Začneme tedy s entitou reprezentující článek:

'use strict';

/**
 * @typedef {Object} ArticleValues
 * @property {string} url - Unikátní URL adresa článku.
 * @property {string} [title] - Titulek článku.
 * @property {string} [content] - Text (obsah) článku.
 * @property {string} [description] - Krátký popis článku.
 */

/**
 * Reprezentuje datové záznamy článků v redakčním systému.
 * @constructor
 * @param {ArticleValues} values - Hodnoty záznamu článku.
 * @implements {ArticleValues}
 * @throws {Error} Jestliže hodnoty záznamu článku nejsou validní.
 */
function Article(values) {
    // Data článku.
    var data = {
        url: null,
        title: '',
        content: '',
        description: ''
    };

    // Definuj data článku jako jeho vlastnosti.
    Object.keys(data).forEach((function (key) {
        Object.defineProperty(this, key, {
            get: function () { return data[key]; },
            set: function (value) {
                if (typeof value !== 'string') throw new TypeError('Pole ' + key + ' musí být typu string!');
                if (!value) throw new Error('Pole ' + key + ' nemůže být prázdné!');
                data[key] = value;
            },
            enumerable: true
        });
    }).bind(this));

    // Inicializace povinné URL.
    this.url = values.url;

    // Automatická inicializace dalších nepovinných hodnot.
    for (var key in this)
        if (this.hasOwnProperty(key) && values.hasOwnProperty(key))
            this[key] = values[key];
}

Někoho možná trochu překvapí typová notace JS dokumentace. Používám zde JSDoc.

api/models/article-model.js

Dále vytvoříme model pro správu článků, hlavně jejich ukládání. Nejdříve jsem si říkal, že to udělám pouze do paměti, tzn při obnovení stránky by se vše vynulovalo. Poté jsem se však rozhodl využít persistentní úložiště LocalStorage na straně klienta, když už chceme psát ty moderní webové aplikace. Tento model, pracující s lokálním úložištěm, bude vypadat nějak takto:

'use strict';

/**
 * Model poskytující metody pro správu článků v redakčním systému.
 * @constructor
 */
function ArticleModel() {
    /**
     * Vrátí seznam článků.
     * @returns {Article[]} - Seznam článků.
     */
    this.getArticles = function () {
        var articles = [];
        for (var key in localStorage) if (localStorage.hasOwnProperty(key))
            articles.push(this.getArticle(key));
        return articles;
    };

    /**
     * Vrátí článek podle jeho URL.
     * @param {string} url - URL článku.
     * @returns {null|Article} Článek s danou URL, nebo null, pokud takový neexistuje.
     */
    this.getArticle = function (url) { return JSON.parse(localStorage.getItem(url)); };

    /**
     * Uloží článek. Pokud již existuje článek s danou URL provede editaci.
     * @param {Article} article - Článek.
     */
    this.saveArticle = function (article) { localStorage.setItem(article.url, JSON.stringify(article)); };

    /**
     * Odstraní článek.
     * @param {string} url - URL článku.
     */
    this.removeArticle = function (url) { localStorage.removeItem(url); }
}

api/api.js

Poslední část API tvoří samotné mapování dotazů na výše implementované funkce pomocí knihovního modulu ngMockE2E:

'use strict';

/** Konfigurace simulace serverového API. */
app.run(function ($httpBackend) {

    // Model pro práci s články.
    var articleModel = new ArticleModel();

    // Výchozí URL adresa simulovaného API.
    var apiUrl = '/api';

    // Adresa simulovaného API pro správu článků.
    var articlesApiUrl = apiUrl + '/articles';

    // Regex výraz pro dynamický parametr :url v adrese simulovaného API správy článků.
    var articleApiUrlRegex = new RegExp(articlesApiUrl.replace(/\//g, '\/') + '\/\\w+');

    // Předem připravené výchozí články.
    var defaultArticles = [
        new Article({
            url: 'uvod',
            title: 'Úvod',
            content: '<p>Vítejte na našem webu!</p>\r\n\r\n<p>Tento web je postaven na <strong>jednoduchém redakčním systému v AngularJS frameworku</strong>.</p>',
            description: 'Úvodní článek na webu v AngularJS'
        }),
        new Article({
            url: 'chyba',
            title: 'Stránka nebyla nalezena',
            content: '<p>Litujeme, ale požadovaná stránka nebyla nalezena. Zkontrolujte prosím URL adresu.</p>',
            description: 'Stránka nebyla nalezena.'
        })
    ];

    // Inicializuje výchozí články, pokud neexistují.
    for (var i = 0; i < defaultArticles.length; ++i)
        if (!articleModel.getArticle(defaultArticles[i].url))
            articleModel.saveArticle(defaultArticles[i]);

    // Mapování URL adres API na lokální data.

    // GET "/api/articles" vrací seznam všech aktuálních článků.
    $httpBackend.whenGET(articlesApiUrl).respond(function () {
        return [200, articleModel.getArticles()];
    });

    // GET "/api/articles/:url" vrací článek s danou URL.
    $httpBackend.whenGET(articleApiUrlRegex).respond(function (method, url) {
        var article = articleModel.getArticle(url.split('/')[3]);
        return (article ? [200, article] : [404, {}, {}, 'Článek nenalezen!']);
    });

    // POST "/api/articles/:url" uloží článek článek s danou URL.
    $httpBackend.whenPOST(articleApiUrlRegex).respond(function (method, url, data) {
        try {
            var article = new Article(angular.fromJson(data));
            articleModel.saveArticle(article);
            return [201, article, {Location: '/' + article.url}];
        } catch (error) {
            if (error instanceof Error) return [400, {}, {}, error.message];
            console.error(error);
            return [500];
        }
    });

    // DELETE "/api/articles/:url" smaže článek s danou URL.
    $httpBackend.whenDELETE(articleApiUrlRegex).respond(function (method, url) {
        articleModel.removeArticle(url.split('/')[3]);
        return [204];
    });

    // POST "/api/contact-messages" zpracuje poslanou kontaktní zprávu.
    $httpBackend.whenPOST(apiUrl + '/contact-messages').respond(function (method, url, data) {
        var message = angular.fromJson(data);
        if (parseInt(message.year) !== (new Date()).getFullYear()) return [400, {}, {}, 'Chybně vyplněný antispam!'];
        alert('Odesílatel: ' + message.email + '\n' + message.content);
        return [201, message];
    });

    // Požadavky na soubory šablon naší aplikace necháme projít.
    $httpBackend.whenGET(/templates\//).passThrough();
});

Myslím, že kód je dobře zdokumentován sám o sobě a pokud rozumíte HTTP, není na něm nic složitého.

Jen poznámka, implementoval jsem zde rovnou i API pro odesílání zpráv kontaktního formuláře, který budeme implementovat později v rámci našeho kurzu. To abychom se k API nemuseli zbytečně vracet a upravovat jej.

To je z dnešní lekce vše. Myslím, že to i bohatě stačí.

V lekci příští, Jednoduchý redakční systém v AngularJS - Výpis článku, se budeme věnovat kontrolerům a šablonám.


 

Předchozí článek
Jednoduchý redakční systém v AngularJS - Struktura projektu
Všechny články v sekci
AngularJS
Přeskočit článek
(nedoporučujeme)
Jednoduchý redakční systém v AngularJS - Výpis článku
Článek pro vás napsal Jindřich Máca
Avatar
Uživatelské hodnocení:
Ještě nikdo nehodnotil, buď první!
Autor se věnuje převážně webovým technologiím, ale má velkou zálibu ve všem vědeckém, nejen ze světa IT. :-)
Aktivity