ナビゲーション
さきほどは単一のページをどのようにサーバから送信するのかを見てきましたが、そういえばここでは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を結びつけています。とてもややこしい問題を避けるようにする方法としては、比較的安価な代償だと言えるでしょう。