Happstack: část druhá

V první části jsme si Happstack zběžně představili, nainstalovali a napsali si triviální program. Abychom si mohli předvést něco složitějšího, musíme se nejprve podívat na zpracování URL a na různé způsoby prezentace HTML kódu.
Seriál: Happstack: Webový framework v Haskellu (3 díly)
- Happstack: část první 19. 5. 2010
- Happstack: část druhá 26. 5. 2010
- Happstack: část třetí 2. 6. 2010
Nálepky:
Jádrem programů napsaných v Haskellu jsou typy. Ani Happstack není
výjimkou. Jak jsme si ukázali minule, funkce simpleHTTP
,
pomocí které se dá jednoduše v prohlížeči vypsat text, jako jeden ze
dvou parametrů požaduje parametr typu ServerPartT IO a
(zkráceně ServerPart a
), což jsme si označili jako
aplikační kód. Typ IO
značí, že se jedná o vstupně-výstupní
akci a typ ServerPartT
představuje součást webového
serveru. Tento pojem v sobě zahrnuje HTTP požadavky (typ Request
)
a odpovědi (typ Response
). Použití jedné nebo více
součástí dává dohromady celý Happstack server, což je vlastně obyčejná
serverová aplikace.
Pod tímto, na první pohled neintuitivním, konceptem si lze představit
aplikaci v Happstacku jako jednu komplexní funkci, která na základě
požadavku a dalších případných okolnostech vrátí odpověď. Hlavní součást
HTTP požadavku je URL, jenž je v Happstacku vyhodnocováno pomocí
monadických funkcí.
URL monády
Typický případ použití funkcí pro zpracování URL v Happstacku je
jejich zapsání do seznamu a jejich následné sloučení pomocí funkce msum
.
Jestliže jste nikdy o této funkci neslyšeli, nalézá se v modulu Control.Monad
a funguje podobně jako standardní seznamová funkce concat
,
jenom pracuje s monádami. Výsledkem její aplikace bude funkce, která
postupně prochází jednotlivé položky seznamu. V případě, že některá
položka seznamu bude vyhovovat HTTP požadavku, zpracuje se a procházení
skončí; v opačném případě se bude pokračovat v procházení seznamu dále,
dokud některá položka nebude vyhovovat. Pokud se žádná vyhovující
položka v seznamu nenalezne, zobrazí se výchozí stránka s chybou
404 (nenalezeno). Dobrým nápadem je si vždy na posledním místě
seznamu definovat vlastní chybovou stránku, jenž nahradí výchozí.
Základní funkce pro zpracování URL jsou nullDir
a dir
.
První uspěje, pokud je zadaná adresa prázdná (jedná se tedy o kořenový
adresář), druhá, pokud zadaná část adresy odpovídá řetězci. Jejich
použití je bohužel trochu nekonzistentní, jak ukazuje příklad:
module Main where import Control.Monad import Happstack.Server main :: IO () main = simpleHTTP nullConf handleURL handleURL :: ServerPart Response handleURL = msum [ do nullDir; ok $ toResponse "Výchozí stránka" , dir "prvni" . ok $ toResponse "První stránka" , dir "druhy" . ok $ toResponse "Druhá stránka" , dir "treti" $ msum [ do nullDir; ok $ toResponse "Třetí stránka" , dir "ctvrty" . ok $ toResponse "Čtvrtá stránka" ] , notFound $ toResponse "Chyba 404" ]
Námi definovaná funkce handleURL
vrací typ Response
,
tedy HTTP odpověď, proto je na všechny řetězce volána funkce toResponse
,
která je převede na odpovídající typ. Funkce ok
je
speciálním případem funkce return
, ale chová se úplně
stejně — naše odpovědí obalí typovým konstruktorem IO
. Jak
můžeme vidět u parametru treti
, součásti URL lze zanořovat.
V našem případě se tak v prohlížeči po zadání adresy http://localhost:8000/treti/
vypíše text „Třetí stránka“ a po zadání adresy http://localhost:8000/treti/ctvrty/
text „Čtvrtá stránka“. Poslední řádek definuje pomocí funkce notFound
chybovou stránku.
Funkce dir
zpracovává adresy bez ohledu na to, zda-li
končí lomítkem nebo ne. Doporučuji toto chování změnit na přesměrování
na verzi s lomítkem, aby byla každá adresa stránky unikátní. Provedeme
to zapsáním následujících řádek do souboru .htaccess
:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([a-z0-9/_-]+[^/])$ $1/ [R=301,NC]
Funkce dir
zachytí pouze statické části adresy stránek.
Pro zpracování dynamických částí slouží funkce path
. Ta
jako parametr očekává funkci zpracovávající řetězec nebo číslo a
vracející odpověď. Typu této funkce určuje, co se bude v adrese
očekávat. Následující kód vypíše číslo zadané v URL:
handleURL :: ServerPart Response handleURL = msum [ do nullDir; ok $ toResponse "Výchozí stránka" , path pathNumber , notFound $ toResponse "Chyba 404" ] pathNumber :: Int -> ServerPart Response pathNumber n = ok . toResponse $ "Číslo v URL: " ++ show n
Po navštívení kupříkladu adresy http://localhost:8000/42/
se vypíše odpověď „Číslo v URL: 42“. Funkce pro zpracování URL můžeme
mezi sebou libovolně kombinovat, jenom si vždy musíme dávat pozor,
abychom vraceli hodnotu správného typu.
Kromě části URL můžeme určovat i jiné parametry dotazu. Kupříkladu
funkce methodOnly
určuje, kterou HTTP metodu od dotazu
očekáváme. Protože se taktéž jedná o monadickou funkci, kombinuje se
pomocí operátoru >>
:
handleURL :: ServerPart Response handleURL = msum [ do methodOnly GET >> nullDir; ok $ toResponse "Výchozí stránka" , methodOnly GET >> path pathNumber , notFound $ toResponse "Chyba 404" ]
V tomto kódu vracíme na všechny dotazy využívající jiné metody než
GET (tedy např. POST) chybovou stránku 404.
Psaní HTML kódu
Happstack přináší tři různé způsoby zadávání HTML kódu aplikace:
přímé zadávání do řetězců, generování pomocí kombinátorů a šablony.
První způsob je nejjednodušší, ale není příliš vhodný pro netriviální
aplikace, protože pak náš kód nebývá příliš přehledný a ani bezpečný.
Kombinátorový generátor je údajně o něco rychlejší než šablonovací
systém, ten ovšem nabízí větší kontrolu nad kódem a práce s ním je
přirozenější.
pomocí příkazu
cabal install happstack-helpers
, protožeobsahuje navíc instance funkcí pro generování HTML kódu.
HTML řetězce
Aby se odlišil obyčejný text od HTML, modul Text.Html
zavádí nový datový typ HtmlString
. Když funkci toResponse
předáme obyčejný řetězec typu String
, je automaticky
escapován — některé speciální znaky jsou převedeny na HTML entity.
Musíme tedy převést typ String
na HtmlString
:
module Main where import Happstack.Server import Happstack.Helpers import Text.Html hiding (HtmlString) main :: IO () main = simpleHTTP nullConf . ok $ toResponse ref ref :: HtmlString ref = HtmlString "<p><a href="http://zdrojak.cz/">Zdroják</a></p>"
Konstruktor HtmlString
převede řetězec na HTML řetězec, v
prohlížeči se tedy zobrazí odkaz. Kdybychom chtěli k řetězci před jeho
převodem na HTML řetězec přiřetězit nějaký vstup, je nutné použít funkci
s relativně dlouhým názvem stringToHtmlString
, jinak
nedojde k jeho escapování a my bychom si v aplikaci vytvořili
potenciální bezpečnostní díru:
ref :: HtmlString ref = HtmlString $ "<p><a href="http://zdrojak.cz/">" ++ stringToHtmlString "<Zdroják>" ++ "</a></p>"
Generování HTML značek
Po nahlédnutí do dokumentace
modulu Text.Html
uvidíme velké množství funkcí se stejným
názvem jako jsou některé HTML značky. To jsou již zmiňované kombinátory.
S jejich použitím a kombinací operátorů <<
(vložení
dovnitř prvku), +++
(přiřetězení dalšího prvku) a !
(modifikace atributu) si HTML kód lehce vygenerujeme:
ref :: Html ref = header << thetitle << "Titulek" +++ body << p << anchor ! [href "http://zdrojak.cz/"] << "<Zdroják>"
Řetězce se automaticky escapují, takže by nemělo dojít k vložení
nějakého zákeřného kódu zvenku. Jak už název napovídá, kombinátory jsou
funkce, jež lze lehce kombinovat a spojovat dohromady, takže při dobrém
návrhu nebývá problém s opakováním kódu.
HTML šablony
Šablony mají tu výhodu, že je oddělen HTML kód od aplikačního. Lehce
tak můžeme zadat vytvoření HTML kódu někomu úplně jinému a pouze mu
poskytnout seznam proměnných, které v šablonách chceme použít. Haskell
obsahuje několik šablonovacích systémů, my se podíváme na HStringTemplate
(modul Text.StringTemplate).
Tento systém je založen na knihovně Java StringTemplate.
Nejprve si založíme adresář templates
a vytvoříme v něm
hlavní šablonu body.st
:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> <html> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <title>$ title $</title> </head> <body> $ ref() $ </body>
Tato šablona obsahuje proměnnou title
a bude inkludovat
šablonu ref.st
, kterou umístíme taktéž do adresáře templates
:
<a href="http://zdrojak.cz/">$ link $</a>
Proměnné se v šabloně vyznačují pomocí dvou dolarových symbolů,
inkluze další šablony stejně, jenom se za identifikátor dopíšou závorky.
Kdybychom chtěli do šablony přidat komentář, stačí ho napsat mezi
dvojici znaků $!
a !$
. Zbývá už jen napsat
nový kód aplikace:
module Main where import Happstack.Server import Happstack.Helpers import Control.Monad.Trans import Text.StringTemplate import Text.StringTemplate.Helpers import Text.Html hiding (HtmlString) main :: IO () main = simpleHTTP nullConf ref escapeKeys :: [(String, String)] -> [(String, String)] escapeKeys = map ((x, y) -> (x, stringToHtmlString y)) ref :: ServerPart Response ref = liftIO $ do templates <- directoryGroup "templates" let keys = escapeKeys [("title", "Titulek"), ("link", "<Zdroják>")] body = renderTemplateGroup templates keys "body" return . toResponse $ HtmlString body
Tento kód je o trochu složitější. V první řadě je zde potřeba funkce liftIO
z modulu Control.Monad.Trans,
jež zařídí převod z monády IO
na jinou. Ve funkci ref
jsou totiž načítány šablony, proto se celá funkce snaží zabalit do
monády IO
. To je taktéž důvod, proč musíme použít obecnější
funkci return
namísto funkce ok
. Dále se zde
volá funkce directoryGroup
sloužící k načtení adresáře s
šablonami. Funkce renderTemplateGroup
vygeneruje šablonu
spolu s proměnnými v proměnné keys
a zajistí i inkluzi.
Protože šablonový systém automaticky neescapuje proměnné, bylo potřeba
sem připsat vlastní funkci escapeKeys
, která zajišťuje
ošetření vstupu.
Související odkazy
- Oficiální web Haskellu
- Happstack Tutorial
- Dokumentace modulů Happstack.Server,
Text.Html
a Text.StringTemplate
V následujícím dílu si vyzkoušíme zpracovávání formulářů.
Dalšími možnostmi, jak generovat HTML je použít Hamlet nebo Heist.