Happstack Lite: Веб-фреймворк на Хаскеле

от автора

image
Картинка для привлечения внимания, clckwrks — веб-фреймворк, тесно связанный с Happstack.

Happstack — веб-фреймворк с большими возможностями и богатым API, который развивался на протяжении последних семи лет, чтобы соответствовать нуждам повседневной веб-разработки. К сожалению, богатый и гибкий API может быть бесполезным и запутывающим, когда вам нужно что-то простое. Однако многие и не догадываются, что под крылом Happstack кроется очень элегантный и простой в использовании веб-фреймворк Happstack Lite.

Предисловие

Happstack Lite представляет из себя простую в своей структуре и легкую в использовании версию Happstack. Для его создания разработчики:

  1. Собрали все основные типы и функции, которые вам нужны для разработки веб-приложения, в единственном модуле Happstack.Lite, так что вам не нужно рыскать по модулям в поисках того, что вам нужно.
  2. Дали функциям намного более простые сигнатуры, исключив монадные трансформеры и избавившись от большинства классов типов.
  3. Создали этот туториал, который в менее чем 2000 словах описывает все основные вещи, которые вам нужно знать, чтобы начать писать веб-приложение.

Но самое главное — Happstack Lite почти полностью совместим с Happstack! Если вы разрабатываете приложение на Happstack Lite, и вам нужна продвинутая возможность из Happstack, вы можете просто-напросто импортировать соответствующий модуль и использовать его.
Чтобы перевести проект с Happstack Lite на обычный, вам понадобится внести всего лишь 4 небольших изменения:

  1. import Happstack.Lite заменить на import Happstack.Server
  2. serve Nothing заменить на simpleHTTP nullConf
  3. добавить import Control.Monad (msum)
  4. добавить явный вызов 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 мне сохранить как-то не удалось — прим. пер.)

По сравнению с предыдущим примером появилось совсем немного нового:

  1. lookCookieValue работает точно так же, как и lookText, с той лишь разницей, что ищет значение в куках, а не параметрах запроса или форме.
  2. addCookies отправляет куки браузеру и имеет следующий тип: addCookies :: [(CookieLife, Cookie)] -> ServerPart ()
  3. CookieLife определяет, как долго куки существуют и считаются корректными. Session означает срок жизни для куки до закрытия окна браузера.
  4. mkCookie принимает имя куки, ее значение, и создает Cookie.
  5. 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. Приятной разработки!

ссылка на оригинал статьи http://habrahabr.ru/post/185524/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *