如何使用 Elm 0.19 创建 SPA?
How to create SPA with Elm 0.19?
我正在尝试使用 Elm 构建一个 SPA 并创建三个页面,这三个页面应该显示内容,取决于 URL。
这三个页面的内容都差不多,比如Page.elm
:
module Page.NotFound exposing (Msg(..), content)
import Html exposing (..)
import Html.Attributes exposing (..)
---- UPDATE ----
type Msg
= NotFoundMsg
content : Html Msg
content =
p [] [ text "Sorry can not find page." ]
在Main.elm
中,我有以下代码:
module Main exposing (Model, Msg(..), init, main, update, view)
import API.Keycloak as Keycloak exposing (..)
import Browser
import Browser.Navigation as Nav
import Html exposing (..)
import Html.Attributes exposing (..)
import Json.Decode as Decode
import Page.Account as Account
import Page.Home as Home
import Page.NotFound as NotFound
import Route
import Url
import Url.Parser exposing ((</>), Parser, int, map, oneOf, parse, s, string)
---- MODEL ----
type alias Model =
{ key : Nav.Key
, url : Url.Url
, auth : Result String Keycloak.Struct
}
init : Decode.Value -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
( Model key url (Keycloak.validate flags), Cmd.none )
---- ROUTE ----
type Route
= Account
---- UPDATE ----
type Msg
= PageNotFound NotFound.Msg
| PageAccount Account.Msg
| PageHome Home.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 ----
info : Html Msg
info =
header [] [ text "Header" ]
createLink : String -> Html Msg
createLink path =
a [ href ("/" ++ path) ] [ text path ]
navigation : Html Msg
navigation =
ul []
[ li [] [ createLink "home" ]
, li [] [ createLink "account" ]
]
content : Model -> Html Msg
content model =
main_ []
[ case parse Route.parser model.url of
Just path ->
matchedRoute path
Nothing ->
NotFound.content
]
matchedRoute : Route.Route -> Html Msg
matchedRoute path =
case path of
Route.Home ->
Home.content
Route.Account ->
Account.content
body : Model -> List (Html Msg)
body model =
[ info
, navigation
, content model
]
view : Model -> Browser.Document Msg
view model =
{ title = "Cockpit"
, body = body model
}
---- PROGRAM ----
main : Program Decode.Value Model Msg
main =
Browser.application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, onUrlChange = UrlChanged
, onUrlRequest = LinkClicked
}
编译器抱怨:
-- TYPE MISMATCH -------------- /home/developer/Desktop/elm/cockpit/src/Main.elm
The 2nd branch of this `case` does not match all the previous branches:
104| [ case parse Route.parser model.url of
105| Just path ->
106| matchedRoute path
107|
108| Nothing ->
109| NotFound.content
^^^^^^^^^^^^^^^^
This `content` value is a:
Html NotFound.Msg
But all the previous branches result in:
Html Msg
Hint: All branches in a `case` must produce the same type of values. This way,
no matter which branch we take, the result is always a consistent shape. Read
<https://elm-lang.org/0.19.0/union-types> to learn how to “mix” types.
-- TYPE MISMATCH -------------- /home/developer/Desktop/elm/cockpit/src/Main.elm
Something is off with the 2nd branch of this `case` expression:
120| Account.content
^^^^^^^^^^^^^^^
This `content` value is a:
Html Account.Msg
But the type annotation on `matchedRoute` says it should be:
Html Msg
-- TYPE MISMATCH -------------- /home/developer/Desktop/elm/cockpit/src/Main.elm
Something is off with the 1st branch of this `case` expression:
117| Home.content
^^^^^^^^^^^^
This `content` value is a:
Html Home.Msg
But the type annotation on `matchedRoute` says it should be:
Html Msg
Detected errors in 1 module.
我知道类型有误,但不知道如何证明。
我怎样才能让它工作?
我也查看了 https://github.com/rtfeldman/elm-spa-example/blob/master/src/Main.elm 中的示例,但无法理解它是如何工作的。
您有多个 Msg
类型,这没关系,但可能会导致混淆。简而言之:Main.Msg
与 NotFound.Msg
.
不是同一类型
函数matchedRoute
returns一个Html Main.Msg
而函数NotFound.content
returns一个Html NotFound.Msg
;完全不同的类型。
你已经完成了 99%,因为你有一个 PageNotFound NotFound.Msg
类型的构造函数,它产生一个 Main.Msg
。这允许您将 包装 NotFound.Msg
在 Main.Msg
中。这应该是在您的 Nothing ->
分支中执行 PageNotFound NotFound.content
的问题。
问题是NotFound.content
引用的Msg
类型是NotFound.Msg
,Main.matchedRoute
引用的Msg
类型是Main.Msg
,并且这些不会自动统一。所以当你在 case
表达式的不同分支中使用它们时,编译器会告诉你它们是不同的,不能统一为 case
表达式到 return 的单一类型。
所以你必须将一个转换为另一个,通常的方法是在 "outer" 消息类型 (Main.Msg
) 中添加一个变体来包装 "inner" 消息类型 (NotFound.Msg
)。幸运的是,您已经将该变体添加为 PageNotFound NotFound.Msg
,因此我们可以继续。
下一步是在 PageNotFound
中包装 NotFound.Msg
。不幸的是,我们很少单独处理 NotFound.Msg
的值,它通常包含在其他类型中,例如 Html
或 Cmd
,处理起来比较棘手。幸运的是,Evan 有足够的先见之明,预见到了这种情况,并添加了 Cmd.map
和 Html.map
供我们使用。就像 List.map
和 Maybe.map
、Cmd.map
和 Html.map
接受一个函数 a -> b
并用它来转换 Html a
或 Cmd a
s 分别为 Html b
s 或 Cmd b
s。
所以,您真正需要做的就是在 NotFound.content
上使用 Html.map
和 PageNotFound
:
content : Model -> Html Msg
content model =
main_ []
[ case parse Route.parser model.url of
Just path ->
matchedRoute path
Nothing ->
NotFound.content |> Html.map PageNotFound
]
两个分支现在都将 return Main.Msg
并且编译器应该会很高兴 :)
顺便说一句,在 elm-spa-example 中,这正在完成 here
我正在尝试使用 Elm 构建一个 SPA 并创建三个页面,这三个页面应该显示内容,取决于 URL。
这三个页面的内容都差不多,比如Page.elm
:
module Page.NotFound exposing (Msg(..), content)
import Html exposing (..)
import Html.Attributes exposing (..)
---- UPDATE ----
type Msg
= NotFoundMsg
content : Html Msg
content =
p [] [ text "Sorry can not find page." ]
在Main.elm
中,我有以下代码:
module Main exposing (Model, Msg(..), init, main, update, view)
import API.Keycloak as Keycloak exposing (..)
import Browser
import Browser.Navigation as Nav
import Html exposing (..)
import Html.Attributes exposing (..)
import Json.Decode as Decode
import Page.Account as Account
import Page.Home as Home
import Page.NotFound as NotFound
import Route
import Url
import Url.Parser exposing ((</>), Parser, int, map, oneOf, parse, s, string)
---- MODEL ----
type alias Model =
{ key : Nav.Key
, url : Url.Url
, auth : Result String Keycloak.Struct
}
init : Decode.Value -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
( Model key url (Keycloak.validate flags), Cmd.none )
---- ROUTE ----
type Route
= Account
---- UPDATE ----
type Msg
= PageNotFound NotFound.Msg
| PageAccount Account.Msg
| PageHome Home.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 ----
info : Html Msg
info =
header [] [ text "Header" ]
createLink : String -> Html Msg
createLink path =
a [ href ("/" ++ path) ] [ text path ]
navigation : Html Msg
navigation =
ul []
[ li [] [ createLink "home" ]
, li [] [ createLink "account" ]
]
content : Model -> Html Msg
content model =
main_ []
[ case parse Route.parser model.url of
Just path ->
matchedRoute path
Nothing ->
NotFound.content
]
matchedRoute : Route.Route -> Html Msg
matchedRoute path =
case path of
Route.Home ->
Home.content
Route.Account ->
Account.content
body : Model -> List (Html Msg)
body model =
[ info
, navigation
, content model
]
view : Model -> Browser.Document Msg
view model =
{ title = "Cockpit"
, body = body model
}
---- PROGRAM ----
main : Program Decode.Value Model Msg
main =
Browser.application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, onUrlChange = UrlChanged
, onUrlRequest = LinkClicked
}
编译器抱怨:
-- TYPE MISMATCH -------------- /home/developer/Desktop/elm/cockpit/src/Main.elm
The 2nd branch of this `case` does not match all the previous branches:
104| [ case parse Route.parser model.url of
105| Just path ->
106| matchedRoute path
107|
108| Nothing ->
109| NotFound.content
^^^^^^^^^^^^^^^^
This `content` value is a:
Html NotFound.Msg
But all the previous branches result in:
Html Msg
Hint: All branches in a `case` must produce the same type of values. This way,
no matter which branch we take, the result is always a consistent shape. Read
<https://elm-lang.org/0.19.0/union-types> to learn how to “mix” types.
-- TYPE MISMATCH -------------- /home/developer/Desktop/elm/cockpit/src/Main.elm
Something is off with the 2nd branch of this `case` expression:
120| Account.content
^^^^^^^^^^^^^^^
This `content` value is a:
Html Account.Msg
But the type annotation on `matchedRoute` says it should be:
Html Msg
-- TYPE MISMATCH -------------- /home/developer/Desktop/elm/cockpit/src/Main.elm
Something is off with the 1st branch of this `case` expression:
117| Home.content
^^^^^^^^^^^^
This `content` value is a:
Html Home.Msg
But the type annotation on `matchedRoute` says it should be:
Html Msg
Detected errors in 1 module.
我知道类型有误,但不知道如何证明。
我怎样才能让它工作?
我也查看了 https://github.com/rtfeldman/elm-spa-example/blob/master/src/Main.elm 中的示例,但无法理解它是如何工作的。
您有多个 Msg
类型,这没关系,但可能会导致混淆。简而言之:Main.Msg
与 NotFound.Msg
.
函数matchedRoute
returns一个Html Main.Msg
而函数NotFound.content
returns一个Html NotFound.Msg
;完全不同的类型。
你已经完成了 99%,因为你有一个 PageNotFound NotFound.Msg
类型的构造函数,它产生一个 Main.Msg
。这允许您将 包装 NotFound.Msg
在 Main.Msg
中。这应该是在您的 Nothing ->
分支中执行 PageNotFound NotFound.content
的问题。
问题是NotFound.content
引用的Msg
类型是NotFound.Msg
,Main.matchedRoute
引用的Msg
类型是Main.Msg
,并且这些不会自动统一。所以当你在 case
表达式的不同分支中使用它们时,编译器会告诉你它们是不同的,不能统一为 case
表达式到 return 的单一类型。
所以你必须将一个转换为另一个,通常的方法是在 "outer" 消息类型 (Main.Msg
) 中添加一个变体来包装 "inner" 消息类型 (NotFound.Msg
)。幸运的是,您已经将该变体添加为 PageNotFound NotFound.Msg
,因此我们可以继续。
下一步是在 PageNotFound
中包装 NotFound.Msg
。不幸的是,我们很少单独处理 NotFound.Msg
的值,它通常包含在其他类型中,例如 Html
或 Cmd
,处理起来比较棘手。幸运的是,Evan 有足够的先见之明,预见到了这种情况,并添加了 Cmd.map
和 Html.map
供我们使用。就像 List.map
和 Maybe.map
、Cmd.map
和 Html.map
接受一个函数 a -> b
并用它来转换 Html a
或 Cmd a
s 分别为 Html b
s 或 Cmd b
s。
所以,您真正需要做的就是在 NotFound.content
上使用 Html.map
和 PageNotFound
:
content : Model -> Html Msg
content model =
main_ []
[ case parse Route.parser model.url of
Just path ->
matchedRoute path
Nothing ->
NotFound.content |> Html.map PageNotFound
]
两个分支现在都将 return Main.Msg
并且编译器应该会很高兴 :)
顺便说一句,在 elm-spa-example 中,这正在完成 here