Přejít k navigační liště

Zdroják » JavaScript » Elm – Hello world on the map – začínáme

Elm – Hello world on the map – začínáme

Články JavaScript

Do minulého dílu se nevešla ani řádka kódu, pojďme se tedy vrhnout po hlavě do vody, vlastně po hlavě do Elmu. Výklad pokračuje v komentářích uvedeného příkladu, přibližujeme se tím ideálům literárního programování sazeče, matematika a programátora Donalda E. Knutha. Kdo si nestihl osvojit základy jazyka Elm, tak si na Y minut odskočí.

Nálepky:

Aplikace v Elmu

Vytvoříme soubor Main.elm, hlavní modul našeho programu. Budeme chtít zobrazit mapu a vypsat souřadnice středu mapy a aktuální přiblížení (zoom).

 

 

{-
   Nejprve deklarujeme modul a naimportujeme potřebné knihovny.
   Vysvětleno to je třeba v již zmiňovaném tutoriálu http://www.elm-tutorial.org/en/01-foundations/04-imports-and-modules.html
-}


module Main exposing (..)

{-
   Vše potřebné pro html aplikaci naimportujeme z balíčku elm-lang/html http://package.elm-lang.org/packages/elm-lang/html
-}

import Html exposing (div, h1, table, tr, td, text)
import Html.Attributes exposing (..)
import Html.App as HtmlApp
import Html exposing (Html)


{-
   Každý správný program začíná funkcí main.
   Programátoři znalí některého z jazyků jako například C, Java, nebo Python apod.  se tomu nediví.
   Ostatním postačí informace, že tato funkce se spustí po spuštění programu.

   My v této funkci v podstatě nastavujeme *Model-View-Controller* architekturu,
   či přesněji *Model-Update-View*, který je základem Elm architecture http://guide.elm-lang.org/architecture/.

   Program typu *Hello world* bez MVC můžete vidět v příkladech http://elm-lang.org/examples/hello-html.

   Interaktivní aplikaci vytvoříme zavoláním funkce Html.App.beginnerProgram http://package.elm-lang.org/packages/elm-lang/html/1.1.0/Html-App,
   která se už magicky postará o vše potřebné.
   Stačí předat náš model, view a update. Protože Elm je jazyk funkcionální, předáváme jako parametry funkce.
   Vytvoříme si je hned dále v kódu poté, jakmile zapíšeme funkci main.
-}


main : Program Never
main =
    HtmlApp.beginnerProgram { model = initialModel, view = view, update = update }



-- MODEL
{-
   Data, jak všichni víme, patří do modelu.
   A protože Elm je staticky typovaný jazyk,
   musí mít i data jasnou strukturu.

   Je s tím trochu práce navíc, ale oceníme to při kompilaci.
   Tam se ukáže většina chyb, které se v Javascriptu snažíme odhalit pomocí testů.
   Elm kompilátor velice pěkně vypisuje chyby a často umí i poradit, jak chybu opravit.
   Ukážeme si to příště.

   Model bude jednoduchý záznam se třemi prvky.
   Zde definujeme pouze datový typ, hodnoty přidáme za chvíli.
   V modelu budeme mít
   - id mapy jako řetězec,
   - souřadnice středu mapy jako dvojice dvou čísel typu float
   - a přiblížení mapy (zoom) jako celé číslo.
-}


type alias Model =
    { id : String
    , center : ( Float, Float )
    , zoom : Int
    }



{-
   Ve funkcionálních jazycích je vše funkce.
   Vytvoříme si tedy funkci, která vrátí nový model
   (snad by se dalo říci instanci modelu, ale tato terminologie je ve funkcionálním světě nadbytečná).

   Místo použitého způsobu zápisu, který výtváří nový záznam, lze použít ekvivaletní zápis, kde použijeme výše definovaný
   typ Model jako konstruktor. Vypadalo by to pak takto:
   initialModel = Model "mapa" ( 48.816, 17.519 ) 14
   Oč je tento zápis stručnější, o to je o něco méně přehledný.

   Všimněte si prosím výše, jak jsme ve funkci main tuto funkci použili, přiřadili ji modelu.
-}


initialModel : Model
initialModel =
    { id = "mapa"
    , center = ( 48.816, 17.519 )
    , zoom = 14
    }



-- UPDATE
{-
   Elm architecture funguje podobně jako Redux v Javascriptu.
   Je tu jisté terminologické zmatení, akce jsou messages, reducer je update,
   store je model.
   Pokud Redux neznáte, nevadí.

   Princip je jednoduchý. Když na stránce dojde k nějaké události,
   odešle se zpráva do funkce update.

   Nejprve si tedy nadefinujeme jaké typy zpráv v našem programu budou dostupné.
   Prozatím žádné, což ale také musíme uvést.
   Elm je totiž, jak víme, staticky typovaný jazyk.
-}


type Msg
    = Noop



{-
   A nyní funkce update, ta vyžaduje dva parametry.

   První parametr je zpráva, popisuje typ události, ke které v aplikaci došlo.

   Druhý parametr je model, tedy vlastně data, stav naší aplikace.

   Funkce update vrátí nový model, nebo, jako v tomto případě, model původní,
   nijak nezměněný.
   Výše jsme si definovali jedinou zprávou, *Noop*, tedy nic nedělej.
   Prozatím nebudeme reagovat na žádné události, stránka bude vlastně statická.
   To sice nechceme, ale rozhýbeme ji až příště.
-}


update : Msg -> Model -> Model
update msg model =
    case msg of
        Noop ->
            model



-- VIEW
{-
   Balíček *elm-lang/html* umožňuje psát Html kód pomocí funkcí.
   Renderuje html kód pomocí virtual domu, podobně jako v reactu.
   Prý je snad i rychlejší http://elm-lang.org/blog/blazing-fast-html-round-two.

   Jak je zřejmé z kódu, pro každý html tag je tu funkce.
   Těmto funkcím předáváme vždy dva parametry, pole atributů a pole
   vložených elementů (včetně textových uzlů).
   Jak se vám to líbí? No, jsx syntaxe z Reactu bude pro někoho
   (včetně mne) o něco přehlednější. Snad jde jen o zvyk.

   Aby to nebylo až tak nepřehledné, vyčlenil jsem do samostatné funkce část html,
   která nám bude vypisovat informace, řekněme klidně ladící informace.
   Takto snadno lze v Elmu vytvořit něco jako jednoduchou subkomponentu.
   Složitější komponenta by byla v samostatném modulu, ale o tom možná až někdy příště.
   A nebo si to nastudujte ve Sportově tutoriálu http://www.elm-tutorial.org/en/04-starting/05-multiple-modules.html.

   Elementu div, ve kterém bude mapa, jsem atributem style nastavil výšku, šířku a barvu pozadí,
   aby byl element na stránce vidět.
   Nastavit výšku (někdy i délku) pro element, kde bude mapa, je dle dokumentace nejspíš všech mapových API povinné.
-}


view : Model -> Html Msg
view model =
    div []
        [ h1 [ class "nadpis" ] [ text "Mapový server" ]
        , viewModelInfo model
        , div
            [ id model.id
            , style
                [ ( "width", "100%" )
                , ( "height", "500px" )
                , ( "background-color", "silver" )
                ]
            ]
            []
        ]



{-
   Zde je část view, která se volá z hlavního view.

   Vypisuje údaje z modelu, jako aktuální souřadnice středu mapy, zoom
   a také id mapy (které se ale při práci s mapou měnit nebude).
-}


viewModelInfo : Model -> Html Msg
viewModelInfo model =
    table []
        [ tr []
            [ td
                []
                [ text "id mapy" ]
            , td [] [ text ("#" ++ model.id) ]
            ]
        , tr []
            [ td [] [ text "střed mapy" ]
            , td [] [ text (toString model.center) ]
            ]
        , tr []
            [ td [] [ text ("zoom") ]
            , td [] [ text (toString model.zoom) ]
            ]
        ]

Zdrojový soubor zkompilujeme a získáme html stránku.

$ elm make Main.elm --output=index.html

a nebo místo toho v tuto chvíli postačí spustit

$ elm reactor

a v prohlížeči otevřít adresu http://localhost:8000/.

Příkazem elm reactor spustíme jednoduchý webový server, který navíc provede kompilaci zdrojových souborů z Elmu do Html (Javascriptu). Jinak bychom si museli nějaký webový server spustit sami a kompilaci provést ručně. Nicméně za chvilku se bez vlastního webového serveru neobejdeme, a tak si to ukážeme.

První pohled na právě vytvořený index.html lehce vyděsí nejednoho ostříleného webového vývojáře. Získali jsme téměř 10 000 řádků kódu v javascriptu. Kód je poměrně přehledný, ale to množství je na první pohled vskutku děsivé. Dokonce ani obyčejný Hello world nebude po kompilaci o nic štíhlejší. Stránka obsahuje jen statický text Hello world, a i ten je generovaný tím více než 10 000 řádkovým Javascriptem. Celý soubor index.html má přes 275 kB. To svědčí o tom, že jsme prozatím použili kanon na vrabce. Zkompilovaný soubor v sobě obsahuje mimo jiné celou implementaci virtuálního domu. Takže to je vlastně, jako kdybychom si do html stránky přidali třeba knihovnu react. Ta má 676 kB, mimifikovaná verze pak „pouhých“ 144 kB. Tak to už je celkem srovnatelné. Elm kompilátor Javascript prozatím nijak nemimifikuje. To si musíte udělat sami. V nějaké příští verzi kompilátoru na to snad bude přepínač.

screen-01

Žádnou mapu na naší stránce nevidíme. Místo ní na nás civí veliký stříbrný čtverec. Přirozeně, potřebujeme ještě mapové api. Jenže to je psáno v Javascriptu. Ačkoliv je Javascript pro Elm něco jako assembler pro jazyk C, chcete-li k Javascriptu z Elmu přímo přistupovat (a použít některou z nepřeberného množství javascriptových či nodejs knihoven), musíte si vzít gumové rukavice, sterilní oblek a k Javascriptu přistupovat speciálně vytvořenou bránou.

Mapové api

Nejlepší by bylo vyhnout se přímému použití Javascriptu a využívat jen krásné funkcionální a typově bezpečné možnosti Elmu. Jenže žádné mapové API pro Elm zatím neexistuje. A přepsat do Elmu nějaké mapové API, nebo napsat nové, to nebude zábava na víkend. Jistě to někdo jednou udělá.

Nyní tedy uděláme přesně to, co se nedoporučuje a od čeho vás celou dobu odrazuji. Propojíme Elm s Javascriptem. Konkrétně s externí knihovnou, mapovým API. Jaké API zvolíte, je vaše volba. Stejný postup bude fungovat na všechna javascriptová mapová API i na jakékoliv jiné javascriptové knihovny, ať už mají dělat cokoliv (kreslit grafy, zobrazovat kalendář, vytvářet UI, …), ale vždy je dobré se zamyslet, zda potřebnou funkčnost nelze dosáhnout přímo v Elmu (třeba kreslit v 2D, či pracovat s SVG umí Elm výborně, dokonce i s WebGL je radost pracovat, když bude zájem, napíšu o tom)

Z mapových api se nabízí samozřejmě klasika od Google. Opensource řešení nejen pro Openstreetmap maps jsou Leaflet, nebo Openlayers. Dále jsou k dispozici komerční API konkurentů Googlu jako třeba ruský Yandex nebo český Seznam.

Pro tento článek (nejen pro něj) jsem si vybral mapy od českého Seznamu. Konkurence funguje a Seznam se v konkurenčním boji s Googlem nevzdává. Nabízí (zdarma) velice kvalitní mapové podklady. Pro Českou republiku jsou bezkonkurenční, pro zbytek světa těží z projektu Openstreetmap.

Elm a HTML page

HTML stránka, kterou vytvoří elm kompilátor, nám nyní nestačí. Potřebujeme si tam přidat link na knihovnu mapového api, a dopsat vlastní kód, který ho spustí. Což o to, mohli bychom to prostě do té vygenerované html stránky napsat. Ovšem při opětovné kompilaci o to zase přijdeme, protože soubor index.html se bez ptaní přepíše. Raději si tedy vytvoříme vlastní HTML stránku obalujicí zkompilovaný Elm.

Znovu si zkompilujme náš zdrojový kód v Elmu.

$ elm make Main.elm --output=main.js

Všimněte si malé změny. V parametru --output je jiný název cílového souboru, místo do index.html kompilujeme do main.js. Název není důležitý, fatální následky způsobí přípona .js. Kompilujeme-li do souboru s příponou .html, vytvoří se jednoduchá html stránka. Najdeme v ní zkompilovaný zdrojový kód Elmu umístěný v elementu <script>. Kompilujeme-li do souboru s příponou .js, vytvoří se samostatný soubor s Javascriptem. Kód v tomto souboru je stejný jako v ten, který byl v generovaném html souboru v elementu <script>. Až na malý detail, který zmíním za chvíli.

Nyní máme zkompilovaný čistý Javascript a html stránku si vytvoříme sami. Zkompilovaný skript do ní vložíme odkazem.

Zkompilovaný soubor můžeme ještě minifikovat některou kompresní knihovnou. Mimifikaci kompilátor Elmu prozatím neumí. To berte jako výhodu, aspoň si můžeme svobodně vybrat mimifikační knihovnu.

<!DOCTYPE html>
<html lang="cs">
  <head>
    <meta charset="UTF-8">
    <title>Elm smapy</title>
    <!--
      přidáme si mapové api,
      rozhodl jsem se vyzkoušet api.mapi.cz
    -->
    <script type="text/javascript" src="//api.mapy.cz/loader.js"></script>
    <!--
      Api map od seznamu má tu zvláštnost, že vyžaduje volání `Loader.load()`,
      které načte všechny potřebné knihovny
    -->
    <script type="text/javascript">Loader.load()</script>
    <!--
      Zde načteme zkompilovaný zdrojový kód Elmu,
      ten jsme si ručně zkompilovali příkazem
      $ elm make Main.elm --output=main.js
    -->
    <script type="text/javascript" src="./main.js"></script>
  </head>
  <body>
    <!--
      POZOR !!!!
      Tady je ten jediný malý detail,
      ve kterém se liší kompilace do přímo html stránky od kompilace do samostatného javascriptu.
      
      Elm musíme spustit, třeba takto
    -->
    <script type="text/javascript">Elm.Main.fullscreen()</script>
  </body>
</html>

Nesmíme zapomenout spustit Elm některým z těchto způsobů.

Elm.Main.embed(someNode);
// nebo takto
Elm.Main.fullscreen();
// a nebo takto
Elm.Main.worker();

V čem se ty tři možnosti liší, je patrné už z názvu volaných funkcí. Více sdělí průvodce.

Ono *Main“ odpovídá názvu souboru hlavního modulu ve kterém máme funkci main naší aplikace. V případě, že kompilujeme do html stránky, tak si můžete všimnout, že na samém konci kompilátor přidal právě volání Elm.Main.fullscreen();. Když kompilujeme do samostatného Javascriptu, musíme se o to postarat sami. Ale zase máme více možností, jak Elm do stránky vložit.

Nyní by to mělo fungovat stejně jako předtím. Žádnou mapu sice stále neuvidíme, ale už máme k dispozici mapové api, byť ho zatím nijak nevyužíváme. Výsledek si můžeme prohlédnout po spuštění serveru, postačí i

$ elm reactor

To co máme, je běžná html stránka s Javascriptem. Na straně serveru nepotřebujeme nic jiného, než tento soubor odeslat do prohlížeče. K tomu postačí třeba i server apache, který bývá někdy nainstalován v linuxových distribucích (jen třeba najít jeho root adresář). Pokud používáte nodejs, dobře poslouží například jednoduchý http-server. Časem si můžete napsat server pomocí express nebo koa. Ale není to vůbec nutné, smysl to bude mít ve chvíli, kdy budeme do aplikace potřebovat posílat data ze serveru, například z nějaké vlastní databáze. Ovšem pokud na ukládání dat využijete firebase, nebo podobnou službu, na hostování aplikace v Elmu by měl stačit jednoduchý hosting podporujicí třeba jen „statické“ HTML stránky.

Jsa rozmazlený možnostmi ekosystému platformy Node.js, měl jsem pokušení na straně serveru mít pečlivě nakonfigurovaný webpack. Postupně zjišťuji, že nic takového vlastně nepotřebuji. Sporto ho sice ve svém tutoriálu doporučuje, uvidím co ukáže praxe na rozsáhlejším projektu. Nemohu se zbavit dojmu, že všechny ty úžasné (a těžce nakonfigurované) funkce Webpacku jsou při vývoji v Elmu celkem zbytečné (a příliš komplikované). Proto jsem se od Webkitu vrátil zpět ke Gulpu. To zásadní, co potřebujeme, je zautomatizovat kompilaci elmu při změně zdrojového souboru a obnovit stránku v prohlížeči. Tohle docela pěkně dělá elm-live. Samotný elm-reactor má ambice nabízet vývojářům mnohem lepší služby, než nyní nabízí, nicméně při přechodu na Elm 0.17.0 reactor trochu zaklopýtal a přestaly na chvíli fungovat i některé věci, které v Elmu 0.16 fungovaly. Ke cti třeba dodat, že jednak je to již opraveno a jednak šlo o chybu v doplňkovém nástroji, bez kterého se lze obejít.

Ve verzi 0.18 má být v Elmu podpora pro server rendering, renderování stránek na serveru. Jakým způsobem to bude řešeno, nevím.

Pokračování příště

Dnes jsme si připravili aplikaci v Elmu a příště se konečně naučíme volat Javascript z Elmu a Elm z Javascriptu (i když se to dělat nemá).

Komentáře

Subscribe
Upozornit na
guest
8 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
vojta tranta

Drobná korekce: Store není model – state je model, spíš, ne?

Já jsem zrovna taky začal s Elmem a je to bomba, takže se moc těším na další díl! Díky!

Jan

Každý správný program začíná funkcí main.
Programátoři znalí některého z jazyků jako například C, Java, nebo Python apod. se tomu nediví.

Python nemá nic jako vstupní bod, funkce main je jen konvence a je nutno ji zavolat.

Martin Hassman

To je myslím v pořádku, v textu se netvrdí, jak se s main v Pythonu zachází, jen to, že není Python programátorům neznámá. A zdá se, že opravdu není 8-)

Marek Fajkus

Kompilace by neselhala. Spousta modulu Main nemá (vše ze standard library). Tyto moduly jen exposuji funkce pro další moduly. Pokud se ale člověk snaží zvolit modul bez main jako vstupní, zahlasí se (myslím pouze v runtime), že není co zobrazit, protože modul neobsahuje main.

segedacz

Děkuji za seriál!

Zrovna asi řeším něco podobného a tak se těším, že mě další díly případně popostrčí.

Jen bych doplnil, že firebase se dá použít nejen na ukládání dat, ale i jako ten jednoduchý hosting.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.