Картинка для привлечения внимания, clckwrks — веб-фреймворк, тесно связанный с Happstack.
Happstack — веб-фреймворк с большими возможностями и богатым API, который развивался на протяжении последних семи лет, чтобы соответствовать нуждам повседневной веб-разработки. К сожалению, богатый и гибкий API может быть бесполезным и запутывающим, когда вам нужно что-то простое. Однако многие и не догадываются, что под крылом Happstack кроется очень элегантный и простой в использовании веб-фреймворк Happstack Lite.
Предисловие
Happstack Lite представляет из себя простую в своей структуре и легкую в использовании версию Happstack. Для его создания разработчики:
- Собрали все основные типы и функции, которые вам нужны для разработки веб-приложения, в единственном модуле
Happstack.Lite
, так что вам не нужно рыскать по модулям в поисках того, что вам нужно. - Дали функциям намного более простые сигнатуры, исключив монадные трансформеры и избавившись от большинства классов типов.
- Создали этот туториал, который в менее чем 2000 словах описывает все основные вещи, которые вам нужно знать, чтобы начать писать веб-приложение.
Но самое главное — Happstack Lite почти полностью совместим с Happstack! Если вы разрабатываете приложение на Happstack Lite, и вам нужна продвинутая возможность из Happstack, вы можете просто-напросто импортировать соответствующий модуль и использовать его.
Чтобы перевести проект с Happstack Lite на обычный, вам понадобится внести всего лишь 4 небольших изменения:
import Happstack.Lite
заменить наimport Happstack.Server
serve Nothing
заменить наsimpleHTTP nullConf
- добавить
import Control.Monad (msum)
- добавить явный вызов
decodeBody
(подробности)
В то время как Happstack Lite легковесен по сравнению с обычным Happtsack, он по-прежнему является полнофункциональным фреймворком наряду с другими веб-фреймворками на Хаскеле.
В целях упрощения разработчики отказались от использования некоторых продвинутых библиотек, которые работают с Happstack. Если вы заинтересованы в фреймворке с типобезопасными URL, типобезопасными формами, HTML-синтаксисом в литералах и многим другим, то возможно вам стоит рассмотреть Happstack Foundation. Кривая обучения выше, но дополнительная надежность стоит того. Поскольку эти библиотеки построены поверх ядра Happstack, то изученный в данном туториале материал пригодится и при их применении.
Для более глубокого ознакомления вы можете прочитать Happstack Crash Course (который я тоже переведу, если будет проявлен интерес к этой статье — прим. пер.)
Запуск сервера
Для начала нам понадобится пара расширений языка:
{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-}
Теперь подключим некоторые библиотеки:
module Main where
import Control.Applicative ((<$>), optional)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text.Lazy (unpack)
import Happstack.Lite
import Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label)
import Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value)
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A
Чтобы запустить приложение, мы вызываем функцию serve
. Первый аргумент — конфигурация, она опциональна. Второй аргумент — наше, непосредственно, веб-приложение.
main :: IO ()
main = serve Nothing myApp
Веб-приложение имеет тип ServerPart Response
. Вы можете считать ServerPart
веб-эквивалентом монады IO
.
(По умолчанию используется порт 8000, то есть увидеть ваше приложение вы можете по адресу http://localhost:8000/ — прим. пер.)
Статичные адреса
Вот и наше веб-приложение:
myApp :: ServerPart Response
myApp = msum
[ dir "echo" $ echo
, dir "query" $ queryParams
, dir "form" $ formPage
, dir "fortune" $ fortune
, dir "files" $ fileServing
, dir "upload" $ upload
, homePage
]
В самом общем виде наше приложение — просто несколько обработчиков, поставленных в соответствие статичным адресам.
dir
используется, чтобы обработчик выполнялся только при успешном сопоставлении статичных компонентов пути. Например, dir "echo"
успешно сработает с адресом http://localhost:8000/echo
. Чтобы назначить обработчик для адреса "/foo/bar"
, достаточно просто написать dir "foo" $ dir "bar" $ handler
.
Выполняется попытка последовательно применить каждый обработчик, до тех пор пока один из них не вернет реузльтат успешно. В данном случае — Response
.
Мы преобразуем список обработчиков в один единственный с помощью msum
.
Последний обработчик — homePage
— ничем не ограничен (к нему не применяется dir — прим. пер.), поэтому он всегда будет вызван, если ни один из других обработчиков не сработает успешно.
HTML-шаблоны
Поскольку создаем мы веб-приложение, то нам понадобится создавать HTML-страницы. Мы можем делать это, используя Blaze, по которому тоже есть туториал.
Тема шаблонизации HTML вызывает масштабные разногласия в сообществе. Ни одна шаблонная система не может удовлетворить всех, так что Happstack поддерживает множество разных систем. В данном туториале применяется Blaze, потому что он поддерживается и базируется на чисто функциональных комбинаторах. Если вам нравятся шаблоны времени компиляции, но вы желаете HTML-синтаксис, можете рассмотреть HSP. Если вы негативно относитесь к шаблонам в своем коде и предпочитаете внешние XML-файлы, рассмотрите Heist.
Удобно иметь шаблонную функцию, которая сочетает в себе общие элементы для всех страниц веб-приложения, такие как импорт CSS, внешние JS-файлы, меню и т. д. В данном туториале мы будем использовать очень простой шаблон:
template :: Text -> Html -> Response
template title body = toResponse $
H.html $ do
H.head $ do
H.title (toHtml title)
H.body $ do
body
p $ a ! href "/" $ "На главную"
Тогда главная страница выглядит вот так:
homePage :: ServerPart Response
homePage =
ok $ template "Главная страница" $ do
H.h1 "Привет!"
H.p "Писать приложения на Happstack Lite быстро и просто!"
H.p "Зацени эти крутые приложения:"
H.p $ a ! href "/echo/secret%20message" $ "Эхо"
H.p $ a ! href "/query?foo=bar" $ "Параметры запроса"
H.p $ a ! href "/form" $ "Обработка формы"
H.p $ a ! href "/fortune" $ "Печеньки-предсказания (куки)"
H.p $ a ! href "/files" $ "Доступ к файлам"
H.p $ a ! href "/upload" $ "Размещение файлов"
Функция ok
устанавливает для страницы HTTP-код «200 OK». Есть и другие вспомогательные функции, например notFound
устанавливает код «404 Not Found», seeOther
— «303 See Other». Чтобы установить HTTP-код числом, используется setResponseCode
.
Динамические части адреса
Функция dir
выполняет сопоставление только со статичной частью адреса. Мы можем использовать функцию path
, чтобы извлечь значение из динамической части адреса и опционально сконвертировать его в некий тип, такой как Integer
. В данном примере мы просто выводим на экран динамическую часть пути. Для проверки посетите http://localhost:8000/echo/fantastic
echo :: ServerPart Response
echo =
path $ (msg :: String) ->
ok $ template "Эхо" $ do
p $ "Динамическая часть адреса: " >> toHtml msg
p "Измени адрес страницы, чтобы вывести на экран что-то иное."
Параметры запроса
Мы также можем получить значения строковых параметров запроса. Строка запроса — это часть адреса, которая выглядит как "?foo=bar". Попробуйте посетить http://localhost:8000/query?foo=bar
queryParams :: ServerPart Response
queryParams =
do mFoo <- optional $ lookText "foo"
ok $ template "Параметры запроса" $ do
p $ "foo = " >> toHtml (show mFoo)
p $ "Измени адрес страницы, чтобы установить другое значение foo."
В случае, если параметр запроса не установлен, функция lookText
вернет mzero
. В данном примере мы используем optional
из модуля Control.Applicative
, так что в итоге получаем значение типа Maybe
.
Формы
Мы можем использовать lookText
и для получения данных с форм.
formPage :: ServerPart Response
formPage = msum [ viewForm, processForm ]
where
viewForm :: ServerPart Response
viewForm =
do method GET
ok $ template "form" $
form ! action "/form" ! enctype "multipart/form-data" ! A.method "POST" $ do
label ! A.for "msg" $ "Напиши что-нибудь умное"
input ! type_ "text" ! A.id "msg" ! name "msg"
input ! type_ "submit" ! value "Отправить"
processForm :: ServerPart Response
processForm =
do method POST
msg <- lookText "msg"
ok $ template "form" $ do
H.p "Ты написал:"
H.p (toHtml msg)
Мы используем ту же функцию lookText
, что и в предыдущем параграфе, чтобы получить данные из формы. Вы также могли заметить, что мы используем функцию method
, чтобы различать GET
и POST
запросы.
Когда пользователь просматривает форму, браузер запрашивает страницу /form
с помощью GET
. В HTML-теге form
в качестве действия по нажатию кнопки мы указали открытие этой же страницы, но с помощью аттрибута выбрали метод POST
.
Печеньки! (HTTP-cookies)
Данный пример расширяет пример с формой, сохраняя сообщение в куки. Это значит, пользователь может покинуть страницу, а когда вернется назад — страница будет помнить сохраненное сообщение.
fortune :: ServerPart Response
fortune = msum [ viewFortune, updateFortune ]
where
viewFortune :: ServerPart Response
viewFortune =
do method GET
mMemory <- optional $ lookCookieValue "Печеньки-предсказания (куки)"
let memory = fromMaybe "Твое будущее будет определено с помощью веб-технологий!" mMemory
ok $ template "fortune" $ do
H.p "Сообщение из твоей печеньки-предсказания (куки):"
H.p (toHtml memory)
form ! action "/fortune" ! enctype "multipart/form-data" ! A.method "POST" $ do
label ! A.for "fortune" $ "Измени свою судьбу: "
input ! type_ "text" ! A.id "fortune" ! name "new_fortune"
input ! type_ "submit" ! value "Отправить"
updateFortune :: ServerPart Response
updateFortune =
do method POST
fortune <- lookText "new_fortune"
addCookies [(Session, mkCookie "fortune" (unpack fortune))]
seeOther ("/fortune" :: String) (toResponse ())
(Игру слов между HTTP-cookie и fortune cookie мне сохранить как-то не удалось — прим. пер.)
По сравнению с предыдущим примером появилось совсем немного нового:
lookCookieValue
работает точно так же, как иlookText
, с той лишь разницей, что ищет значение в куках, а не параметрах запроса или форме.addCookies
отправляет куки браузеру и имеет следующий тип:addCookies :: [(CookieLife, Cookie)] -> ServerPart ()
CookieLife
определяет, как долго куки существуют и считаются корректными.Session
означает срок жизни для куки до закрытия окна браузера.mkCookie
принимает имя куки, ее значение, и создаетCookie
.seeOther
(т. е. 303, редирект) говорит браузеру сделать новыйGET
-запрос на страницу/fortune
.
Доступ к файлам
В большинстве веб-приложений возникает нужда предоставить доступ к статичным файлам с диска, таким как изображения, таблицы стилей, скрипты и т. д. Мы можем достичь этого с помощью функции serveDirectory
:
fileServing :: ServerPart Response
fileServing =
serveDirectory EnableBrowsing ["index.html"] "."
Первый аргумент определяет, должна ли serveDirectory
создать список файлов в директории, чтобы их можно было просматривать.
Второй аргумент — список файлов индексации. Если пользователь запрашивает просмотр директории и она содержит файл индексации, то вместо списка файлов будет отображен он.
Третий аргумент — путь к директории, к которой предоставляется доступ. В данном примере мы обеспечиваем доступ к текущей директории.
На поддерживаемых платформах (Linux, OS X, Windows), функция serveDirectory
автоматически использует sendfile()
для доступа к файлам. В sendfile()
применяются низкоуровневые операции ядра, обеспечивающие перенос файлов с накопителя в сеть с минимальной нагрузкой на процессор и максимальным использованием сетевого канала.
Размещение файлов
Обработка загрузки файлов на сервер достаточно прямолинейна. Мы создаем форму, как и в предыдущем примере, но вместо lookText
используем lookFile
.
upload :: ServerPart Response
upload =
msum [ uploadForm
, handleUpload
]
where
uploadForm :: ServerPart Response
uploadForm =
do method GET
ok $ template "Размещение файла" $ do
form ! enctype "multipart/form-data" ! A.method "POST" ! action "/upload" $ do
input ! type_ "file" ! name "file_upload" ! size "40"
input ! type_ "submit" ! value "upload"
handleUpload :: ServerPart Response
handleUpload =
do (tmpFile, uploadName, contentType) <- lookFile "file_upload"
ok $ template "Файл загружен" $ do
p (toHtml $ "Временный файл: " ++ tmpFile)
p (toHtml $ "Имя загрузки: " ++ uploadName)
p (toHtml $ "Тип контента: " ++ show contentType)
Когда файл загружен, мы храним его во вре́менной локации. Временный файл будет автоматически удален, когда сервер отправит ответ браузеру. Это гарантирует, что неиспользуемые файлы не загрязняют дисковое пространство.
В большинстве случаев, пользователь не хочет загрузить файл только ради того, чтобы он был удален. Обычно в обработчике вызываются moveFile
или copyFile
, чтобы переместить (или скопировать) файл в его перманентную локацию.
От переводчика
Автор статьи предполагает наличие базовых знаний языка Хаскель. Для установки Happstack воспользуйтесь инструкцией на сайте.
Если вас заинтересовал этот фреймворк, я рекомендую ознакомиться с его полной версией (курс по которой я тоже собираюсь перевести), а также основанном на нем clckwrks. Приятной разработки!
Автор: int_index