Node.js: Koa — první aplikace

Dnes se podíváme na zoubek novému frameworku koa. Koa je lehký serverový framework, který používá ECMA6 generátory pro tvorbu middleware. To má několik důsledků, například se v aplikaci nevyskytují nepřehledné callbacky a middleware se chová jako skutečný middleware, tedy může provádět akce před i po předání řízení nižším vrstvám aplikace.

Koa navazuje na koncept použitý v modulu co. Pokud jste zapomněli, o co se jedná, určitě si osvěžte paměť a podívejte se na předchozí článek Zbavte se asynchronních callbacků v Node.js za pomocí generátorů, který vám dá nezbytnou průpravu.

Pro účely tohoto článku nepoužiji existující yeoman generátor, který nainstaluje hned několik užitečných modulů, ale budeme postupovat hezky od začátku, řekneme si, jak celý framework funguje. Později si řekneme, jaké moduly je vhodné použít, proč a jak fungují.

Kompletní zdrojový kód aplikace je na GitHubu, hezky jdoucí po commitech.

Příprava

Protože koa potřebuje vývojovou verzi node.js (minimálně 0.11.9, v době psaní tohoto článku je to verze 0.11.12), nainstalujeme si prvně nástroj n, který umožňuje provozovat více verzí node.js naráz:

# nainstalujeme n globálně
sudo npm install --global n
# nainstalujeme a aktivujeme poslední verzi node
sudo n latest

# takto lze spustit poslední (vývojovou) verzi node bez ovlivnění globálního prostředí
n use `n --latest` --harmony-generators
# případně konkrétní verzi (rovněž bez ovlivnění globálního prostředí)
n use 0.11.12 --harmony-generators

Dále již začneme pracovat na kódu. Obvykle začínám prázdným repozitářem, aby bylo vše hezky verzováno od začátku, a tak nebudu dělat výjimky.
Pokud používáte Linux, následující příkazy vytvoří a připraví nový projekt:

mkdir -p zdrojak/koa-first-app
cd zdrojak/koa-first-app
# vytvoříme prázdný repozitář
git init
# stáhneme předpřipravený soubor .gitignore z GitHubu
curl https://raw.github.com/github/gitignore/master/Node.gitignore -o .gitignore
# Vytvoříme nový prázdný modul, odklikejte enter
npm init
# npm install --save koa

Nyní máme nainstalované moduly, které potřebujeme a můžeme začít vyvíjet. Pro představu vypíšeme nainstalované moduly:

$ npm ls
koa-first-app@0.0.0 zdrojak/koa-first-app
└─┬ koa@0.5.1
  ├─┬ accepts@1.0.1
  │ └── negotiator@0.4.2
  ├── co@3.0.4
  ├─┬ cookies@0.4.0
  │ └── keygrip@1.0.0
  ├── debug@0.7.4
  ├── delegates@0.0.3
  ├── finished@1.1.1
  ├── fresh@0.2.2
  ├── koa-compose@2.2.0
  ├── mime@1.2.11
  └── type-is@1.0.0

Aplikace, kontext, požadavek a odpověď

Nejjednodušší aplikace vypadá takto:

var koa = require('koa');
var app = koa();

app.use(function *() {
    this.body = 'Hello World!';
});

app.listen(process.env.PORT || 3000);

Koa Aplikace je objekt obsahující pole middleware v podobě generátorových funkcí, které jsou v průběhu požadavku složeny a spouštěny od shora dolů a následně zpět.

Instanci aplikace se dá nastavit několik vlastností, například jméno app.name nebo zda se má důvěřovat hlavičkám, které nastavují proxy servery app.proxy. Více v dokumentaci.

Kontext generátoru (hodnota this) je objekt, který šikovně deleguje (kód) metody a vlastnosti požadavku (Request) a odpovědi (Response).

Z požadavku máme přes kontext přístup k těmto vlastnostem a funkcím:

  • this.header (get) – objekt hlaviček ({„host“: „127.0.0.1:3000“, „connection“: „keep-alive“, …})
  • this.method (get, set) – HTTP metoda (GET, POST, PUT…)
  • this.url (get, set) – URL („/search?q=key“)
  • this.path (get, set) – pouze cesta bez query stringu („/search“)
  • this.query (get, set) – objekt obsahující zpracovaný query string ({„q“: „key“})
  • this.querystring (get, set) – část URL za otazníkem („q=key“)
  • this.host (get, set) – HTTP host („localhost:3000“)
  • this.fresh (get) – zda je požadavek nutné zpracovat, podpora cache (hlavičky If-None-Match, ETag, If-Modified-Since, Last-Modified)
  • this.stale (get) – negovaná hodnota this.fresh
  • this.socket (get)
  • this.protocol (get) – „https“ nebo „http“. Podporuje hlavičku X-Forwarded-Proto, pokud je nastaven příznak app.proxy
  • this.secure (get) — zkratka pro this.protocol == "https"
  • this.ip (get) – IP adresa protistrany. Podporuje hlavičku X-Forwarded-For, pokud je nastaven příznak app.proxy
  • this.ips (get) – pole IP adres protistrany (pouze pokud věříme proxy hlavičkám)
  • this.subdomains (get) – pole subdomén v obráceném pořadí "jan.novak.verezny.cz" → ["novak", "jan"]. Vynechávají se domény po řád definovaný hodnotou app.subdomainOffset
  • this.is() – zjištění MIME typu příchozího požadavku (Content-Type)
  • this.accepts() – Podpora domlouvání preferovaného MIME typu odpovědi podle hlavičky Accept (html, json, text, png, …)
  • this.acceptsEncodings() – gzip, defalte, identity
  • this.acceptsCharsets() – zjištění nejvhodnější znakové sady odpovědi
  • this.acceptsLanguages() – zjištění nejvhodnější lokalizace
  • this.get(header) – vrátí hodnotu hlavičky požadavku

A ještě tyto pro odpověď:

  • this.body (set, get) – tělo odpovědi, může být string, Buffer, Stream nebo objekt, který se vrátí jako JSON. null pak znamená žádné tělo a koa nastaví automaticky this.status na 204 (No content)
  • this.status (set, get) – HTTP status kód. (výchozí 200)
  • this.length (set, get) – hlavička Content-Length, určí se podle this.body, pokud není nastaveno
  • this.type (set, get) – Content-Type, dá se nastavit i podle přípony (viz dokumentace)
  • this.headerSent (get) – zda byla již odeslána hlavička
  • this.redirect(url, [alt]) – Provede přesměrování (302). Hodnota „back“ je speciální, použije se hodnota hlavičky Referrer
  • this.attachment([filename]) – Nastaví Content-Disposition na „attachment“ pro signalizaci stahování
  • this.set(header, value) – Nastaví hlavičku odpovědi
  • this.remove(header) – Smaže hlavičku odpovědi
  • this.lastModified (set) – Podpora cache, může být string nebo objekt Date
  • this.etag (set) – Podpora cache, normalizuje uvozovky, pokud je třeba

Je třeba podotknout, že ne všechny vlastnosti jsou plně delegovány, např. this.response.lastModified lze i číst a vrací vždy Date nebo undefined, ale this.lastModified lze pouze zapisovat.

Middleware

Předchozí příklad je docela nezajímavý, pojďme si ho rozšířit o jednoduchý middleware:

var koa = require('koa');
var app = koa();

// x-response-time
app.use(function *xResponseTime(next){
  var start = process.hrtime();
  yield next; // předáme řízení dalšímu middleware
  var diff = process.hrtime(start);
  this.set('X-Response-Time', (diff[0] * 1e3 + diff[1] / 1e6) + 'ms');
});

app.use(function *() {
    this.body = 'Hello World!';
});

app.listen(process.env.PORT || 3000);

Jen pro zajímavost, na mém počítači se hodnota X-Response-Time v ladícím módu pohybuje okolo 0.3 ms, v produkčním módu je to kolem 50 μs. Celý request-response cyklus se podle Google Chrome pohybuje okolo dvou až šesti milisekund. Je zajímavé, že načtení stránky trvá zhruba stejně jak ve vývojovém, tak produkčním módu, u složitější aplikace by tento rozdíl byl jistě znatelně větší.

Tak máme první middleware popsaný generátorem xResponseTime. Nedělá toho zase tak moc, jen změří čas potřebný pro zpracování dalšího middleware pomocí časovače s vysokým rozlišením a nastaví HTTP hlavičku X-Response-Time.

Všimněte si parametru next a způsobu jeho použití. Předání řízení dalšímu middleware se provádí pomocí yield next. Když je tento řádek opomenut, řízení se nepředá dále, ale vybublá zpět. Následující obrázek převzatý z příručky názorně ilustruje předávání řízení mezi middleware:

Koa middleware

Koa middleware

Pokud vás zajímá, jak fungoval middleware v Connect a Express, podívejte se na článek JavaScript na serveru: začínáme programovat e-shop (část Connect)

Na GitHubu je skript development.sh, který při výstupu vypisuje ladící informace o připojení a hlavně průchody jednotlivými middleware se změnou stavu, to vše krásně barevně, vše díky modulu debug:

$ ./development.sh 
  koa:application use xResponseTime +0ms
  koa-session-redis key config is: koa:sess +0ms
  koa-session-redis cookie config all: {} +0ms
  koa-session-redis cookie config overwrite: true +0ms
  koa-session-redis cookie config httpOnly: true +0ms
  koa-session-redis cookie config signed: true +1ms
  koa-session-redis redis config all: {} +0ms
  koa-session-redis redis config port: 6379 +0ms
  koa-session-redis redis config host: 127.0.0.1 +0ms
  koa-session-redis redis config options: {} +0ms
  koa-session-redis redis config db: 0 +0ms
  koa-session-redis redis config ttl: null +0ms
  koa:application use - +8ms
  koa:application use - +0ms
  koa:application listen +1ms
Warning: do not run DEBUG=koa-compose in production
as it will greatly affect the performance of your
application - it is designed for a development
environment only.

  koa-session-redis redis is connecting +11ms
  koa-session-redis redis ready +4ms
  koa-session-redis redis host: 127.0.0.1 +0ms
  koa-session-redis redis port: 6379 +0ms
  koa-session-redis redis parser: javascript +0ms
  0 >> xResponseTime
  status: undefined Not Found
  header:
    x-powered-by: koa
  body: undefined

  1 >> 
  status: undefined Not Found
  header:
    x-powered-by: koa
  body: undefined

  koa-session-redis new session +1.1m
  2 >> 
  status: undefined Not Found
  header:
    x-powered-by: koa
  body: undefined

  2 << 
  status: 200 OK
  header:
    x-powered-by: koa
    content-type: text/plain; charset=utf-8
    content-length: 37
  body: "Hello World!\nClient request count: 1\n"

  koa-session-redis save eyJjb3VudGVyIjoxfQ== +6ms
  1 << 
  status: 200 OK
  header:
    x-powered-by: koa
    content-type: text/plain; charset=utf-8
    content-length: 37
    set-cookie: koa:sess=ltluL9M1sXmMnQJ3TXawFDvF; path=/; httponly,koa:sess.sig=OU_iGiu2oKJ777Onurd0Ary6zZk; path=/; httponly
  body: "Hello World!\nClient request count: 1\n"

  0 << xResponseTime
  status: 200 OK
  header:
    x-powered-by: koa
    content-type: text/plain; charset=utf-8
    content-length: 37
    set-cookie: koa:sess=ltluL9M1sXmMnQJ3TXawFDvF; path=/; httponly,koa:sess.sig=OU_iGiu2oKJ777Onurd0Ary6zZk; path=/; httponly
    x-response-time: 10.923407ms
  body: "Hello World!\nClient request count: 1\n"

Sušenky a sezení

Nyní si představíme způsob, jak naimplementovat session. Session, jak jistě všichni vědí, je stavová informace uložená na straně serveru, komunikovaná v požadavcích přes cookie. Koa obsahuje metody pro práci s cookies, nikoli však se session, ale není to handicap, modulů pro kou na práci se session existuje hned několik.

Napíšeme si jednoduché počítadlo návštěv z jednoho prohlížeče, zatím jen za použití cookie. Předchozí tři řádky kódu vracející „Hello World!“ nahradíme tímto:

app.use(function *(next) {
  // only for homepage
  if (this.path != '/') return next;

  // see https://github.com/jed/cookies#cookiesget-name--options--
  var clientRequestCount = this.cookies.get('counter', { signed: true }) || 1;

  this.body = 'Hello World!\n';
  this.body += 'Client request count: ' + clientRequestCount + '\n';

  clientRequestCount++;

  // see https://github.com/jed/cookies#cookiesset-name--value---options--
  this.cookies.set('counter', clientRequestCount, { signed: true });
});

Ukázali jsme si jak se pracuje s cookies a nyní je čas udělat krok dál a představit si modul koa-session.

npm install --save koa-session

Nyní stačí jen použít:

// Pro pořádek na začátek souboru k ostatním require
var session = require('koa-session');

// chceme měřit i režii session
app.use(xResponseTime);

// koa-session přijímá svůj parametr key,
// zbytek předává metodě ctx.cookie.set
app.use(session({ signed: true }));

// upravená routa
app.use(function *(next) {
  // only for homepage
  if ('/' != this.path) return next;

  // see https://github.com/jed/cookies#cookiesget-name--options--
  var clientRequestCount = (this.session.counter || 0) + 1;
  this.session.counter = clientRequestCount;

  this.body = 'Hello World!\n';
  this.body += 'Client request count: ' + clientRequestCount + '\n';
});

Má to ale háček. Modul koa-session je spíše pouze normativní interface, který má dát ostatním modulům návod, jak implementovat session řadiče. Pro ukládání dat se používá cookie, jediný rozdíl je v tom, že celý session objekt se serializuje jako JSON, překóduje do base64 a uloží do cookie v odpovědi.

Naštěstí existují jiné moduly, které lze použít jako drop-in replacement, samozřejmě kromě konfigurace, která je implementačně závislá. Na session se výborně hodí redis, což je něco jako memcached dotáhnutý do dokonalosti. :-)

Nejprve neinstalujeme koa-session-redis:

npm install --save koa-session-redis

A pak změníme jeden řádek na začátku naší aplikace a máme plnohodnotné úložiště:

var session = require('koa-session-redis');

Konzolový výstup takto upravené aplikace byl uveden zde v článku výše (část o middleware).

Alternativy

Alternativ je mnoho, vybrat vhodnou je obtížnější. Mně účelově připadá rozběhnout redis jako chvilková záležitost a řešení založená na node.js k redisu a nosql mají blízko, možná proto zmíním ještě koa-session-mongo, tento modul poslouží, pokud používáte MongoDB jako úložiště dat.

A co dál

Co dál? No to záleží na účelu. Node.js je perfektní na realtime aplikace a různá (nejen REST) APÍčka sigle-page aplikací, tak bych se chtěl dále ubírat tímto směrem. Myslím, že bude vhodné představit dnešní možnosti a způsoby realtime komunikace na webu (na HTTP vrstvě).

O node.js existuje dnes povědomí hlavně díky front-end vývojářům, kteří javascript používají, pracují s ním denně. Grunt (a nové alternativy jako Gulp), spousta utilitek (uglify-js). Ale to není to hlavní. Chci jeden dobrý jazyk na serveru, na klientovi, v chytré televizi i ledničce. Pojďme propagovat nový JavaScript, který si sice táhne pomyslnou kouli na noze, ale časem se tříbí, přejímá to dobré a inspiruje svět…

Příště bych se proto chtěl zabývat způsoby nasazení node.js aplikací, aby se node.js představila konečně jako aplikační platforma, se kterou se do budoucna musí počítat.

Přes 15 let jsem programoval pro desktop, v posledních několika letech jen pro web. Vyzkoušel jsem si všechny možné programovací jazyky a prostředí od QBasicu, Pascalu, Assembleru (x51, x86), Deplhi (Object Pascal), C++, lehce Javu, dost C# a pak také ty dynamické jazyky jako PHP, Python, Perl. Ochutnal jsem LISP, byl to první interpret, který jsem si implementoval (hned po RPN), narazil jsem i na Haskell… Pak jsem se naučil JavaScript a zamiloval se, i když to chvilku trvalo :-) Ve volném čase se věnuji Node.js a komunitě kolem JavaScriptu. Občas si pro radost něco dám na GitHub

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 10

Přehled komentářů

abtris n versus nvm
Pavel Lang
abtris Re:
Pavel Lang nvm místo n
Petr Nevyhoštěný Výhody oproti jiným
Pavel Lang Re: Výhody oproti jiným
Pavel Lang Re: Výhody oproti jiným
Petr Nevyhoštěný Re: Výhody oproti jiným
Petr Nevyhoštěný Re: Výhody oproti jiným
Pavel Lang
Zdroj: https://www.zdrojak.cz/?p=11574