Přejít k navigační liště

Zdroják » JavaScript » Node.js: Koa — první aplikace

Node.js: Koa — první aplikace

Články JavaScript

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.

Komentáře

Subscribe
Upozornit na
guest
10 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
abtris

Je nejaka vyhoda pouzit n proti podle me rozsirenejsimu nvm?

abtris

Jen je potreba to poustet trochu jinak pomoci:

NODE_ENV=development PORT=3000 DEBUG=* nvm run v0.11.12 --harmony app.js

Petr Nevyhoštěný

Chtěl bych se zeptat, jaké výhody (klidně osobní názor) má koa oproti jiným frameworkům jako třeba Sails, Meteor nebo třeba geddy? Nikdy jsem v koa ani v expressu nic pořádného neudělal a teď objevuju Sails, ale pořád mi koa připadá moc low-level, kdežto třeba právě v Sails mám rozvrženou architekturu, modely, ORM, automatické routování na controllery, a tak dále. Možná je ta výhoda právě v té úplné svobodě. Možná to vyplívá z určité lenosti, ale podle mě je pohodlnější používat nějakou nadstavbu, která může mít třeba právě koa jako takové jádro.

Petr Nevyhoštěný

Díky za rozsáhlou odpověd. Koncept middleware, jak je chápan v connect/express/koa a implementován formou use, je vážně dobře vymyšlen. Jen tak na okraj, nedávno jsem narazil na tohle, což mi přijde docela brutální.

Jinak s těmi novými jazykovými prostředky, takovéto probublávání requestu dolu a zpátky nahoru pomocí yield, to mě vážně zaujalo. A třeba časem přijdou s dalšími novinkami z ECMAScriptu 6, uvidíme.

Se Sails jsem teď začal, protože mě baví Angular a potřebuji jenom backend, který mi vytvořit v Sails přijde velmi pohodlné, protože je tam právě vše předpřipraveno. A začlenení socket.io má taky zajímavé. Protože jestli jsem to správně pochopil, nabízí i klientskou knihovnu a spolu umí vytvořit Real-time REST nebo jak to nazvat.

Meteor se mi taky líbí a jeho propojení serveru s klientem je vážně super (díky za odkaz na Comet). Taky jsem ho zkoušel, a přijde mi docela uzavřený, nic moc k nakonfigurování. Ale nic pořádného jsem v něm nevytvořil.

A co se týče geddy, tak ten jsem bohužel ani nezkoušel, pouze jsem na něj narazil, tak jsem ho uvedl jako příklad. Takže odkazy bych uvést mohl, ale jen bych googlil. Každopádně o Sails, až ho pořádně prozkoumám, bych článek napsat klidně mohl, kdyby byl zájem :-).

Taky mě teď napadlo, že ta provázanost klienta se serverem v Meteoru nám neumožňuje styl napsat si backend a ten používat např. pomocí RESTu na více klientech (browser, iOS, Android, …). Nebo to tak není?

Prostě všechno má svá pro a proti. Každopádně, vím, co jsem potřeboval, díky.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.