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

в 13:05, , рубрики: happstack, haskell, web, Веб-разработка, перевод

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. Приятной разработки!

Автор: int_index

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js