ナビゲーション
さきほどは単一のページをどのようにサーバから送信するのかを見てきましたが、そういえばここではpackage.elm-lang.org
のようなウェブサイトを作っているのでした。そのようなウェブサイトにはたくさんのページがあり(たとえば検索やREADME、ドキュメント)、それぞれ異なる動作をしています。このような URL の異なる複数のページを持つようなサイトを Elm で作るには、どのようにすればいいのでしょうか?
複数のページ
簡単な方法としては、ページごとにそれぞれ異なる HTML ファイルをサーバから送信するというものがあるでしょう。サイトのホームページに行きますか?それでは新しい HTML を読み込みましょう。今度はelm/core
ドキュメントへ行きますか?では新しい HTML を読み込みましょう。次はelm/json
へ行くのですか?では新しい HTML を読み込みます、というようにです。
Elm 0.19 まで、このパッケージウェブサイトがしていたことが、まさにそれでした! これはうまく動きますし、シンプルです。でもいくつか弱点もあります。
- 空白の画面。新しい HTML が読み込まれるたびに、画面は真っ白になります。代わりに滑らかな遷移をすることはできるでしょうか?
- 冗長なリクエスト。 どのパッケージもそのパッケージに含まれるすべてのモジュールのドキュメント情報をひとつにまとめて格納した
docs.json
ファイルを持っていますが、String
やMaybe
のような各モジュールのページを移動するたびに毎回新しい HTML を読み込むと、このdocs.json
も繰り返し読み込まれます。どうにかしてこのデータを各ページで共有することはできないものでしょうか? - 冗長なコード。『サイトのホームページ』と『ドキュメント』は
Html.text
やHtml.div
といった多くの関数を共有しています。ページ間でこのコードを共有することはできるでしょうか?
これらすべての点を改良することができます!根本のアイデアは、HTML は一度だけ読み込み、URL の変更をちょっと巧妙に操るというものです。
単一のページ
URL が変わるたびに新たな HTML を読み込むのを避けるため、Browser.element
やBrowser.document
を使ったプログラムを作成する代わりに、Browser.application
を使うといいでしょう。
application :
{ init : flags -> Url -> Key -> ( model, Cmd msg )
, view : model -> Document msg
, update : msg -> model -> ( model, Cmd msg )
, subscriptions : model -> Sub msg
, onUrlRequest : UrlRequest -> msg
, onUrlChange : Url -> msg
}
-> Program flags model msg
これは、次の3つの重要な状況において、Browser.document
の機能を拡張するものです。
アプリケーションが開始したとき、init
はブラウザのナビゲーションバーから現在のUrl
を取得します。これで、Url
の内容に沿ってそれぞれ異なる表示をするということが可能になります。
<a href="/home">Home</a>
のようなリンクをクリックしたとき、それをUrlRequest
として傍受します。いろいろな欠点がある HTML の再読み込みをするのではなく、onUrlRequest
はupdate
へメッセージを送り、次に何をするのかを細かく決定することができるようにします。スクロール位置を保存したり、データを永続化したり、URL を自分自身で変更したりなどです。
URL が変更されたとき、新しいUrl
がonUrlChange
へと送信されます。このメッセージはupdate
へ送信され、そこで新しいページをどのように表示するのかを決定することができます。
これら3つの仕組みによって、新しい HTML を読み込むのではなく、URL の変更について完全な制御ができるようになります。それでは実際のコードを見て行きましょう!
例
次のようなBrowser.application
の基本的なプログラムをもとにしてはじめましょう。これは現在の URL を追跡していくだけのものです。コードを流し読みしてみましょう!update
関数ではいろいろな新しいことや面白いことがたくさん起こっていますので、このコードを見たあとでそれらの詳細に踏み込んでいきましょう。
import Browser
import Browser.Navigation as Nav
import Html exposing (..)
import Html.Attributes exposing (..)
import Url
-- MAIN
main : Program () Model Msg
main =
Browser.application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, onUrlChange = UrlChanged
, onUrlRequest = LinkClicked
}
-- MODEL
type alias Model =
{ key : Nav.Key
, url : Url.Url
}
init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
( Model key url, Cmd.none )
-- 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 ->
( { model | url = url }
, Cmd.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
-- VIEW
view : Model -> Browser.Document Msg
view model =
{ title = "URL Interceptor"
, body =
[ text "The current URL is: "
, b [] [ text (Url.toString model.url) ]
, ul []
[ viewLink "/home"
, viewLink "/profile"
, viewLink "/reviews/the-century-of-the-self"
, viewLink "/reviews/public-opinion"
, viewLink "/reviews/shah-of-shahs"
]
]
}
viewLink : String -> Html msg
viewLink path =
li [] [ a [ href path ] [ text path ] ]
このupdate
関数ではLinkClicked
かUrlChanged
のどちらかを扱うことができます。LinkClicked
の分岐のほうには新しい要素がたくさんありますので、まずはそちらに注目していくことにしましょう!
UrlRequest
<a href="/home">/home</a>
のようなリンクがクリックされると、UrlRequest
の値が生成されます。
type UrlRequest
= Internal Url.Url
| External String
Internal
バリアントは同じドメイン内に留まるリンクがクリックされたときを表しています。たとえばもしhttps://example.com
を閲覧しているなら、settings#privacy
や/home
、https://example.com/home
、//example.com/home
は内部リンクとなります。
External
バリアントは異なるドメインへのリンクを表しています。https://elm-lang.org/examples
やhttps://static.example.com
、http://example.com/home
などはすべて、異なるドメインへのリンクです。プロトコルがhttps
からhttp
へと変わるリンクは異なるドメインであると見なされることに注意してください!
誰かがリンクを押すと、このサンプルプログラムはLinkClicked
メッセージを生成し、それをupdate
関数へと送信するでしょう。これでこの新しいコードの全体を確認できました!
LinkClicked
このupdate
関数の大部分では、これらのUrlRequest
の値によって何を行うかが決定されます。
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 ->
( { model | url = url }
, Cmd.none
)
特に興味深い関数はNav.load
とNav.pushUrl
です。これらはどちらもBrowser.Navigation
モジュールで定義されているものですが、それぞれ異なる方法で URL を変更します。このモジュールで最もよく使われるふたつの関数です。
load
は新たな HTML を読み込みます。これは URL バーに URL を入力してエンターキーを押したのと同じです。Model
に何が起こっていようがすべて投げ捨てて、新たなページ全体が読み込まれます。pushUrl
は URL を変更しますが、新たに HTML を読み込むことはしません。その代わり、UrlChanged
メッセージを引き金にして、独自に動作を制御できます!これは『ブラウザ履歴』に URL を追加しますので、『進む』あるいは『戻る』ボタンを押したときもちゃんと動作します。
これでupdate
関数を見に戻ってみると、これらを総合してどのように改良されたのかがわかるようになったと思います。ユーザがhttps://elm-lang.org
へのリンクをクリックしたときは、External
メッセージを受け取り、load
関数を使って新しい HTML をサーバから読み込みます。それに対して、ユーザが/home
へのリンクをクリックしたときは、Internal
メッセージを受け取り、pushUrl
関数を使って新たな HTML を読み込むことなくURL が変更されます。
Note 1: このサンプルでは
Internal
リンクとExternal
リンクのどちらもコマンドを直ちに生成していますが、これは必須ではありません!External
リンクがクリックされたとき、別のページに遷移する前にテキストボックスの内容をデータベースに保存したいというような場合もあるでしょう。Internal
リンクがクリックされたときは、あとで『戻る』で戻ってきたときのためにgetViewport
を使ってスクロール位置を保存しておきたくなるかもしれません。これらはどちらも可能です! これはupdate
関数では普通のことで、ナビゲーションを先送りにし、やりたいことをなんでもすることができます。Note 2: もし『戻る』で戻ってきたときに、以前見ていた状態をそのまま再現したいなら、スクロール位置を保存するだけでは完璧とは言えません。もしブラウザの大きさを変えたりデバイスの向きを変えたりすれば、ぜんぜん違ったものになってしまうかもしれません!そうではなく『以前見えていた状態』を保存するのがいいでしょう。もしかしたらそれは、
getViewportOf
を使って、その瞬間に画面に見えているものが何なのかを調べるということかもしれません。詳細はそれぞれのアプリケーションの動作しだいですので、これ以上アドバイスすることはできません!
UrlChanged
UrlChanged
メッセージを受け取る方法はいくつかあります。pushUrl
がこれを生成することはこれまで見てきましたが、ブラウザの『戻る』や『進む』ボタンでも同じようにこのメッセージを生成します。そして先ほどの Note 1 で述べたように、LinkClicked
メッセージを受け取ったからといって、pushUrl
コマンドをすぐに実行するようなコードにはなっていないこともあります。
UrlChanged
メッセージを LinkClicked
やブラウザバックの動作などとは独立したメッセージにしておくことで、いつどのように URL が変更されたのかについては気にしないで常に「ページの遷移が実際に起こった後に何をするか」だけを考えればよくなります。
このサンプルでは新しい URL を保持しているだけですが、実際の Web アプリケーションでは、URL を構文解析してどんな内容を表示するのかをわかるようにする必要があります。これについては次のページで見ていきましょう!
Note: より重要な概念に注目するため、
Nav.Key
についての説明は飛ばしました。でも、興味がある人のために、ここで説明しておきます。ナビゲーション『キー』(
Key
)は、URL を変更する(pushUrl
のような)ナビゲーションコマンドを生成するのに必要です。Browser.application
でプログラムを作成したときだけKey
を取得することができ、プログラムが URL の変更を検出する用意があることを保証します。もしKey
の値がほかのプログラムから利用可能だとすると、不注意な開発者がやっかいなバグを引き起こし、いろんな技巧を苦労して学ぶはめになるのはまず間違いないでしょう。 このような理由により、このKey
とModel
を結びつけています。とてもややこしい問題を避けるようにする方法としては、比較的安価な代償だと言えるでしょう。