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/articles.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/article.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.