导航
我们刚刚了解了如何加载一个页面,但是如果我们正在制作如 package.elm-lang.org
这样的网站,该怎么办?它拥有众多(如:搜索、自述文件和 文档)页面的工作原理都不一样。它如何做到的?
多个页面
最简单的方法是加载一组不同的 HTML 文件。进入主页?加载新的 HTML。进入 elm/core
文档?加载新的 HTML。进入 elm/json
文档?加载新的 HTML。
在 Elm 0.19 之前,这就是软件包网站所做的!它的工作原理简单。不过它也有一些缺点
- 空白屏幕。每次加载新 HTML 时,屏幕都会变白。我们可以进行良好的过渡吗?
- 冗余请求。每个软件包都有一个
docs.json
文件,但是它会在您每次访问String
或Maybe
等模块时加载该文件。我们能否在不同页面之间共享数据? - 冗余代码。主页和文档共享许多函数,如
Html.text
和Html.div
。我们能否在不同页面之间共享此代码?
我们可以改善所有这三个问题!基本思想是只加载一次 HTML,然后巧妙地处理 URL 更改。
单个页面
我们可以使用 Browser.application
来创建我们的程序,而不是使用 Browser.element
或 Browser.document
来避免在 URL 更改时加载新的 HTML
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
它扩展了 Browser.document
在三种重要情况下的功能。
当应用程序启动时,init
从浏览器的导航栏获取当前 Url
。这允许您根据 Url
显示不同的内容。
当有人单击某个链接,例如 <a href="/home">首页</a>
,它会被截获为 UrlRequest
。因此,为了不加载带有各种缺点的新 HTML,onUrlRequest
将为 update
创建一条消息,在其中你可以决定下一步要做什么。你可以保存滚动位置、保存数据、自己更改 URL 等等。
当 URL 更改时,新的 Url
会发送到 onUrlChange
。生成的邮件会传送到 update
,在其中你可以决定如何显示新页面。
因此,这些三个附加的内容让你完全控制 URL 更改,而不用加载新 HTML。让我们看看它的实际应用!
示例
我们将从基础 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
消息!它还会向“浏览器历史”添加一个条目,如此一来,人们单击BACK
或FORWARD
按钮时,各项功能依旧正常运行。
那么回头看看 update
函数,我们现在可以更好地理解它是如何组合在一起的。当用户单击 https://elm-lang.org
链接时,我们收到一条 External
消息并使用 load
从这些服务器加载新的 HTML。但是,当用户单击 /home
链接时,我们收到一条 Internal
消息并使用 pushUrl
来更改 URL,但不会加载新的 HTML!
注意 1:在我们的示例中,
Internal
和External
链接都立即产生命令,但这不是必需的!当有人单击External
链接时,也许您希望在导航离开之前将文本框内容保存到您的数据库。或者,当有人单击Internal
链接时,也许您希望使用getViewport
保存滚动位置,以防他们稍后导航到BACK
。这一切都是可能的!这是一般的update
函数,您可以延迟导航并执行任何您想做的事情。注意 2:如果您想在他们返回
BACK
时还原“他们正在查看的内容”,则滚动位置并不完美。如果他们调整浏览器大小或重新调整设备方向,它可能会有很大偏差!因此,最好还是保存“他们正在查看的内容”。也许这意味着使用getViewportOf
来准确了解当前屏幕上的内容。具体情况取决于应用程序的确切工作方式,所以我无法提供确切的建议!
UrlChanged
有几种方法可以获取 UrlChanged
消息。我们刚才看到 pushUrl
会产生这些消息,但是按下浏览器 BACK
和 FORWARD
按钮也会产生这些消息。正如我刚才在说明中所说的,当您收到 LinkClicked
消息时,可能不会立即发出 pushUrl
命令。
因此,拥有一个单独的 UrlChanged
消息的好处是,无论 URL 如何或何时更改,都没有关系。您只需要知道它已经更改了!
在本示例中,我们只是存储了新的 URL,但在真正的 Web 应用程序中,您需要解析 URL 以确定要显示什么内容。这就是下一页的主要内容!
注意:我跳过了对
Nav.Key
的讨论,以尝试专注于更重要的概念。但我将在这里为感兴趣的人进行解释!导航
Key
是创建能改变 URL 的导航命令(例如pushUrl
)的必需品。只有在用Browser.application
创建程序时才能访问Key
,以保证你的程序具备检测这些 URL 更改的能力。如果Key
值在其他种类的程序中可用,那么毫无防备的程序员肯定会遇到一些恼人的错误,并艰辛地学到许多技巧!因此,我们的
Model
为我们的Key
加了一行。这是一个相当小的代价,却能帮助每个人避免极为隐晦的一类问题!