29
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

正月にElm触りだしてつまずいたこと

Last updated at Posted at 2019-01-05

この記事について

正月にElmを使って自分用のブログのクライアントサイドを作っていたのですが、つまずいたことを備忘録として書いておきます。

なお、公式ガイド (日本語版はこちら)をサーッと目を通してから触り始めてつまずいたことを書いているので、とりあえずそちらを読んだ方を想定しています。

ちなみに、私はReactとかはちょこっと触っていてなにか作ったことはあるものの、Reduxとか使ってまじめな開発はやっていないという感じのレベルです。

開発環境をどう作るべきか?

@ababup1192さんのこちらの記事をほぼ丸パクリさせてもらいました。
https://gist.github.com/ababup1192/a1a091bcc0e535d180544639f531302c

最初elm reactorとか使って開発していたのですが、いざJSにコンパイルして、html内に読み込ませて、ほんでcssも読み込ませて―とか考えだすとやっぱりwebpackとか使ったほうがよいですね。

ちなみに、@ababup1192さん曰く、開発環境のベースリポジトリとしては最近はこちらの方がおすすめとのことでした(情報提供ありがとうございます!)。
https://github.com/simonh1000/elm-webpack-starter

Navigationでページ遷移を実装

公式ガイドのナビゲーションするを見ながら、ページ遷移を作って行きました。
が、公式ガイドの例にはURLによってViewを切り替える例がなかったため、小一時間どうやるべきなのか考えてしまいました。

とりあえず公式ガイドを読みなおし、URLをパースするの統合の章に書いてある内容を参考に実装していきました。

あと、elm-spa-exampleのソースコードも参考にさせてもらいました。

どうやら、UrlからRoute型のvariantsを作って、Modelにもたせてやればよさそうな感じがしたので、こんな感じの実装になりました。

-- URL PARSER
type Route
    = TopPage
    | NextPage String

routeParser : Parser (Route -> a) a
routeParser = oneOf [ map NextPage (s "next" </> string) ]

parseUrl : Url.Url -> Route
parseUrl url =
    let
        parsed =
            -- #をパースするためにUrlをparseする前にfragmentの設定してる
            -- https://package.elm-lang.org/packages/elm/url/1.0.0/Url
            -- 以下の実装を参考にした
            -- https://github.com/rtfeldman/elm-spa-example/blob/b5064c6ef0fde3395a7299f238acf68f93e71d03/src/Route.elm#L59
            { url | path = Maybe.withDefault "" url.fragment, fragment = Nothing }
                |> parse routeParser
    in
    case parsed of
        Just route -> route
        Nothing -> TopPage


-- UPDATE
type Msg
    = LinkClicked Browser.UrlRequest
    | UrlChanged Url.Url

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        LinkClicked urlRequest ->
            case urlRequest of
                Browser.Internal url ->
                    ( model, Nav.pushUrl model.key (Url.toString url) )

                Browser.External href ->
                    ( model, Nav.load href )

        UrlChanged url ->
            let
                -- Urlが変更されたら、parseUrlでRoute型variantsへ変換
                route = parseUrl url
            in
            ( { model | url = url, route = route }, Cmd.none )


-- VIEW
view : Model -> Browser.Document Msg
view model =
    case model.route of
        TopPage ->
            { title = "URL Interceptor"
            , body =
                let
                    contentPath = "/#/next/"
                in
                [ text "The current URL is: "
                , b [] [ text (Url.toString model.url) ]
                , ul []
                    [ viewLink <| contentPath ++ "foo"
                    , viewLink <| contentPath ++ "bar"
                    ]
                ]
            }
        NextPage param ->
            { title = "URL Interceptor"
            , body = [ text param ]
            }

コードの全体はGistに貼っておきます。
https://gist.github.com/yuizho/c0b08ee59d3da81891426b17209fff4d

トップがListのJsonをDecodeする

トップレベルがリストのJsonをどうDecodeするのかでちょっと悩みましたが、
Decode.listを使えば一発でした。

たとえば、こんなJsonをデコードしたかったら、

[
    {
        "title": "タイトル",
        "id": 1,
    },
    {
        "title": "タイトル2",
        "id": 2,
    },
]

こんなかんじですね。

type alias Article =
    { title : String
    , id : Int
    }

articlesDecorder : Decode.Decoder (List Article)
articlesDecorder =
    Decode.list
        (Decode.map2 Article
            (Decode.field "title" Decode.string)
            (Decode.field "id" Decode.int)
        )

ページを初期化するために複数のAPIをたたきたい

一つのAPIをたたいてその結果を元にModelを更新する方法は公式ガイドに書いてあったのでスムーズに実装できました。
が、複数のAPIをたたき、その複数の結果を同期してModelを更新する方法がよくわかりませんでした。

いろいろ漁っているとどうやら、Taskを使うとJavaScriptのPromise.allみたいな感じで複数の非同期処理の結果を同期して取得できるらしい。
https://www.reddit.com/r/elm/comments/91t937/is_it_possible_to_make_multiple_http_requests_in/e30vlns/

ただ、これをHttp.getとかに適用する方法がよくわからない……

そんなときにHttp.toTaskというものがあることを発見しました。
こんな感じでHttp.getの結果を食わしてやれば、前述のredditのサンプルコードと同様にタスクとしてTask.map2へ渡すことが出来た。

Http.get articleUrl articleDecorder |> Http.toTask

全体像としてはこんな感じ。

-- UPDATE


type Msg
    = ShowContent (Result Http.Error ( Article, String ))


-- ...

-- HTTP
fetchContent : String -> Cmd Msg
fetchContent id =
    let
        articleTask =
            Http.get articleUrl articleDecorder |> Http.toTask

        contentTask =
            Http.getString contentUrl |> Http.toTask
    in
    Task.attempt ShowContent <|
        Task.map2 (\article content -> ( article, content )) articleTask contentTask

ちなみに、これ実装するのに参考にしたソースが数ヶ月前にcloneした公式ガイドにのっていたサンプルコードだったため、使ったHttpのバージョンが古いです(1.0.0。いまは2.0.0がでている)。

多分2.0.0でも似たような感じで同様のことができるんだろうと思います。

おわりに

これはElmに限ったことでは無いと思うのですが、公式ガイドに書いてあることは真似して実装できるがそれ以外のことはすべてつまずくみたいな感じでした(やり方がわかってみれば、なーんだ簡単じゃんって感じなんですが)。もっと触りつづけよう。
またなにか増えたら書いていこうと思います。

一週間程度Elmを触ってみて、Elmはコンパイルが通るまではなかなか大変だけど、一度コンパイルが通ってしまえばほぼ考えた通りに動くという感じでなかなか心地よいです。

まだテストコードとか書いてないんですが、そんなに一生懸命テストを書かなくても大丈夫そうだなぁという気持ちです。

しばらく触ってみて、この辺の感想が変わってきたら追記しようと思います。

29
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?