我希望有的基本 Elm 示例

2025-06-10

我希望有的基本 Elm 示例

注意:您无需具备太多 Elm 知识即可从中受益,但同样,这并非面向 Elm 零基础用户。它更适合那些刚刚开始学习该语言基础知识,并试图理解如何处理“组件间通信”(或者更确切地说,为什么不这样想呢!)的人。

2018 年 10 月 9 日更新:已更新至 Elm 0.19

今天,我正在用 Elm 重新创建一个示例应用程序(原始示例是在 React+Redux 中),并意识到我编写了 Elm 示例,我希望在我第一次开始探索该语言时就拥有它,所以我认为它可能值得与更广泛的受众分享。

初始设置

如果您的机器上尚未安装 Elm,您可能需要立即查看安装说明

安装 Elm 后,在您选择的终端中运行以下命令elm init来初始化您的新项目。这将为elm.json您创建一个文件及其src目录。请注意,这不会为您创建应用程序目录,因此请确保您此时位于您实际希望应用程序所在的目录中。

Main.elm首先在目录中创建一个文件src,然后在您喜欢的任何编辑器中打开它(尽管如果您需要推荐,Visual Studio Code 和Elm 语言插件非常好用),然后将模块声明添加到顶部:

module Main exposing (..)
Enter fullscreen mode Exit fullscreen mode

 Elm 架构简介

Elm 应用程序的关键是 Elm 架构,至少在完全不与外界对话的应用程序中,它看起来像这样:

这意味着我们需要一个Model,一个View和一个Update。但是这些是什么呢?

模型

我们Model将在这里定义state应用程序的。这个例子背后的核心思想是,首先考虑应用程序的状态,不过我们稍后会更详细地讨论这一点。

 看法

我们的view是一个函数,它接受一个,并返回 Elm 中DOMModel的表示。Elm 包含一个虚拟 DOM 以及一个 HTML 库,让您能够编写自己的标记,同时充分利用 Elm 提供的保证。

更新

我们的update函数将负责处理对我们的 的任何更新Model。需要注意的是,尽管名称暗示了这一点,但实际上并没有对模型进行任何更新 - 相反,update它会返回一个新模型,并将其传递给我们的view函数,从而允许在我们的应用程序中呈现任何必要的更改。

我们要构建什么

好了,说完了这些,我们来看看我们的任务。我们要搭建一个高科技、最先进的虚拟房间。这个房间将包含一扇门和一个报警系统。具体要求如下:

If the alarm is armed, then it should be triggered by the door opening.
If the alarm has been triggered, then it can be disarmed, but not armed.
If the door is open, the alarm's current state can not be altered manually.
If the door is open it can be closed.
If the door is closed it can be opened or locked.
If the door is locked it can be unlocked.
Enter fullscreen mode Exit fullscreen mode

所以我们知道,我们需要一个房间,里面有两个东西,每个东西都可以用多种可能的状态来表示,我们知道可能的状态组合是什么,也知道这些可能状态之间的转换关系。在我看来,我们正在寻找:

-- POSSIBLE STATES:

Door: 
  Locked
  Closed
  Opened

Alarm:
  Armed
  Disarmed
  Triggered

Combined: 
  Locked + Armed
  Locked + Triggered
  Locked + Disarmed
  Unlocked + Armed
  Unlocked + Triggered
  Unlocked + Disarmed
  Opened + Triggered
  Opened + Disarmed

-- POSSIBLE TRANSITIONS: 

Door: 
  Closed <-> Locked
  Closed <-> Opened

Alarm: 
  Armed -> Triggered
  Triggered -> Disarmed
  Armed <-> Disarmed
Enter fullscreen mode Exit fullscreen mode

但是整个应用程序呢?理想情况下,它似乎只能存在于一种状态:房间正在显示的状态。但为了保险起见,我们考虑一下,我们可能会在某个时刻引入一些逻辑错误,如果出现这种情况,我们可能需要一些提示来了解发生了什么。因此,我们假设这个应用程序有两种总体状态可能性:DisplayingRoom,它需要同时感知DoorAlarm,以及Failure,它需要某种消息来告诉我们它是如何到达那里的。

嗯,感觉就像我们刚刚描述了我们的Model,以及可能的门和警报状态。Main.elm现在让我们将所有这些添加到我们的文件中:

type Model 
  = DisplayingRoom DoorState AlarmState
  | Failure String

type DoorState
  = Opened
  | Closed
  | Locked

type AlarmState
  = Armed
  | Disarmed
  | Triggered
Enter fullscreen mode Exit fullscreen mode

这就是在 Elm 中创建的方法custom type,以上所有内容都是自定义类型的示例。

update编写函数  代码

在这个阶段,由于我们提前做好了所有规划,我们也掌握了编写函数所需的所有信息update。更新需要接受两个参数:a message(需要发生的转换的描述)和 the (应用更新之前的model模型),它会返回一个新的模型。

那么,我们的转换将采用的形式messages,并且再次感谢我们对问题的前期思考,我们已经知道它们将是什么,所以Main.elm现在让我们将它们添加到我们的文件中:

type Msg
    = Open
    | Close
    | Lock
    | Unlock
    | Arm
    | Disarm
Enter fullscreen mode Exit fullscreen mode

(如果您想知道为什么我们没有Trigger消息,那是因为我们的规范中没有任何方法可以手动触发警报!)

不过,我们还没看到任何东西,是不是很奇怪?嗯,一开始确实有点奇怪。以我的经验来看,先整理好数据,再理清外观,效果会很好。

好了,回去工作!我们的update函数将接受 amessage和 a model,并返回一个新的model。我们首先检查应用程序的状态(即 的值Model):

update msg model =
  case model of
    DisplayingRoom doorState alarmState ->
      model
    Failure errorMessage -> 
      model
Enter fullscreen mode Exit fullscreen mode

如果我们处于失败状态,update函数将无法恢复,因此它只能返回model,这就是我们Failureupdate函数中完成的操作。

那么,当我们展示房间时,我们对可能的状态组合了解多少呢?我们知道,当门打开时,我们的约束最多——事实上,从那个状态开始,我们只有一种可能的转换,因为在门关闭之前,我们无法与闹钟互动。现在让我们添加它:

update : Msg -> Model -> Model
update msg model =
    case model of
        DisplayingRoom doorState alarmState ->
          case doorState of
            Opened ->
              case msg of
                Close ->
                  DisplayingRoom Closed alarmState

                _ ->
                  Failure "unexpected message received while door was in Opened state"

        Failure _ -> 
          model
Enter fullscreen mode Exit fullscreen mode

Close现在,当门处于状态时,我们正在处理我们的消息Opened,并且我们说在这种情况下,我们想要返回的模型是DisplayingRoom Closed alarmState-是保存警报传递到模型时的状态的变量的名称;这确保了当门从到时alarmState,它被传递到新模型而不会被改变OpenedClosed

我们还希望,当门处于此状态时,收到的任何其他消息Failure,都应将应用程序置于此状态,并附带一条消息,说明门是如何到达此状态的。我们将以某种方式编写视图,使此处不可能出现其他消息,但至少现在,如果我们犯了错误,我们可以轻松地获取该信息。

接下来,我们来处理Closed门的状态。在本例中,我们知道如果门关闭了,并且我们收到了一条Open消息,那么我们就需要关注警报的状态。我们也知道,当门关闭时,我们也可以手动更改警报的状态。所以,让我们将所有这些添加到:

                Closed ->
                  case msg of
                    Open ->
                      case alarmState of
                        Armed ->
                          DisplayingRoom Opened Triggered

                        _ ->
                          DisplayingRoom Opened alarmState

                    Lock ->
                      DisplayingRoom Locked alarmState

                    Arm ->
                      DisplayingRoom Closed Armed

                    Disarm ->
                      DisplayingRoom Closed Disarmed

                    _ ->
                      Failure "unexpected message received while door was in Closed state"
Enter fullscreen mode Exit fullscreen mode

好的,现在如果我们的门在警报启动时被打开,就会触发警报。我们现在也在处理与警报相关的消息。所以剩下要添加的就是在门启动时可能出现的消息locked

                Locked ->
                  case msg of
                    Unlock ->
                      DisplayingRoom Closed alarmState

                    Arm ->
                      DisplayingRoom Locked Armed

                    Disarm ->
                      DisplayingRoom Locked Disarmed

                    _ ->
                      Failure "unexpected message received while door was in Locked state"
Enter fullscreen mode Exit fullscreen mode

这就是我们的update功能!现在我们已经添加了所有描述数据如何表示以及所有可以更改的方式,是时候添加一些view代码来查看它们是否正常工作了。

view编写函数代码

正如我之前提到的,Elm 有一个HTML库,它允许我们在 Elm 代码中编写 HTML。现在,在

import Html exposing (..)

模块声明下方添加即可导入它。

让我们从最简单的部分开始:我们的failure函数。我们知道在这个状态下我们会从模型中收到一条消息,所以让我们创建一个函数来显示它:

failure message =
    div []
        [ p [] [ text message ] ]
Enter fullscreen mode Exit fullscreen mode

接下来,让我们创建我们的Door。我们知道它在关闭时需要两条可能的消息,但在打开或锁定时只需要一条消息。根据传入的内容,用一个函数来表示所有这些不同的状态很容易,但坦白地说,分别创建它们并不需要太多代码,而且随着我的高科技门和警报应用程序的扩展,谁知道它们会有多大的差异——我们不要过早地优化自己,让自己陷入困境。看看我在 Github 上的door 模块,你会发现我们已经拥有满足验收标准所需的所有门功能。

现在我们要对我们的 做同样的事情Alarm,你也可以在 Github 上查看。你会注意到,在这种情况下,我们要说的是 Alarm 不仅需要知道message它能够发送 (记住,我们的警报只能根据应用程序所处的状态发送一条消息 - 它永远不可能同时处于 和 的状态armdisarm,还需要知道是否允许执行任何操作。这使我们能够禁用任何警报消息的发送,但至关重要的是,Alarm模块本身不需要从任何地方接收任何消息来实现这一点。我们将在 中连接我们的view函数时处理这个问题Main.elm,我们现在就这样做。

记得将import模块也导入到你的 中Door如果你的设置方式与我相同(即,并且每个模块都根据门/警报状态公开单独的函数),则可以按如下方式导入它们:AlarmMain.elmView/Door.elmView/Alarm.elm

import View.Alarm as Alarm exposing (armedAlarm, disarmedAlarm, triggeredAlarm)
import View.Door as Door exposing (closedDoor, lockedDoor, openDoor)
Enter fullscreen mode Exit fullscreen mode

我们希望显示 ourdoor和 our alarm,并确保它们根据应用程序的状态以正确的状态显示。现在我们知道了应用程序的初始状态,以及之后可能的状态,并且我们已经构建了允许我们显示所需内容的模块,因此我们只需要在中间添加一些代码!我们的view函数Main.elm应该如下所示:

view : Model -> Html Msg
view model =
    case model of
        Failure message ->
            failure message

        DisplayingRoom doorState alarmState ->
            div
                []
                [ div
                    [ class "doorPanel" ]
                    [ case doorState of
                        Opened ->
                            openDoor Close

                        Closed ->
                            closedDoor Open Lock

                        Locked ->
                            lockedDoor Unlock
                    ]
                , div
                    [ class "alarmPanel " ]
                    [ case alarmState of
                        Armed ->
                            armedAlarm Disarm (doorState /= Opened)

                        Disarmed ->
                            disarmedAlarm Arm (doorState /= Opened)

                        Triggered ->
                            triggeredAlarm Disarm (doorState /= Opened)
                    ]
                ]
Enter fullscreen mode Exit fullscreen mode

在这里您可以看到,我们正在检查doorState从 传出的model,并根据门的状态调用相应的函数以及相应的消息。我们对 也做了同样的事情alarmState,并在每个警报函数中传递了正确的消息以匹配函数中我们期望的行为,并且我们还传递了对 的检查结果doorState /= Opened,因为我们告诉alarm函数需要布尔值来表明是否允许任何操作。

 连接运行时

我们需要 Elm 的核心Browser模块才能使其在运行时执行任何操作。将其添加import Browser到文件顶部Main.elm,模块声明下方。(注意:通常情况下,您需要install先添加新的包才能导入,但Browser运行时会自动安装elm init。)

Browser有一些函数,但我们这里需要的是Browser.sandbox。Sandbox 允许你创建一个使用 Elm 架构的应用程序,但它不与“外部世界”(即任何外部 API 或 JavaScript)交互。它需要接受一个record包含三个字段的参数:initupdateview。我们已经有了updateview函数,但是init用来描述应用程序的初始模型——假设我们要显示房间,门关闭,警报已启动:

initialModel : Model
initialModel = DisplayingRoom Closed Armed

main : Program () Model Msg
main =
    Browser.sandbox
        { init = initialModel 
        , view = view
        , update = update
        }
Enter fullscreen mode Exit fullscreen mode

运行我们的应用程序

好的,这就是我们需要做的一切 - 在你的终端中运行elm make src/Main.elm。这将为你提供一个index.html文件,你可以在你选择的浏览器中打开它。

 它只是...起作用了?

感觉应该还有更多事情要做,不是吗?如果你习惯了 Web 开发的“先看样子 -> 修改代码 -> 看看现在的样子”这种循环,一开始可能会有点奇怪,但我发现从下往上做比从modelview往下做更有意义,而且能让事情保持干净整洁。

我希望这对某人有用!

鏂囩珷鏉ユ簮锛�https://dev.to/nimmo/the-basic-elm-example-that-i-wish-id-had-40m8
PREV
让我们用 Python 制作一个 Twitch 机器人!
NEXT
在装有 macOS 的 iOS 设备上使用 Google Chrome 进行远程调试