创建我们的第一个 Elm 应用程序可能看起来是一项艰巨的任务。 如果您以前没有使用过其他函数式编程语言,新的语法和新的范式可能会令人生畏。 但是,一旦您创建了第一个应用程序,您就会明白为什么 Elm 最近备受关注。
文章系列
- 为什么选择 Elm? (以及如何开始使用它)
- Elm 架构简介及如何构建第一个应用程序 (您现在就在这里!)
- Elm 应用程序的结构
在本文中,我们将构建一个简单的应用程序,它将向我们介绍语言基础知识:Elm 架构以及如何开始构建内容。 它将会非常简单,但它将向我们介绍任何应用程序中最常见的任务之一:对用户操作做出反应,并在应用程序内使用它们。

它将包含一个表单,用于添加文本片段和过滤之前的条目。 还包括所有条目的列表。 所有代码的 GitHub 库 可以在本文中找到。
虽然这针对没有任何语言经验的 Elm 初学者,但我们假设您已经阅读了关于它的基本内容,例如编写函数的语法以及如何应用它们。 要了解有关此的更多信息,您可以阅读专门的 语法页面,该页面位于 Elm 文档 网站上。
介绍 Elm 架构
每个 Elm 应用程序都倾向于遵循特定的模式,以至于现在它被认为是编写 Elm 的方式。 如果您熟悉 MVC 架构,您会发现一些概念的相似之处,但如果这些概念对您来说完全陌生,请不要担心,一旦我们构建了这个应用程序,一切都将更加清晰。
在大多数应用程序中,我们将找到这三个基本部分
- 模型:
model
函数存储我们应用程序的所有状态;这是在应用程序执行期间在应用程序中四处移动的所有动态数据。 - 更新:
update
函数包含使您的应用程序动态的其他函数。 我们使用这些函数来处理我们的状态,使用完成应用程序目标所需的逻辑。 - 视图:
view
函数处理应用程序的视觉部分。 使用类似于 HTML 的较小的函数,我们可以构建应用程序的结构,将来自模型的数据嵌入其中。 我们还可以包含对将触发某些功能的事件的调用。
你好,世界!
使用 Elm 构建动态应用程序可以被认为比使用原生 HTML/JS 开始构建一个应用程序更简单。 首先,我们只需要一个文件。 我们不需要考虑我们将需要哪些其他库。
继续并使用.elm
扩展名在该项目的某个目录中创建一个文件,我们将将其称为main.elm
,尽管您可以随意命名它。 一旦您在 您喜欢的编辑器 中打开了该文件,请添加以下内容,这是一个简单的“Hello, World!”应用程序,它将帮助我们在构建真正的应用程序之前入门
import Html exposing (text)
main =
text "Hello, World!"
首先,我们导入 Html 模块,它允许我们使用类似于 HTML 的东西来编写可见的应用程序结构,并且我们公开 text 函数,它使我们能够在网站上输出纯文本。
与其他编程语言一样,main
函数是应用程序的入口点;它是程序启动后执行的第一个函数。 我们在这里做的是将"Hello, World!"
字符串作为参数传递给 text 函数,然后将其分配给main
函数。 一旦应用程序执行,它将在浏览器中显示一个简单的“Hello, World!”文本。
要查看浏览器中的结果,我们必须先编译代码。 正如文章 为什么选择 Elm? (以及如何开始使用它) 中所述,Elm 平台 包含 Reactor 实用程序,它允许我们在浏览器中查看我们的 Elm,自动编译代码。
在终端/命令行窗口中,在包含您刚刚创建的 Elm 文件的目录内执行以下命令
elm-reactor
它将启动一个开发服务器,我们可以通过在浏览器中输入http://localhost:8000
来访问它。

进入浏览器后,您将看到一个文件导航窗口。 点击main.elm 文件,等待几秒钟,直到它构建完成。 在此过程中,它还会创建一个elm-package.json 文件,它等同于 node.js 项目中的package.json
文件;它包含有关您的应用程序的基本信息,包括依赖项,在本例中,直接从我们的应用程序文件获得。 它还会下载这些依赖项并将它们放在elm-stuff 目录中。 一旦进程完成,您将在页面上看到文本“Hello, World!”。
现在我们已经确认一切正常,现在该开始构建我们的应用程序了。
添加模块并定义入口点
要使用某些函数,我们首先必须导入包含它们的模块。 将您的文件内容替换为以下内容
module Main exposing (..)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
第一行将我们的应用程序定义为一个名为Main
的模块,它将公开其中的所有内容。 虽然我们不会在外部使用该模块,但了解它很重要。 您可以在 模块 页面中阅读有关模块的更多信息,该页面来自 Elm 简介 网站。
接下来的三行导入了一些我们将在整个应用程序中使用的模块。 第一个是我们之前在 Hello World 示例中使用的模块,Html.Attributes 允许我们在 Elm 代码中使用 HTML 属性,而 Html.Events 使我们能够使用常见的事件,例如鼠标或表单操作。
在本例中,我们公开了这些组件中的所有内容,因此如果在您的工作中遇到任何名称冲突,您可以指定要导入的具体组件。
接下来是定义应用程序的入口点,将以下代码添加到您的文件中
main =
Html.beginnerProgram { model = model, view = view, update = update }
对于简单的应用程序,通常使用Html.beginnerProgram
函数,它可以帮助我们将程序组合在一起,该程序由前面提到的三个主要组件组成:模型、视图和更新。
名称的重复可能一开始看起来有点多余(model = model
等),但这是因为我们将命名我们的模型函数为model
,我们的视图为view
,更新为update
,就像通常在其他 Elm 程序中一样(并且推荐这样做),但这并不是严格的规则;您也可以使用以下内容
main =
Html.beginnerProgram { model = mydata, view = myhtml, update = mylogic }
但是,如果您决定在程序中这样做,请记住相应地命名函数定义。
模型
在我们的模型中,我们将放置整个程序的状态,因此首先,我们必须考虑要存储和使用的事物。
我们可以将Model
描述为 记录 的
entries
:包含所有条目的字符串列表。results
:包含已过滤条目的字符串列表。filter
:包含过滤器的字符串。
为了指示我们 Elm 程序中的信息类型,我们将创建所谓的 类型别名 用于我们的model
函数,顾名思义,它将允许我们以更简单的方式表示复杂的数据结构。
在您之前定义的main
函数下方包含以下代码
type alias Model =
{ entries : List String
, results : List String
, filter : String
}
如您所见,代码非常具有表现力,与我们之前创建的描述没什么不同,只是更简洁。 现在我们可以使用Model
(类型始终大写)作为函数的类型,而无需手动编写所有结构;这就是我们使用类型别名的原因,使事情更清晰、更易于理解且重复性更低。
虽然可以推断类型,但最好在编写实际函数之前编写它们;它可以帮助我们思考并从一开始就进入正确的位置。 此外,如果我们在函数内部做了错误的事情,编译器会告诉我们。
现在我们可以编写我们的函数,其中将包含我们刚刚定义的信息,将以下内容放在您的文件中
model : Model
model =
{ entries = []
, results = []
, filter = ""
}
第一行告诉 Elm,我们的 model
函数的类型是 Model
,这是我们之前定义的类型,接下来我们定义实际的函数。但因为我们的应用程序在没有任何数据的情况下启动,我们只包含空列表 ([]
)。一旦用户开始与应用程序交互,这些值就会改变。
严格来说,在 Elm 中,值不会改变(它们是 不可变的),而是会创建新的值,虽然这对我们来说大多是透明的。
就这样!现在我们应用程序的状态在一个中心位置表示,但我们仍然需要实现将与该状态一起工作的函数,并使应用程序的内部工作。
更新
我们程序中的所有逻辑都由一个名为 update
的单一函数处理,该函数将接收一个名为 [消息] 的东西——我们可以将其视为对可以做出反应的操作的容器——以及我们当前的模型,并返回一个新模型,其中包含根据应用的操作进行的更改。
我们的应用程序将有两个操作,一个是将项目从表单输入中输入到字符串列表中,另一个是过滤这些条目。为了在 Elm 中定义它,我们可以将以下内容添加到我们的文件中
type Msg
= Filter String
| Add
这里我们正在定义一个新的 联合类型,名为 Msg
,用来表示我们的消息,每条消息可以是带有附加 String
的 Filter
操作,其中包含来自过滤器的文本,或者是一个 Add
操作,我们将使用它来触发将过滤器(表单输入中的字符串)添加到条目列表中。
现在我们已经准备好 Msg
类型在我们的 update
函数中使用;我们可以继续将以下内容添加到我们的文件中
update : Msg -> Model -> Model
update msg model =
case msg of
Filter filter ->
{ model
| results = List.filter (String.contains filter) model.entries
, filter = filter
}
Add ->
{ model
| entries = model.filter :: model.entries
, results = model.filter :: model.results
}
虽然它看起来可能有点复杂,但它由非常简单的部分组成,以实现我们应用程序的功能。
第一行是 update
函数的类型定义。它说它将接收一个 Msg
和一个 Model
,并将返回一个 Model
,因为我们将根据其内容在每个消息上更新我们的模型。
接下来,我们定义实际的函数,该函数将使用 msg
和 model
作为参数,我们将在函数内部使用这些参数。首先,我们必须使用 case
函数检查要处理的操作类型,就像我们之前定义的那样,它是 Filter
和 Add
。
Filter 操作接收一个字符串,我们将其命名为 filter
,它将更新我们的 model
,更改 results
字段,并使用新的列表产品对包含(String.contains
)过滤器字符串的条目进行过滤 (List.filter
)。我们还用过滤器的内容更新 model.filter
的值,这样我们就可以根据需要访问它。您可以在 语法文档 中查看更新记录的语法。
下一个案例是 Add
,它将从 model
中添加 filter
字符串到 entries
和 results
字段中,这将在用户每次单击“添加”按钮时发生,我们将在下一节中定义它。将字符串添加到条目列表中是不言自明的,但我们也将其添加到结果列表中,因为这样我们就可以在发送 Add
消息后立即看到它,否则我们将不得不删除一部分字符串并重新编写它才能看到添加。
这就是关于我们应用程序逻辑的所有内容,我们现在要做的唯一事情就是定义它如何在浏览器中显示。
视图
到目前为止,我们已经定义了我们应用程序的内部部分;幕后发生的一切。但我们仍然需要定义如何将所有信息显示给用户以及用户如何与之交互。
应用程序的界面将由三个主要元素组成:用户可以在其中输入过滤器字符串进行搜索或将其添加到条目中的表单输入,一个用于将文本添加到条目中的**添加**按钮,以及条目列表。
在 HTML 中,我们会有类似这样的内容
<div>
<input placeholder="Filter…" oninput="Filter()">
<button onclick="Add()">Add New</button>
<ul>
<li>Item list</li>
<li>…</li>
</ul>
</div>
但我们不能在 Elm 中直接编写 HTML。我们必须使用模拟 HTML 标签的专用函数。要实现这一点,请将以下内容添加到您的文件中
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Filter…", onInput Filter ] []
, button [ onClick Add ] [ text "Add New" ]
, ul [] (List.map viewEntry model.results)
]
首先,我们定义视图函数的类型,它将以包含所有数据的 Model
作为输入,并以 Html Msg
作为输出,它是将在浏览器中显示的 HTML 表示。函数主体几乎是我们的 HTML 代码的直接表示,但语法不同,形式为 <tag> [<attributes>] [<content>]
。
请注意,我们是如何在每个 onInput
事件中发送 Filter
消息的,这会在用户每次在输入元素中输入内容时发生。我们会在 Add
消息中使用 onClick
事件做类似的事情。
对于条目列表,我们使用 List.map
遍历 mode.results
列表中的元素,并将每个条目传递给我们将要定义的 viewEntry
函数(将其添加到您的文件中)
viewEntry : String -> Html Msg
viewEntry entry =
li [] [ text entry ]
这只是一个简单的辅助函数,用于在 view
中获得更简洁的代码。它接收一个字符串作为 entry
,并将其返回到 li
元素中,类型为 Html Msg
。
现在,我们的应用程序已经完成。您只需重新加载浏览器窗口(确保 Reactor 仍在执行),您将看到最终结果。

结论
本文中包含的示例非常简单,但希望它能帮助您度过学习 Elm 编程语言的初始曲线。如果您愿意,可以尝试将此示例扩展为更复杂的应用程序;这将有助于您继续学习,而不必处理使用新技术时常见的“如何开始”问题。