Yesod + Fay
Проблема
Во время разработки front-end вообще и использования фреймворка Yesod в частности приходится иметь дело с JavaScript. Разумеется, пока. Дело в том, что для работы с JS рекомендуется использовать Shakespearean Template. А значит, куски JS будут лежать либо в файлах с расширением .julius, либо в Template Haskell. Проблема удобства разработки встаёт тут же. Yesod предоставляет живую перезагрузку кода. Тем самым, цикл разработки становится следующим:
- написание кода;
- сохранение изменений;
- автоматическая перекомпиляция изменений (если изменялись шаблоны hamlet или обработчики);
- создание нового js файла по адресу static/tmp/autogen-*.js;
- перезагрузка страницы в браузере;
- тестирование изменений.
А значит, если у нас в коде ошибка, то мы отловим её в runtime. Вдобавок, сказывается отсутствие REPL для JS. Если поднять cabal repl
, то из ghci нереально будет проверить JS-код.
Что же делать? Как сместить баги из runtime в compile-time? Как добавить в ужас JS немного статической типизации?
Способы решения
На эти вопросы уже есть ответы.
GHCJS, Elm - судя по отзывам, превосходные вещи. Однако мне не довелось ещё соприкоснуться с ними. У Сноймана были какие-то мысли на их счёт, но я не вдавался в подробности и не могу ничего сказать на сей счёт конкретного.
Fay
Fay - это подмножество Haskell, которое можно успешно скомпилировать в валидный JS код. Среди возможностей Fay отмечают:
- статическая типизация;
- лень;
- чистота;
- базовые типы, с которыми коррелирует JS + алгебраические типы данных;
- автоматическое преобразование данных из/в JSON.
Как можно заметить из их wiki, он успешно интегрирован во многие вэб-фреймворки. Но остановимся на Yesod.
Yesod + Fay
Создадим обычный проект на yesod:
$ yesod init
Welcome to the Yesod scaffolder.
I'm going to be creating a skeleton Yesod project for you.
What do you want to call your project? We'll use this for the cabal name.
Project name: faytest
Yesod uses Persistent for its (you guessed it) persistence layer.
This tool will build in either SQLite or PostgreSQL or MongoDB support for you.
We recommend starting with SQLite: it has no dependencies.
s = sqlite
p = postgresql
pf = postgresql + Fay (experimental)
mongo = mongodb
mysql = MySQL
simple = no database, no auth
mini = bare bones, the "Hello World" of multi-file Yesod apps
(Note: not configured to work with yesod devel)
url = Let me specify URL containing a site (advanced)
So, what'll it be? pf
That's it! I'm creating your files now...
---------------------------------------
___
{-) |\
[m,].-"-. /
[][__][__] \(/\__/\)/
[__][__][__][__]~~~~ | |
[][__][__][__][__][] / |
[__][__][__][__][__]| /| |
[][__][__][__][__][]| || | ~~~~
ejm [__][__][__][__][__]__,__, \__/
---------------------------------------
The foundation for your web application has been built.
There are a lot of resources to help you use Yesod.
Start with the book: http://www.yesodweb.com/book
Take part in the community: http://yesodweb.com/page/community
It's highly recommended to follow the quick start guide for
installing Yesod: http://www.yesodweb.com/page/quickstart
If your system is already configured correctly, please run:
cd faytest && stack build && stack exec -- yesod devel
Выберем pf
…
Тут следует отметить, что для работы с PostgreSQL, необходимо, чтобы были разрешены следующие зависимости:
- во-первых, сама СУБД: postgresql-9.4
- во-вторых, нужные для работы пакетов библиотеки: libpq-dev;
- в-третьих, нужно сконфигурировать юзера
postgres
в СУБД, если вы этого ещё не сделали ранее.
После того, как зависимости разрешены, создаём базу данных и юзера.
Подсоединяемся к СУБД.
Создаём базу и юзера.
postgres=# create database faytest;
CREATE DATABASE
postgres=# create user faytest with password 'faytest';
CREATE ROLE
Теперь заводим проект. Как поднимется, смотрим тут.
Последним пунктом значится вычисление числа Фибоначчи по его индексу. А теперь посмотрим, как это реализовано.
В структуре проекта появились следующие файлы:
./static/faygen-omEJ2n4J.js
./static/fay-runtime.js
./fay-shared
./fay-shared/SharedTypes.hs
./fay
./fay/FFIExample.hs
./fay/Home.hs
./fay/Fay
./fay/Fay/Yesod.hs
В шаблоне templates/homepage.hamlet
находим строки:
Текстовое поле для ввода и вывода значений. Где же описана логика?
В обработчике Handler/Home.hs
находим строку $(fayFile "Home")
. Так подключается fay/Home.hs
.
Посмотрим на содержимое:
{-# LANGUAGE RebindableSyntax #-}
{-# LANGUAGE OverloadedStrings #-}
module Home where
import FFIExample
import DOM
import Data.Text (fromString)
import qualified Data.Text as T
import Fay.Yesod
import Prelude
import SharedTypes
main :: Fay ()
main = do
input <- getElementById "fibindex"
result <- getElementById "fibresult"
onKeyUp input $ do
indexS <- getValue input
index <- parseInt indexS
call (GetFib index) $ setInnerHTML result . T.pack . show
С помощью монады Fay осуществляются все преобразования с побочными эффектами, прямо как с IO. Не правда ли, код стал более читаем? Часть функций мы импортировали из DOM
, call
- из Fay.Yesod
, остальное - из FFIExample
. Как можно заметить, у нас тут и клиентская, и серверная часть. Как водится, они разделены. С помощью call
мы делаем асинхронную отправку запроса по адресу /fay-command
. Сервер принимает запрос и вызывает обработчик appFayCommandHandler
. В Application.hs
прописывается функция onCommand
из Handler.Fay
, которая отрисовывает на клиенте число Фибоначчи по индексу.
Для того, чтобы разобраться с тем, как в Fay реализована клиентская и серверная часть, рассмотрим их по отдельности.
Клиентская часть
Все файлы, относящиеся к клиентской части лежат в директории fay
:
В Home.hs лежит подключаемый fayFile
. Его содержимое мы уже рассмотрели выше.
В Fay/Yesod.hs
лежат функции для взаимодействия с сервером. Ровно то же самое можно обнаружить в хакадже.
В FFIExample - самое интересное: примеры реализации внешнего интерфейса вызова JS. С побочными действиями, со всеми делами.
-- | Example of defining FFI functions.
--
-- The `ffi' method is currently incompatible with 'RebindableSyntax',
-- so these are defined in another module.
module FFIExample where
import Data.Text (Text)
import DOM
import FFI
onKeyUp :: Element -> Fay () -> Fay ()
onKeyUp = ffi "%1.onkeyup=%2"
setInnerHTML :: Element -> Text -> Fay ()
setInnerHTML = ffi "%1.innerHTML=%2"
Как можно заметить, мы не передаём аргументы в функцию, а используем порядковые номера %1
, %2
и так далее, как в Си.
Серверная часть
Все файлы, относящиеся к серверной части, лежат в fay-shared
. В нашем случае это SharedTypes.hs:
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE DeriveDataTypeable #-}
module SharedTypes where
import Prelude
import Data.Data
import Fay.Yesod
data Command = GetFib Int (Returns Int)
deriving (Typeable, Data)
Command
здесь - это тип, который принимает сервер и обрабатывает в Handler.Fay
(напомню, что обработчик можно найти по адресу Handler/Fay.hs
). Выше уже это упомяналось, рассмотрим же теперь подробнее:
module Handler.Fay where
import Fay.Convert (readFromFay)
import Import
import Prelude ((!!))
import Yesod.Fay
fibs :: [Int]
fibs = 0 : 1 : zipWith (+) fibs (drop 1 fibs)
onCommand :: CommandHandler App
onCommand render command =
case readFromFay command of
Just (GetFib index r) -> render r $ fibs !! index
Nothing -> invalidArgs ["Invalid command"]
Нетрудно заметить, что при добавлении новых конструкторов типов, можно будет сопоставлением с образцом легко дополнить взаимодействие с сервером так, как того требует бизнес-логика приложения. Да, это устаревающий AJAX, но он ещё не совсем умер. С этим действительно можно работать.
Fay Runtime
А как это чудо выглядит после компиляции? Помните, в начале в списке файлов проекта появились static/fay-runtime.js
, static/faygen-*.js
? Вот это как раз оно и есть. В файле fay-runtime.js
содержится Prelude
и множество полезных вещей, которые мы можем использовать. В сгенерированном файле содержатся результаты компиляции файлов из директории fay
.