『基礎からわかるElm』を写経してみる(7)実践3:ナビゲーションとテスト(複数ページのElmアプリの作り方)
以下の本を写経しています。 以下の環境構築の後、elm-formatとVisualStudioCodeのElm拡張機能を用いて、『Alt + Shift + F』と『Ctrl + S』を使用しながらやっています。
今回は、p207の『実践3:ナビゲーションとテスト』を写経して、Elmで複数ページのアプリの作り方を学びたいと思います。
本家サイトのコード
https://github.com/jinjor/elm-book/tree/master/4_7_navigation-github
- 1. 開発環境
- 2. 今回作る予定のアプリの概略
- 3. 新規Elmアプリの作成
- 4. URLパーサーの実装
- 5. トップページのRouteのテスト
- 6. src/Route.elmの新規作成
- 7. 最初のelm-testを通す
- 8. tests/Example.elmとsrc/Route.elmを編集
- 9. src/Main.elmの新規作成
- 10. MODELにページを定義する
- 11. UPDATE
- 12. Msgその1: LinkClicked Browser.UrlRequest
- 13. Msgその2: Loaded result
- 14. Msgその3: UrlChanged Url.Url
- 15. VIEW
- 16. elm reactorしてみる
- 17. Netlifyにデプロイ
- 18. ソースコード
- 19. GitHubモジュールを作る
開発環境
Windows 10 Pro
Chrome
VisualStudioCode 1.32.3
git version 2.20.1.windows.1
nvm 1.1.7
node 10.2.0
npm 6.4.1
elm 0.19.0-bugfix6
elm-format 0.8.1
VisualStudioCodeの拡張機能でelmをインストールして、settings.jsonに以下のようにelmを設定。
(『Alt + Shift + F』と『Ctrl + S』を使用。)
"[elm]": {
"editor.formatOnSave": true
},
elm-live 3.4.1
elm-test 0.19.0-rev6
今回作る予定のアプリの概略
3ページからなるアプリだそうです。
- GitHubのユーザ名一覧を表示(トップページ : “/")
- ユーザ名をクリックしたら、ユーザのリポジトリ一覧を表示(2ページ目 : “/{user}")
- リポジトリをクリックしたら、リポジトリのIssue一覧を表示(3ページ目 : “/{user}/{repo}" )
新規Elmアプリの作成
C:/elm/ フォルダに、 elm-github-viewer1/ フォルダを新規作成し、
C:/elm/elm-github-viewer1/ フォルダをVisualStudioCodeで開き、Ctrl+@でコマンドプロンプトを開き、以下を入力します。
elm init
elm-test init
URLパーサーの実装
URLパーサーって何?って感じですが、
parse 『解析する』
とのことで、ブラウザにURLが入力されたら、そのURLを解析して、
- URLを"/"などで分割して、どのページを表示するかを決定
- URLから”ユーザ名”などを取り出して、その『ユーザ名』をGitHub APIに問い合わせて、ユーザ名に対応するリポジトリのリストをとってくる
といった感じに、
URLから意味のあるデータを取り出す
もののようです。
今回は、以下のようなパスを用意することにします。
- /
- /{user}
- /{user}/{repo}
これらのパスを扱うためにRouteモジュールを用意します。(多少言葉が分からなくても、とにかく、『基礎からわかるElm』を写経するのみ。。。)
type Route
= Top
| User String
| Repo String String
トップページ(Top)と、ユーザページ(2ページ目)、リポジトリページ(3ページ目)のRouteを定義しているようです。
URLから、上記のRouteオブジェクト(?)を作成するための関数parse を用意します。いきなり実装は困難なので、とりあえず、Debug.todoと記載しておくようです。
parse : Url -> Maybe Route
parse url =
Debug.todo "implement parser"
なお、今回は、elm/url パッケージのUrl.Parserモジュールを使います。"elm install elm/url"でパッケージをインストールしておき、Elmファイルでは、"import Url"などをしておく必要があります。
( 『基礎からわかるElm』 p191 )
トップページのRouteのテスト
tests/Example.comを以下のように書き換えます。(
『基礎からわかるElm』 p208)
コマンドプロンプトで"elm-test"を実行します。
なんだか、 『基礎からわかるElm』 p208の結果とだいぶ異なるようです。
https://github.com/jinjor/elm-book/blob/master/4_7_navigation-github/elm.json
を見ると、elm/http, elm/json, elm/urlパッケージはインストールした方がよさそうですので、以下を実行してインストールします。
elm install elm/http
elm install elm/json
elm install elm/url
もう一度elm-test してみますが、同じエラー
import するのですね。Route と Urlをimport してみます。
Routeなんてあったかな?
Urlのimportのエラーは消えましたが、Routeがないよと言われます。
“The “source-directories" field of your elm.json tells me to look in directories"
と教えてくれているので、elm.jsonを見ると、
"source-directories": [
"src"
]
となっているので、というか、いつも通りですが、src/フォルダに、Route.elmを作成します。
src/Route.elmの新規作成
src/Route.elm
module Route exposing (Route(..), parse)
import Url exposing (Url)
import Url.Parser exposing (..)
type Route
= Top
| User String
| Repo String String
parse : Url -> Maybe Route
parse url =
Debug.todo "implement parse"
elm-test します。
やっとelm-testがまともに動きました。当然Failedですが。
最初のelm-testを通す
src/Route.elmを編集して、テストを通します。
(変更前)
parse url =
Debug.todo "implement parser"
(変更後)
parse url =
Just Top
とりあえず最初のテストをPASSED にすることはできました。
さらにtest/Example.elmのテストを増やし、それをPASSするようにsrc/Route.elmを編集していくと、以下のようになるそうです。 (『基礎からわかるElm』 p209 )
ごりごり写経していきます。
ここで、一旦、GitHubに新規リポジトリを作成して、pushしておきます。(その前に、 .gitignoreファイルを作成し、 /elm-stuff と記載しておきます。)
git init
git add .
git commit -m "first commit"
git remote add origin https://github.com/adash333/elm-github-viewer1.git
git push -u origin master
この時点でのコード
https://github.com/adash333/elm-github-viewer1/tree/2965fc90b3eae3d1d11864c78e80575cbf603418
tests/Example.elmとsrc/Route.elmを編集
『基礎からわかるElm』 p209 を写経します。
test/Example.elmを写経してからelm-testすると、以下のようになります。
さらに、src/Route.elmを写経してからelm-testすると、以下のようにテストがPASSED になります。
src/Main.elmの新規作成
src/Main.elmを新規作成します。
SPA(Single Page Application)なので、Elmの初期化方法としては、Browser.applicationを用います。
(参考:@jinjor 2018年12月03日に更新 Elm 0.19 の初期化方法 6 種類)
この時点でのソースコード
https://github.com/adash333/elm-github-viewer1/tree/9d95a04b46458dfde3ed274d4812a7f8d4e4cb96
MODELにページを定義する
このアプリが持つ状態としては、ページの種類(トップページ、2ページ目の各ユーザ名のリポジトリのリスト、3ページ目の各リポジトリのリスト、Not Found)でしょうか?
それぞれのユーザ名も『状態』になるのか、よくわかりませんが、写経していきます。
init 初期状態は、トップページにしますが、よくわかりませんが、これも写経しておきます。
type Page
= NotFound
| TopPage
| UserPage (List Repo)
| RepoPage (List Issue)
UPDATE
UPDATEのmsgとしては、まずは、以下の2つを考える必要があります。
- リンクをクリックしたとき
- ブラウザのURLが変更されたとき
type Msg
= LinkClicked Browser.UrlRequest
| UrlChanged Url.Url
具体的には、例えば、TopPage(1ページ目)の各ユーザ名をクリックしたときには、
- 2ページ目を表示する
- クリックされたユーザ名のデータをGitHub APIに送り(HTTPリクエスト)、そのユーザのリポジトリのリストをとってくる
- 何らかの原因により、GitHub APIからリポジトリのリストをとってこれなかったときは、エラーがあったことを表示する
といった流れになりそうです。ここで、HTTPリクエストが必要だということがわかるので、Msgに『HTTPリクエストが返ってきたとき(ページがロードされたとき)』を加えます。
type Msg
= LinkClicked Browser.UrlRequest
| UrlChanged Url.Url
| Loaded Page
HTTPリクエストの結果がエラーであったときは、今回は、専用のエラーページに飛ぶこととします。
type Page
= NotFound
| TopPage
| UserPage (List Repo)
| RepoPage (List Issue)
| ErrorPage Http.Error
UPDATEは大枠に記載すると以下のようになります。
msgのところを少し追加します。
Msgその1: LinkClicked Browser.UrlRequest
今回はMsgは3つとなります。1個目は、リンクがクリックされたときの、InternalリンクとExternalリンクのお決まりの書き方です。ここは毎回コピペでさそうです。( 『基礎からわかるElm』 p181 )
LinkClicked urlRequest ->
case urlRequest of
-- 内部リンクならブラウザのURLを更新する
-- (SPAなので実際に表示するページはindex.htmlのまま)
Browser.Internal url ->
( model, Nav.pushUrl model.key (Url.toString url) )
-- 外部リンクなら通常の画面遷移を行う
Browser.External href ->
( model, Nav.load href )
Msgその2: Loaded result
ページの内容を非同期で取得したときの共通処理を記載します。
『非同期』ユーザには便利そうですが、実装する側は大変です。私はよくわかっていません。写経します。でも、これも毎回コピペでやさそうな気がします。
Loaded result ->
( { Model
| page =
case result of
Ok page ->
Page
Err e ->
-- 失敗したときはエラー用のページ
ErrorPage e
}
, Cmd.none
)
Msgその3: UrlChanged Url.Url
移動前のページのリンクがクリックされた後、ブラウザのURLが変更されたときの挙動の実装を行います。
コードがすごく長くなるので、ページの初期化処理を、ヘルパー関数
goTo (Route.parse url) model
( Route.parse url を maybeRoute とします )
に移譲します。goTo関数は、URLを解析したものと、モデルから新しいモデルを作成します。(当たり前か、、、)
(変更前)
(変更後)
maybeRouteがRoute.Userであったとき(2ページ目、各ユーザのリポジトリ一覧を表示するページのとき)は、
- GitHub APIに『ユーザ名』を含んだHTTPリクエストを送り、
- 『ユーザ名』に対応してリポジトリのリストを受け取り、
- 『ページがロードされたというMsg』にリポジトリのリストを送る
httpとか、>> とか、さっぱりわかりませんが、やっぱりゴリゴリ写経です。
-- Route.Userだった場合
Just (Route.User userName) ->
( model
, Http.get
{ url =
Url.Builder.crossOrigin "https://api.github.com"
[ "users", userName, "repos"]
[]
, expect =
Http.expectJson
(Result.map UserPage >> Loaded)
reposDecoder
}
)
はい、ここ、分かりません。いろいろ勉強しないと理解できなさそうです。。。まあ、いつか。。。
Decoderは、JSON(文字列)を、Elmが分かるデータに変換する関数だそうです。後の方で、Decoderも定義します。Json.Decodeを用います。
- Http (2.0.0)
- Http.get
- Http.expectJson
- >>
- map
- Decoder (Json)
参考1: https://qiita.com/ababup1192/items/b03fce202e1018bc4992
参考2: https://package.elm-lang.org/packages/elm-lang/core/latest/Basics#(%3E%3E)
maybeRoute(Route.parse url)がRoute.Repoだった場合 は、Route.Userの場合の引数が1個から2個になるだけなので、本質的には同じだと思いますので、目をつむってコピペしておきます。
GitHub APIとのデータのやり取りを行うときの方typeの定義とDecoderの定義を行います。
はい、コピペです。リポジトリRepoの型typeとDecoderは以下のように定義します。
import Json.Decode as D exposing (Decoder)
-- GITHUB
type alias Repo =
{ name: String
, description: String
, language: String
, owner: String
, fork: Int
, star: Int
, watch: Int
}
reposDecoder : Decoder (List repo)
reposDecoder =
D.list repoDecoder
repoDecoder : Decoder Repo
repoDecoder =
D.map7 Repo
(D.field "name" D.string)
(D.field "description" D.string)
(D.maybe (D.field "language" D.string))
(D.at [ "owner", "login" ] D.string)
(D.field "forks_count" D.int)
(D.field "stargazers_count" D.int)
(D.field "watchers_count" D.int)
出ました、map関数。。。そっとしておきます。通常のパッケージでは、map8 までだそうです。(map9以上を使用する場合は別のパッケージをインストールする必要あり。)
参考:https://qiita.com/ymtszw/items/1cabbdbda4273b4c1978
issuesDeocderは、reposDecoderと本質的に同じなので、目をつむって写経しておきます。
ここまでのソースコード
https://github.com/adash333/elm-github-viewer1/tree/
VIEW
最後に、場合分けしてページを表示します。
- トップページ
- 2ページ目(指定したユーザのリポジトリ一覧)
- 3ページ目(指定したユーザ/リポジトリのissue一覧)
- NotFoundのページ
- Errorページ(HTTPリクエストがうまくいかなかったとき?)
なお、今回は、トップページに表示するユーザ名は"elm"と"evancz"(Elmの作者)とします。
写経します。
-- VIEW
view : Model -> Browser.Document Msg
view model =
{ title = "My GitHub Viewer"
, body =
[ a [ href "/" ] [ h1 [] [text "My GitHub Viewer1"] ]
-- 場合分けしてページを表示
, case model.page of
NotFound ->
viewNotFound
ErrorPage error ->
viewError error
TopPage ->
viewTopPage
UserPage repos ->
viewUserPage repos
RepoPage issues ->
viewRepoPage issues
]
}
あとは、それぞれのページのVIEWを書いていくことになります。その後、保存するたびに、いろいろコンパイルエラーを指摘されるので、大文字や小文字、タイプミスをいろいろ直していきます。
型の定義を間違えたり、VisualStudioCodeが勝手に文字を大文字にしたり、変換したりしたのを直したり、苦労しますが、なんとかコンパイルエラーをゼロにすることができました。
“type mismatch"のエラーが多く、型typeを明確にすることにより、コンパイルエラーを事前に察知することができるのだなあと、思ったり思わなかったりしました。
elm reactorしてみる
ここまで数時間。。。elm reactorしてみます。elm reactorは複数elmファイルに対応しているのでしょうか?きっと対応していますよね?
elm reactor
ブラウザで、http://localhost:8000 を開きます。
src -> Main.elm とクリックしていくと、一瞬、ユーザ名2つが表示されるのですが、すぐにBadStatus 404と表示されてしまい、一瞬がっくり。
とりあえず、My GitHub Viewer1のリンクをクリックします。
すると、大丈夫そうでした。この後、リポジトリ一覧、issue一覧も表示することができました。
写経しただけなのに、すっごく疲れました。まだHttpやDecoder, map, >> も理解できていないし。。。
しかし、これで、Elmで複数ページのアプリを作るのに一歩近づけた気がします。(IonicとかVueなら超簡単なのに、、、なぜ自分はElmやっているのでしょうか。。。まあ、Elmがなんとなく、楽しそうだからなのですが、、、)
ここまでのソースコード
https://github.com/adash333/elm-github-viewer1/tree/cad6f25cd24d71bd4c7237e402a07b06b53a8ef1
Netlifyにデプロイ
Netlifyにデプロイするために、index.htmlを作成します。
ターミナル画面で以下を入力して、ブラウザで見ながら、Main.elmのVIEWのところをちょこっとBulmaで装飾します。
そのとき気付いたのですが、トップページでevanczをクリックすると、以下のようなエラーになってしまいました。Ctrl+Shift+I でconsoleを見たり、いろいろしましたが、原因は分かりませんでした。がっくし。。。
とりあえず、このときのソースコードを以下に保存しておきます。
https://github.com/adash333/elm-github-viewer1/tree/6fda359287296c8697059d8bb4907828b18daae1
仕方がないので、サポートページのコードのsrc/Main.elmをコピペします。目視でいろいろ差分を見ようとしたのですが、見つけられなかったので、あきらめて『コピーアンドペースト』します。
https://github.com/jinjor/elm-book/blob/master/4_7_navigation-github/src-v1/Main.elm
えっと、結局、同じエラーが出ました。これはあきらめた方がよさそうです。
ちなみに、
https://github.com/jinjor/elm-book/tree/master/4_7_navigation-github/src-v2 のコードをelm reactorで試してみると、evanczのrepoもちゃんと表示できました。
https://github.com/jinjor/elm-book/blob/master/4_7_navigation-github/src-v2/GitHub.elm を見ながら、 repoDecoder = のところを少し変えてみます。
(変更前)
(変更後)
type alias Repo = のところも、 description : Maybe String にしておきます。そのあと、elm reactorしてからブラウザでsrc/Main.elmを見ると、evanczのrepoもちゃんと表示されました。
GitHub APIから受け取ったJSONの中の
“description"
をデコードするときに、Maybe Stringにしておかないと、上記のようなエラーになるそうです。なぜかは分かりませんが、evanczとelmのdescriptionの違いを見てみると、ピリオドが入っているかとか、そんな感じでしょうか(他にもいろいろ試してみたのですが、descriptionがnullだったり、やはり中にピリオドや` が入っていたりするとダメそうでした。)。
残念ながら、このDecodeのところは、コンパイル時には見つけらないエラーということなのでしょうか。
ちなみに、elmの場合のdescriptionは以下のようになっていました。
やっとエラーが解決されたので、Netlifyへのデプロイに戻ります。
netlify.tomlの新規作成
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
production用main.jsの作成(コンパイル)を行います。
ターミナル画面で以下を入力します。(本当は、さらに、uglify.jsを用いた方が、ファイルサイズが小さくなるそうですが、今回はパスします。)
elm make src/Main.elm --output=main.js --optimize
あ、そういえば、どこかで、–optimizeするときは、コードからDebugを除いておかないとダメって書いてありました。コード内のDebugのところを削除してあら、もう一度、以下を入力します。
elm make src/Main.elm --output=main.js --optimize
この時点でのソースコード
https://github.com/adash333/elm-github-viewer1/tree/cf793f768b2b38b1c5f12918c74062d7c5ea17d4
https://www.netlify.com/ にログインして、New Site from Gitから、上記GitHubのコードを指定してデプロイします。
Build commandとPublish directoryは空欄のままです。
無事、デプロイできました。
https://modest-kalam-a2885a.netlify.com/
ソースコード
ソースコード
https://github.com/adash333/elm-github-viewer1/tree/63ea51facae3f9698cc96766ea11730bc74f4639
DEMOサイト
https://modest-kalam-a2885a.netlify.com/
GitHubモジュールを作る
src/Main.elmが肥大化したので、GitHubに関するロジックを別のファイルGitHub.elmに分離します。
Main.elmに必要なものは、2つの型Issue, Repoと、それらを取得する getIssues, getReposとなります。
src/GitHub.elm を新規作成します。
— TYPESと– DECODERはMain.elmからコピペし、新たにgetReposとgetIssuesを以下のように定義します。
なんか、ここ、難しそうです。
-- API
getRepos : (Result Http.Error (List Repo) -> String -> Cmd msg)
getRepos toMsg userName =
Http.get
{ url =
Url.Builder.crossOrigin "https://api.github.com"
[ "users", userName, "repos" ]
[]
, expect =
Http.expectJson toMsg reposDecoder
}
getIssues : (Result Http.Error (List Issue) -> msg) -> String -> String -> Cmd msg
getIssues toMsg userName projectName =
Http.get
{ url =
Url.Builder.crossOrigin "https://api.github.com"
[ "repos", userName, projectName, "issues" ]
[]
, expect =
Http.expectJson toMsg issuesDecoder
}
上の方で、必要なものをimportしておきます。
import Http
import Json.Decode as D exposing (Decoder)
import Url
import Url.Builder
上記GitHub.elmを用いるために、Main.elmを編集します。
import GitHub exposing (Issue, Repo)
(変更前)
(変更後)
なんかコードがすっきりしています。その他、コンパイルエラーを直して、再度、以下を入力して、ちゃんと動くことを確認したのち、git pushします。
elm-live src/Main.elm --open -- --output=main.js
elm make src/Main.elm --output=main.js --optimize
ソースコードは以下となりました。
https://github.com/adash333/elm-github-viewer1/tree/0a7ecdffd375224a7367c63a7f3a44fbb75a65b8
DEMOサイト
https://modest-kalam-a2885a.netlify.com/ もちゃんと動いているのを確認して、今回はこれでおしまいです。
ディスカッション
コメント一覧
まだ、コメントがありません