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.
Nálepky:
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.freshthis.socket
(get)this.protocol
(get) – „https“ nebo „http“. Podporuje hlavičku X-Forwarded-Proto, pokud je nastaven příznak app.proxythis.secure
(get) — zkratka prothis.protocol == "https"
this.ip
(get) – IP adresa protistrany. Podporuje hlavičku X-Forwarded-For, pokud je nastaven příznak app.proxythis.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.subdomainOffsetthis.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, identitythis.acceptsCharsets()
– zjištění nejvhodnější znakové sady odpovědithis.acceptsLanguages()
– zjištění nejvhodnější lokalizacethis.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í nastavenothis.type
(set, get) – Content-Type, dá se nastavit i podle přípony (viz dokumentace)this.headerSent
(get) – zda byla již odeslána hlavičkathis.redirect(url, [alt])
– Provede přesměrování (302). Hodnota „back“ je speciální, použije se hodnota hlavičky Referrerthis.attachment([filename])
– Nastaví Content-Disposition na „attachment“ pro signalizaci stahováníthis.set(header, value)
– Nastaví hlavičku odpovědithis.remove(header)
– Smaže hlavičku odpovědithis.lastModified
(set) – Podpora cache, může být string nebo objekt Datethis.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:
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.
Je nejaka vyhoda pouzit
n
proti podle me rozsirenejsimunvm
?Na n jsem narazil dříve, pravděpodobně to je jedno.
Jen je potreba to poustet trochu jinak pomoci:
NODE_ENV=development PORT=3000 DEBUG=* nvm run v0.11.12 --harmony app.js
Díky za tip
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.
Dobrá otázka.
Express je vlastně taková nadstavba nad connect (+ velice jednoduchý scaffold, který se časem opět odštěpil). Vše důležité je udržováno v connectu a pokud se middleware ukázalo jako „věc názoru, preference,“ přechází potom do express a tam se opět časem vyštěpí do samostatného mudulu (příkladem třeba může být
connect.multipart()
)Koa je nástupcem expressu a connectu v tomto smyslu. Jeden z hlavních vývojářů (TJ Holowaychuk) je také iniciátorem tohoto projektu.
Koa se nesnaží v současné době nahradit express ani connect (vysvětlení zde), pouze jde dál a používá nové jazykové prostředky, jak tomu je u mladých (nebo přelomových technologií). Porovnejte například .NET 1.1 a 3.5. Proto pořád v perexu to ECMA6 (no, vlastně jen generátory). Snaha je ukázat, že to jde jinak/lépe již se „stávajícími standardy“
Sails je oproti tomu komplexní framework včetně scafoldu, který začátečníkovi nabízí vše co ho napadne.
Comet je oproti tomu úplně jiný přístup k celé aplikaci. Tímto směrem se se ubírá Meteor.
Můj osobní názor je, že se technologie v určitých oblastech bude posouvat směrem k Meteoru, ale netřeba nyní spěchat. Dan Steigerwald (snad to mohu prozradit) se v blízké době chystá psát o Reactu v souvislosti s Polymerem a Angularem, tak se budu těšit…
Musím přiznat, že Meteor mě hodně zaujal jak pozitivně tak i negativně, ale to už je ta věc názoru :-)
Omlovám se, zapomněl jsem reagovat na geddy. Neznám, ale vy padá to zajímavě. Má spoustu použitých modulů a mezi nimi třeba i socket.io.
Můžeš uvést pár odkazů na přiklady? Možná, jestli máš zkušenost, nebylo by marné napsat krátký článek nebo zprávičku! :-)
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.
Tak to je vážně síla :-D Takový kód/modul raději vyloučit na začátku. Jistě to lze vyřešit lépe i bez použití nějakého promise modulu, například pojmenováním funkcí.
chrome://flags/#enable-javascript-harmony
) a tento stav potrvá, dokud nebude ES6 dokončenaDobrá zpráva ale je, že už dne tu je traceur, takže novinky lze používat již nyní ale rychlost a efektivita bude pravděpodobně nižší.
Ano, to je nevýhoda, nevím jakým způsobem to Meteor řeší, ale programátor by to měl řešit segregací modelu a aplikační logiky. Meteor je ještě mladý a neotestovaný a potenciálně nebezpečný pro nezkušeného programátora (vynesení aplikační logiky na veřejnost), jistě není vhodný na veřejně dostupné účetní a bankovní aplikace