Introducción
¡Hola a todos! Mi nombre es Nikita, y en Typeable usamos el enfoque FRP para desarrollar la interfaz para algunos proyectos, y específicamente su implementación en Haskell, un marco web reflex
. No hay manuales sobre este marco sobre recursos en ruso (y no hay tantos en Internet en inglés), y decidimos arreglarlo un poco.
Esta serie de artículos lo guiará a través de la construcción de una aplicación web Haskell utilizando reflex-platform
. reflex-platform
proporciona paquetes reflex
y reflex-dom
. El paquete reflex
es una implementación de Haskell de programación reactiva funcional (FRP) . La biblioteca reflex-dom
contiene una gran cantidad de funciones, clases y tipos para trabajar DOM
. Estos paquetes están separados porque El enfoque de FRP se puede utilizar no solo en el desarrollo web. Desarrollaremos una aplicación Todo List
que te permitirá realizar diversas manipulaciones con la lista de tareas.
Se requiere un nivel de conocimiento distinto de cero del lenguaje de programación Haskell para comprender esta serie de artículos, y es útil un conocimiento previo de la programación reactiva funcional.
FRP. , — , :
Behavior a
— , . , .Event a
— . , .
reflex
:
Dynamic a
—Behavior a
Event a
, .. , , , , ,Behavior a
.
reflex
— . , . , , , , .., .
, nix
. , NixOS, /etc/nix/nix.conf
:
binary-caches = https://cache.nixos.org https://nixcache.reflex-frp.org binary-cache-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI= binary-caches-parallel-connections = 40
NixOS, /etc/nixos/configuration.nix
:
nix.binaryCaches = [ "https://nixcache.reflex-frp.org" ]; nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];
:
todo-client
— ;todo-server
— ;todo-common
— , ( API).
- :
todo-app
; -
todo-common
(library),todo-server
(executable),todo-client
(executable)todo-app
; -
nix
(default.nix
todo-app
);
-
useWarp = true;
;
-
-
cabal
(cabal.project
cabal-ghcjs.project
).
default.nix
:
{ reflex-platform ? ((import <nixpkgs> {}).fetchFromGitHub { owner = "reflex-frp"; repo = "reflex-platform"; rev = "efc6d923c633207d18bd4d8cae3e20110a377864"; sha256 = "121rmnkx8nwiy96ipfyyv6vrgysv0zpr2br46y70zf4d0y1h1lz5"; }) }: (import reflex-platform {}).project ({ pkgs, ... }:{ useWarp = true; packages = { todo-common = ./todo-common; todo-server = ./todo-server; todo-client = ./todo-client; }; shells = { ghc = ["todo-common" "todo-server" "todo-client"]; ghcjs = ["todo-common" "todo-client"]; }; })
:reflex-platform
.nix
.
ghcid
. .
, , todo-client/src/Main.hs
:
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Reflex.Dom
main :: IO ()
main = mainWidget $ el "h1" $ text "Hello, reflex!"
nix-shell
, shell:
$ nix-shell . -A shells.ghc
ghcid
:
$ ghcid --command 'cabal new-repl todo-client' --test 'Main.main'
, localhost:3003
Hello, reflex!
3003?
JSADDLE_WARP_PORT
. , 3003.
, GHCJS
, GHC
. jsaddle
jsaddle-warp
. jsaddle
JS - GHC
GHCJS
. jsaddle-warp
, - DOM
JS-. useWarp = true;
, jsaddle-webkit2gtk
, . , jsaddle-wkwebview
( iOS ) jsaddle-clib
( Android ).
TODO
!
todo-client/src/Main.hs
.
{-# LANGUAGE MonoLocalBinds #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Reflex.Dom
main :: IO ()
main = mainWidgetWithHead headWidget rootWidget
headWidget :: MonadWidget t m => m ()
headWidget = blank
rootWidget :: MonadWidget t m => m ()
rootWidget = blank
, mainWidgetWithHead
<html>
. — head
body
. mainWidget
mainWidgetWithCss
. body
. — , style
, — body
.
HTML , . HTML . , , ,DOM
, , .
blank
pure ()
, DOM
.
<head>
.
headWidget :: MonadWidget t m => m ()
headWidget = do
elAttr "meta" ("charset" =: "utf-8") blank
elAttr "meta"
( "name" =: "viewport"
<> "content" =: "width=device-width, initial-scale=1, shrink-to-fit=no" )
blank
elAttr "link"
( "rel" =: "stylesheet"
<> "href" =: "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
<> "integrity" =: "sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
<> "crossorigin" =: "anonymous")
blank
el "title" $ text "TODO App"
head
:
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport">
<link crossorigin="anonymous" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" rel="stylesheet">
<title>TODO App</title>
MonadWidget
DOM
, , .
elAttr
:
elAttr :: forall t m a. DomBuilder t m => Text -> Map Text Text -> m a -> m a
, . , , DOM
, , . , blank
. — . el
. , — elAttr
. , — text
. — . , , , , . html, elDynHtml
.
, MonadWidget
, .. DOM
. , , MonadWidget
DOM
, . , , DomBuilder
, , , . , , , , . MonadWidget
, . , MonadWidget
:
type MonadWidgetConstraints t m =
( DomBuilder t m
, DomBuilderSpace m ~ GhcjsDomSpace
, MonadFix m
, MonadHold t m
, MonadSample t (Performable m)
, MonadReflexCreateTrigger t m
, PostBuild t m
, PerformEvent t m
, MonadIO m
, MonadIO (Performable m)
#ifndef ghcjs_HOST_OS
, DOM.MonadJSM m
, DOM.MonadJSM (Performable m)
#endif
, TriggerEvent t m
, HasJSContext m
, HasJSContext (Performable m)
, HasDocument m
, MonadRef m
, Ref m ~ Ref IO
, MonadRef (Performable m)
, Ref (Performable m) ~ Ref IO
)
class MonadWidgetConstraints t m => MonadWidget t m
body
, , :
newtype Todo = Todo
{ todoText :: Text }
newTodo :: Text -> Todo
newTodo todoText = Todo {..}
:
rootWidget :: MonadWidget t m => m ()
rootWidget =
divClass "container" $ do
elClass "h2" "text-center mt-3" $ text "Todos"
newTodoEv <- newTodoForm
todosDyn <- foldDyn (:) [] newTodoEv
delimiter
todoListWidget todosDyn
elClass
, () . divClass
elClass "div"
.
, foldDyn
. reflex
:
foldDyn :: (Reflex t, MonadHold t m, MonadFix m) => (a -> b -> b) -> b -> Event t a -> m (Dynamic t b)
foldr :: (a -> b -> b) -> b -> [a] -> b
, , , . Dynamic
, .. . -, Dynamic
. , Dynamic
. .
foldDyn
( ), . , .. (:)
.
newTodoForm
DOM
, , , Todo
. .
newTodoForm :: MonadWidget t m => m (Event t Todo)
newTodoForm = rowWrapper $
el "form" $
divClass "input-group" $ do
iEl <- inputElement $ def
& initialAttributes .~
( "type" =: "text"
<> "class" =: "form-control"
<> "placeholder" =: "Todo" )
let
newTodoDyn = newTodo <$> value iEl
btnAttr = "class" =: "btn btn-outline-secondary"
<> "type" =: "button"
(btnEl, _) <- divClass "input-group-append" $
elAttr' "button" btnAttr $ text "Add new entry"
pure $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl
, , inputElement
. , input
. InputElementConfig
. , , , initialAttributes
. value
HasValue
, input
. InputElement
Dynamic t Text
. , input
.
, , elAttr'
. DOM
, , . , . domEvent
. , Click
, . :
domEvent :: EventName eventName -> target -> Event t (DomEventType target eventName)
. ()
.
, — tagPromptlyDyn
. :
tagPromptlyDyn :: Reflex t => Dynamic t a -> Event t b -> Event t a
, , , Dynamic
. .. , tagPromptlyDyn valDyn btnEv
btnEv
, , valDyn
. .
, , promptly
, — . , . tagPromplyDyn valDyn btnEv
, , tag (current valDyn) btnEv
. current
Behavior
Dynamic
. . Dynamic
Event
tagPromplyDyn
, .. , , Dynamic
. , tag (current valDyn) btnEv
, , current valDyn
, .. Behavior
, .
Behavior
Dynamic
: Behavior
Dynamic
, Dynamic
, Behavior
. , t1
t2
, Dynamic
, t1
[t1, t2)
, Behavior
— (t1, t2]
.
todoListWidget
Todo
.
todoListWidget :: MonadWidget t m => Dynamic t [Todo] -> m ()
todoListWidget todosDyn = rowWrapper $
void $ simpleList todosDyn todoWidget
simpleList
. :
simpleList
:: (Adjustable t m, MonadHold t m, PostBuild t m, MonadFix m)
=> Dynamic t [v]
-> (Dynamic t v -> m a)
-> m (Dynamic t [a])
reflex
, DOM
, div
. Dynamic
, , . :
todoWidget :: MonadWidget t m => Dynamic t Todo -> m ()
todoWidget todoDyn =
divClass "d-flex border-bottom" $
divClass "p-2 flex-grow-1 my-auto" $
dynText $ todoText <$> todoDyn
dynText
text
, , Dynamic
. , , DOM
.
2 , : rowWrapper
delimiter
. . :
rowWrapper :: MonadWidget t m => m a -> m a
rowWrapper ma =
divClass "row justify-content-md-center" $
divClass "col-6" ma
delimiter
-.
delimiter :: MonadWidget t m => m ()
delimiter = rowWrapper $
divClass "border-top mt-3" blank
, Todo
. . .