大多数语言在它们处于起步阶段时,往往被认为是“玩具语言”,只用于琐碎或小型项目。 但 Elm 并非如此,它的真正实力体现在复杂和大型应用程序中。
不仅可以在 Elm 中构建应用程序的某些部分并将这些组件集成到更大的 JS 应用程序中,还可以完全构建应用程序而无需触碰任何其他语言,这使其成为 React 等 JS 框架的绝佳替代方案。
在本文中,我们将使用一个简单的网站来管理纯文本文档作为示例,探讨 Elm 应用程序的结构。
文章系列
- 为什么选择 Elm?(以及如何开始使用它)
- Elm 架构简介以及如何构建我们的第一个应用程序
- Elm 应用程序的结构(您现在就在这里!)
本文中涵盖的一些主题包括
- 应用程序架构以及事物如何流经它。
- 示例应用程序中如何定义
init
、update
和view
函数。 - 使用 Elm 命令进行 API 通信。
- 单页路由。
- 处理 JSON 数据。
这些是构建几乎任何类型应用程序时会遇到的主要主题,并且相同的原则可以通过添加所需的功能扩展到更大的项目中,而无需进行重大或根本性的更改。
要遵循本文,建议将 Github 存储库 克隆到您的计算机上,以便您可以看到整个画面; 本文以描述性方式解释一切,而不是一个逐步的教程,谈论语法和其他细节,因此 Elm 语法页面 和 Elm 包站点 对于代码的具体细节(例如所用函数的类型签名)可能非常有用。
关于应用程序
我们在本文中将要描述的示例是一个针对纯文本文档的 CRUD 应用程序,它通过 HTTP 与 API 通信以与数据库进行交互。 由于本文的主题是 Elm 应用程序,因此服务器将不会详细解释; 它只是一个使用 json-server 将数据存储在简单 JSON 文件中的 Node.js 假 REST API。

在应用程序中,主页面包含一个输入框,用于写入新文档的标题,并在下方列出之前创建的文档。 当您单击文档或创建新文档时,将显示一个编辑页面,您可以在其中查看、编辑和添加内容。

在文档标题下方,有两个链接:**保存** 和 **删除**。 单击 **保存** 时,当前文档将作为 PUT
请求发送到服务器。 单击 **删除** 时,将向当前文档 id
发送 DELETE
请求。
创建文档会发送一个包含新文档标题和空内容字段的 POST 请求,创建文档后,应用程序将切换到编辑模式,您可以继续添加文本。
有关使用源文件的快速提示
当您使用 Elm 时(以及可能使用任何其他语言时),您将不得不处理除了代码本身之外的外部细节。 您最想做的事情是自动化开发命令:启动 API 服务器、启动 Elm Reactor、编译源文件,以及启动文件观察程序以在每次更改时重新编译。
在本例中,我使用了 Jake,它是一个简单的 Node.js 工具,类似于 Make。 在 Jakefile 中,我包含了所有必要的命令以及一个默认命令,该命令将并行运行其他命令,因此我只需在终端/命令行中执行 jake default
,一切都将启动并运行。
如果您有一个更大的项目,您还可以使用更复杂的工具,如 Gulp 或 Grunt。
克隆应用程序并安装依赖项后,您可以执行 npm start
命令,该命令将启动 API 服务器、Elm Reactor 和一个文件观察程序,该观察程序将在每次更改时编译 Elm 文件。 您可以在 **https://127.0.0.1:3000** 上查看编译后的文件,并且您可以在 **https://127.0.0.1:8000** 上查看调试模式(使用 Elm Reactor)下的应用程序。
应用程序架构
在上一篇文章中,我们介绍了 Elm 架构的概念,但为了避免复杂性,我们使用 Html.beginnerProgram
函数展示了一个初学者版本。 在此应用程序中,我们使用了一个扩展版本,该版本允许我们包含命令和订阅,尽管原则保持不变。
完整的结构如下所示
现在我们有一个 Html.program
函数,它接受一个包含 4 个函数的记录
- init : ( Model, Cmd Msg ):
init
函数返回一个包含应用程序模型和带有消息的命令的元组,这允许我们与外部世界通信并产生副作用,我们将使用该命令通过 HTTP 获取和发送数据到 API。 - update : Msg -> Model -> ( Model, Cmd Msg ): 更新函数接受两个参数:带有我们应用程序中所有可能操作的消息和包含应用程序状态的模型。 它将返回与前一个函数相同的项,但根据我们收到的消息更新值。
- view : Model -> Html Msg: view 函数接受一个包含我们应用程序状态的模型,并返回能够处理消息的 Html。 通常,它将包含一系列类似于 HTML 的函数,这些函数渲染来自模型的值。
- subscriptions : Model -> Sub Msg: 订阅函数接受一个模型,并返回一个带有消息的订阅。 每当订阅收到某物时,它将发送一条可以在
update
函数中捕获的消息。 我们可以订阅可以在任何时间发生的事件,例如鼠标移动或网络中的事件。
您可以根据需要创建任意数量的函数,但最终一切都将返回到这四个函数。
在幕后,Elm 运行时正在处理我们应用程序的流程,我们所做的只是定义流动的事物。 首先,我们描述应用程序的初始状态、其数据结构以及我们要在应用程序启动时执行的命令,然后根据这些数据显示视图,在每次交互、订阅事件或命令执行时,都会向更新函数发送一条新消息,循环再次开始,并执行一个新的模型和/或命令。
如您所见,我们实际上不必处理应用程序中的任何控制流,因为 Elm 是一种函数式语言,我们只是声明事物。
路由:Navigation.program 函数
示例应用程序由两个主要页面组成:包含之前创建的文档列表的主页,以及您可以查看或编辑文档的编辑页面。 但这两个视图之间的转换不会重新加载页面,而是仅从服务器获取所需数据,并更新视图,包括 URL(后退和前进按钮仍按预期工作)。
为了实现这一点,我们使用了两个包:Navigation 和 UrlParser。 第一个处理所有导航部分,第二个帮助我们解释 URL 路径。
导航包为 Html.program
函数提供了一个包装器,使我们能够处理页面位置,因此您可以在代码中看到我们使用 Navigation.program
而不是它,它基本上与前面的函数相同,但它还接受一个名为 UrlChange
的消息,该消息在浏览器更改位置时发送,该消息携带一个类型为 Navigation.Location
的值,其中包含我们可能需要的所有信息,包括路径,我们可以解析它来选择要显示的正确视图。
Init 函数
Init 函数可以被认为是应用程序的入口点,它代表了初始状态(模型)以及我们想要在应用程序启动时执行的任何命令。
类型定义
我们首先定义将要使用的值类型,从Model
类型开始,它包含应用程序的**所有**状态
type alias Model =
{ currentLocation : Maybe Route
, documents : List Document
, currentDocument : Maybe Document
, newDocument : Maybe String
}
- 我们存储一个类型为
Maybe Route
的currentDocument
值,它包含页面上的当前位置,我们使用此值来知道在屏幕上显示什么。 - 我们有一个名为
documents
的文档列表,我们在其中存储来自数据库的所有文档。我们这里不需要Maybe
值;如果我们没有任何文档,我们可以只使用一个空列表。 - 我们还需要一个类型为
Maybe Document
的currentDocument
值。当我们打开一个文档时,它将包含Just Document
,而当我们在主页上时,它将包含Nothing
,此值是在我们从数据库请求特定文档时获得的。 - 最后,我们有
newDocument
,它以Maybe String
的形式表示新文档的标题,当输入字段中存在内容时,它为Just String
,否则为Nothing
。此值在表单提交时发送到 API。
注意:在 Javascript 中,您可能认为直接从输入元素获取该值可能没有必要,但您必须在 Elm 中定义所有内容;当您在输入元素中输入内容时,模型会更新,当您提交表单时,模型中的值会通过 HTTP 发送。
如您所见,在Model
中,我们还使用其他类型别名,我们需要定义,分别是Document
和Route
type alias Document =
{ id : Int
, title : String
, content : String
}
type Route
= HomeRoute
| DocumentRoute Int
首先,我们定义一个Document
类型别名来表示文档的结构:一个包含整数的id
值,然后是标题和内容,两者都是String
。
我们还创建一个联合类型,它可以将两个或多个相关类型分组,在本例中,它们将对应用程序的导航有用。您可以根据需要为它们命名。我们只有两个:一个用于主页,名为HomeRoute
,另一个用于编辑视图,名为DocumentRoute
,它包含一个代表请求的特定文档的id
的整数。
整合在一起
一旦我们定义了类型,我们就可以继续声明初始化函数,以及它的初始值。
init : Navigation.Location -> ( Model, Cmd Msg )
init location =
( { currentLocation = UrlParser.parsePath route location
, documents = []
, currentDocument = Nothing
, newDocument = Nothing
}
, getDocumentsCmd
)
在引入导航包后,我们的init
函数现在接受一个类型为Navigation.Location
的值,该值包含来自浏览器有关当前页面位置的信息。我们将该值存储在一个location
参数中,以便我们可以解析并将其保存为currentLocation
,我们使用该值来知道要显示的正确视图。
currentLocation
值是使用导航包中的parsePath
函数获得的,它接受一个解析器函数(类型为Parser (a -> a) a
)和一个Location
。
存储在currentLocation
中的值具有Maybe
类型。例如,如果我们的浏览器中有一个/documents/12
路径,我们将获得Just DocumentRoute 12
。
我们调用的解析器函数route
的结构如下所示
route : UrlParser.Parser (Route -> a) a
route =
UrlParser.oneOf
[ UrlParser.map HomeRoute UrlParser.top
, UrlParser.map DocumentRoute (UrlParser.s "document" </> UrlParser.int)
]
最重要的部分是
HomeRoute UrlParser.top
我们基本上创建一个关系,其中HomeRoute
是我们为首页定义的类型,而UrlParser.top
表示路径中的根(/
)。
然后我们有
DocumentRoute (UrlParser.s "document" </> UrlParser.int)
在这里,我们再次使用一个名为DocumentRoute
的路由类型,然后是(UrlParser.s "document" </> UrlParser.int)
,它表示一个类似于/document/<id>
的路径。s
函数接受一个字符串,在本例中为document
,并将匹配任何包含document
的内容(如/document/…
)。然后,我们有一个</>
函数,可以认为是路径中斜杠字符(/
)的表示,用于将document
部分与int
值(要查看的文档的 ID)分开。
我们模型的其余部分由一个文档列表组成,该列表默认情况下是空的,尽管一旦getDocumentCmd
命令完成,它就会被填充。还有当前文档和新文档的值,两者都是Nothing
。
更新函数
更新函数使用消息和模型作为输入,并使用新模型和命令的元组作为输出,通常,输出将取决于正在处理的消息。
在我们的应用程序中,我们为每个事件定义了一条消息
- 当请求一个新页面位置时。
- 当页面位置发生变化时。
- 当在输入元素中输入新的文档标题时。
- 当保存并创建新的文档时。
- 当检索到数据库中的所有文档时。
- 当请求并检索特定文档时。
- 当更新文档的标题和内容时。
- 当保存并检索特定文档时。
- 当删除文档时。
这可以使用联合类型来完成
type Msg
= NewUrl String
| UrlChange Navigation.Location
| NewDocumentMsg String
| CreateDocumentMsg
| CreatedDocumentMsg (Result Http.Error Document)
| GotDocumentsMsg (Result Http.Error (List Document))
| ReadDocumentMsg Int
| GotDocumentMsg (Result Http.Error Document)
| UpdateDocumentTitleMsg String
| UpdateDocumentContentMsg String
| SaveDocumentMsg
| SavedDocumentMsg (Result Http.Error Document)
| DeleteDocumentMsg Int
| DeletedDocumentMsg (Result Http.Error String)
某些消息需要携带一些额外的信息,它可以在消息名称旁边定义,例如,NewUrl
消息附加了一个包含新 URL 路径的String
。
此外,大多数消息都成对出现,尤其是向 Runtime 添加新命令的消息,一条消息是在命令执行之前发送的,而另一条消息是在命令执行之后发送的。
例如,当您删除文档时,您将发送一个包含要删除的文档的id
的DeleteDocumentMsg
消息,然后,一旦文档被删除,就会发送一个包含 HTTP 调用结果的DeletedDocumentMsg
消息:一个状态值Http.Error
和一个String
形式的结果。
正如我们将在下面看到的那样,包含命令结果的消息应该对其两个值进行模式匹配,无论是错误值还是成功值。
一旦我们定义了所有消息,我们就可以开始处理每个消息。为此,我们对消息进行模式匹配,让我们以读取特定文档为例
ReadDocumentMsg id ->
( model, getDocumentCmd id )
这将匹配包含一个int
(根据我们的类型定义)的ReadDocumentMsg
消息,名为id
。
注意 int
值的名称是在它被匹配时分配的,在它被匹配之前,该值只是Int
类型的某个东西。
然后,我们返回一个包含模型的元组,该模型没有任何更改,但我们还返回一个要执行的命令,称为getDocumentCmd
,它接收文档的 ID 作为输入。现在不用担心命令定义,我们将在下面讨论。
现在,我们需要匹配在获得请求的文档后发送的消息
GotDocumentMsg (Ok document) ->
( { model | currentDocument = Just document }, Cmd.none )
GotDocumentMsg (Err _) ->
( model, Cmd.none )
请记住,GotDocumentMsg
消息携带一个(Result Http.Error Document)
值,因此我们必须对其两个可能的值进行匹配:如果成功,以及如果失败。
这里的第一种情况将匹配错误类型为Ok
的情况,这意味着没有错误,第二个值将是检索到的文档。然后,我们可以返回一个包含一个修改后的model
的元组,其中currentDocument
值为我们刚刚获得的文档,前面是Just
,因为currentDocument
的类型是Maybe
。此外,现在在元组的第二部分,我们表示我们不会执行任何命令(Cmd.none
)。
在第二种情况下,如果发生了错误,我们将其与类型为Err
的值进行匹配,并且可以使用_
作为占位符来代表任何可能存在的值。在实际应用中,我们可以向用户显示一个信息框,告知他们错误,但为了避免在这个例子中过于复杂,我们将简单地忽略它;因此,我们再次返回没有任何更改的模型,并且我们也不执行任何命令。
所有其他消息匹配都遵循相同的模式:它们返回一个包含消息所携带的信息的新模型,或者执行一个命令。
使用命令进行 API 通信
尽管 Elm 是一种纯函数式编程语言,但我们仍然可以执行副作用,例如通过 HTTP 与服务器进行通信,这是使用命令完成的。
正如我们之前所见,每次匹配一条消息时,我们都会返回一个包含新模型和命令的元组。命令是任何返回类型为Cmd
的值的函数。
让我们看一下我们在init
函数中包含的命令,该命令在应用程序启动时对服务器执行一个请求,其中包含数据库中的所有文档
getDocumentsCmd : Cmd Msg
getDocumentsCmd =
let
url =
"https://127.0.0.1:3000/documents?_sort=id&_order=desc"
request =
Http.get url decodeDocuments
in
Http.send GotDocumentsMsg request
函数的两个重要部分是它的类型声明getDocumentsCmd : Cmd Msg
和in
部分中的Http.send GotDocumentsMsg request
。
该类型表示它是一个命令,并且还携带一条消息,该消息是从Http.send
函数返回的类型获得的,您可以在包文档中看到。
在函数体中,我们可以看到请求完成后发送的消息。为了清晰起见,我们创建了两个变量:一个包含发送请求的 API 的 URL,另一个包含Http.send
将发送的请求本身。
该请求是使用Http.get函数构建的,因为我们要向服务器发送一个 GET 请求。
您还会注意到其中的一个decodeDocuments
函数,它是一个 JSON 解码器,我们使用它将服务器响应中的 JSON 转换为可用的 Elm 值。我们将在下一节中看到在本应用程序中使用的解码器是如何构建的。
获取服务器上单个文档的命令非常相似,因为Http.get
函数为我们完成了构建请求的大部分工作。我们只需更改要获取的资源的 URL,在本例中使用请求文档的id
。
但要向服务器发送数据,历史略有不同;相反,我们可以使用Http.request
函数自己构建请求。
让我们看一下向服务器发送新文档的函数
createDocumentCmd : String -> Cmd Msg
createDocumentCmd documentTitle =
let
url =
"https://127.0.0.1:3000/documents"
body =
Http.jsonBody <| encodeNewDocument documentTitle
expectedDocument =
Http.expectJson decodeDocument
request =
Http.request
{ method = "POST"
, headers = []
, url = url
, body = body
, expect = expectedDocument
, timeout = Nothing
, withCredentials = False
}
in
Http.send CreatedDocumentMsg request
我们再次拥有一个返回类型为Cmd Msg
的值的函数,但现在我们还接受一个类型为String
的值,它是要创建的新文档的标题。
使用Http.request函数,我们将一个记录作为参数传递,该记录包含请求的所有部分,我们主要关心以下内容
method
: 请求的 HTTP 方法,我们之前使用 GET 从服务器获取信息,但现在我们发送信息,因此我们使用方法 POST。url
: 接收请求的 API 端点。body
: 请求的主体,包含我们要添加到数据库的文档,以 JSON 格式表示。为了构建主体,我们使用Http.jsonBody函数,该函数会自动为我们添加Content-Type: application/json
头。此函数需要一个 JSON 值,我们使用 JSON 编码器和新文章的标题来生成该值。在下一节中,我们将看到 JSON 编码器的实现方式。expect
: 在这里,我们指示应该如何解释请求的响应,在本例中,我们将获得新的文档,因此我们使用Http.expectJson函数来使用我们的decodeDocument
JSON 解码器转换响应。
Http.send
函数与我们之前提到的函数基本相同;唯一的区别是现在文档创建后,我们将发送CreatedDocumentMsg
消息。
更新文档的命令也与创建新文档的命令非常相似,主要区别在于
- 我们将数据发送到不同的 API 端点,具体取决于要更新的文档的
id
。 - 主体使用完整文档构建,并使用不同的编码器编码为 JSON。
- 使用的 HTTP 方法是
PUT
,这是对现有资源进行更新的首选方法。 - 我们收到响应后使用
SavedDocumentMsg
消息。
最后,我们有deleteDocumentCmd
命令函数。原则仍然相同,但在这种情况下,我们不会在请求的主体中发送任何内容,因此我们使用Http.emptyBody。此外,我们表明我们期望一个String
值,但这并不重要,因为我们在应用程序中没有使用它。
使用 JSON 值
在 Elm 中,我们不能直接在代码中使用 JSON,也不能使用像JSON.parse()
这样的简单解析函数,就像我们在 JavaScript 中做的那样,因为我们必须确保我们正在处理的数据类型安全。
要在 Elm 中使用 JSON,我们必须将 JSON 值解码为 Elm 值,然后才能使用它,我们使用 JSON 解码器来实现。同样,反向也类似;要生成 JSON 值,我们必须使用 JSON 编码器对 Elm 值进行编码。
在我们的示例应用程序中,我们有两个解码器和两个编码器。让我们分析一下解码器
decodeDocuments : Decode.Decoder (List Document)
decodeDocuments =
Decode.list decodeDocument
decodeDocument : Decode.Decoder Document
decodeDocument =
Decode.map3 Document
(Decode.field "id" Decode.int)
(Decode.field "title" Decode.string)
(Decode.field "content" Decode.string)
解码器函数必须具有类型Decoder
(在本例中为Decode.Decoder
,因为我们导入 JSON 包的方式。)签名中还指示了解码器中数据的类型,第一个是文档列表,因此类型为List Document
,第二个只是一个文档,因此它具有Document
类型(我们在应用程序开头定义了此类型)。
正如您所注意到的,我们实际上正在组合这两个编码器,因为第一个解码器解码的是文档列表,我们可以将文档解码器用于Decode.list函数。
在decodeDocument
解码器中,真正的事情发生了。我们使用Decode.map3函数来解码具有三个字段的值:id
、title
和content
,以及它们各自的类型,然后将结果放入我们在应用程序开头定义的Document
类型中,以创建最终值。
注意: Elm 有八个映射函数来处理 JSON 值,如果您需要更多函数,可以使用elm-decode-pipeline包,该包允许使用管道(|>
)运算符构建任意解码器。
现在,我们可以看到两个编码器是如何实现的
encodeNewDocument : String -> Encode.Value
encodeNewDocument title =
let
object =
[ ( "title", Encode.string title )
, ( "content", Encode.string "" )
]
in
Encode.object object
encodeUpdatedDocument : Document -> Encode.Value
encodeUpdatedDocument document =
let
object =
[ ( "id", Encode.int document.id )
, ( "title", Encode.string document.title )
, ( "content", Encode.string document.content )
]
in
Encode.object object
要编码 JavaScript 对象,我们使用函数Encode.object,它接受元组列表,每个元组包含键的名称和根据其类型进行编码的值,在本例中为Encode.int
和Encode.string
。此外,与解码器不同,这些函数始终返回类型为Value
的值。
因为我们正在创建内容为空的文档,所以第一个编码器只需要该文档的标题,并且我们在将其发送到 API 之前手动编码一个空内容字段。第二个编码器接受一个完整的文档,并只生成一个 JSON 等效项。
您可以在 Elm Packages 网站上查看更多与 JSON 相关的函数:Json.Decode 和 Json.Encode
视图函数
与之前文章的代码相比,视图函数仍然非常简单明了。这里有趣的变化是我们根据 URL 路径显示每个页面的方式。
首先,我们有一个始终指向主页的链接,我们这样做的方式——而不是使用普通链接——是捕获点击事件,然后我们发送一条包含新路径的NewUrl
消息。
因为我们仍然在应用程序中使用普通的<a>
元素,而不是按钮,所以我们创建了一个名为onClickLink
的自定义事件,它与onClick事件相同,但阻止了被点击元素的默认行为(preventDefault
)。
此事件的实现如下
onClickLink : msg -> Attribute msg
onClickLink message =
let
options =
{ stopPropagation = False
, preventDefault = True
}
in
onWithOptions "click" options (Decode.succeed message)
这里需要注意的是使用onWithOptions
函数,该函数允许我们在click
事件中添加两个选项:stopPropagation 和 preventDefault。这里起作用的选项是preventDefault
,它阻止了<a>
元素的默认行为。
接下来,我们实现了根据 URL 中的路径处理显示页面的函数
page : Model -> Html Msg
page model =
case model.currentLocation of
Just route ->
case route of
HomeRoute ->
viewHome model
DocumentRoute id ->
case model.currentDocument of
Just document ->
viewDocument document
Nothing ->
div [] [ text "Nothing here…" ]
Nothing ->
div [] [ text "404 – Not Found" ]
请记住,我们正在模型中存储当前位置,该位置存储在一个名为currentLocation
的变量中,因此我们可以对该变量应用模式匹配,并根据其值显示一些内容。在我们的示例中,我们首先检查Maybe
值是否是Just Route
或Nothing
类型,然后如果我们有一个路由,我们检查它是否是HomeRoute
或DocumentRoute
。对于第一种情况,我们包含viewHome
函数,它表示主页的内容,对于第二种情况,我们在viewDocument
函数中传递currentDocument
值,该函数显示所选文档。
对于每个文档条目,请注意在viewDocumentEntry
函数中,我们再次发送一条包含指向相应文档的链接的NewUrl
消息,使用onLinkClick
事件。此消息负责加载相应的文档。
最后,我们可以通过添加一个类型为Attribute
的函数,使用Html.Attributes.style
,在每个组件中添加内联 CSS,该函数具有以下形式
myStyleFunction : Attribute Msg
myStyleFunction =
Html.Attributes.style
[ ( "<property>", "<value>" )
]
在示例应用程序中,我们有一些组件的样式,以及直接包含在应用程序嵌入的 HTML 文件中的其他通用样式。您可以选择直接包含 CSS 文件,就像您通常在任何网站上做的那样,或者您也可以直接在 Elm 源文件中编写它们。虽然在本例中显示的方法相当简单,但有一个专门的库可以完成此操作,如果您需要更多控制,可以使用:Elm-css.
关于订阅的一句话
订阅在许多应用程序中都很常见,虽然我们在本例中没有使用订阅,但其机制非常简单:它们允许我们监听某些事件的发生,而我们不知道它们何时会发生。
让我们看一下订阅的基本结构
subscriptions : Model -> Sub Msg
subscriptions model =
WebSocket.listen "ws://echo.websocket.org" NewMessage
首先要提到的是,所有订阅都具有类型Sub
,这里我们有Sub Msg
,因为我们在订阅上收到任何内容时都会发送一条消息。
它的工作原理是WebSocket.listen
函数为地址ws://echo.websocket.org
创建一个套接字监听器,并且每次有内容到达时,都会发送NewMessage
消息,并且在我们的更新函数中,我们可以对该消息进行适当的操作,就像我们之前做的那样(感谢 Elm 架构)。
应用程序嵌入
现在我们已经了解了如何构建完整的应用程序,现在是时候看看如何将该应用程序包含在 HTML 文件中以便进行分发了。虽然 Elm 可以生成 HTML 文件,但您只需生成 JavaScript 并自行包含它们,这样您也可以控制其他内容,例如样式。
在 HTML 中,您可以包含以下内容
…
<body>
<main>
<!-- The app is going to appear here -->
</main>
<script src="main.js"></script>
<script>
// Get the <main> element
var node = document.getElementsByTagName('main')[0];
// Embed the Elm application in the <main> element
var app = Elm.Main.embed(node);
</script>
</body>
…
首先,我们在 <script>
标签中包含编译后的 Elm 文件(.js),然后获取应用程序将要渲染的元素(在本例中为 <main>
元素),最后,我们调用 Elm.Main.embed(<element>)
,其中 <element>
是我们之前获取的 HTML 节点。
就是这样。
结论
Elm 是一个构建大型 Web 应用程序时 JavaScript 框架的绝佳替代方案。它不仅提供默认架构来保持事物的顺序,而且还为现代应用程序提供了经过精心设计的语言的所有优点。
本文中介绍的主题在您将要构建的大多数应用程序中都可以找到,并且提供了足够的入门信息,以便在您熟悉这些信息后,即可开始构建生产网站,其余的只是不断探索的问题。
文章系列
- 为什么选择 Elm?(以及如何开始使用它)
- Elm 架构简介以及如何构建我们的第一个应用程序
- Elm 应用程序的结构(您现在就在这里!)
精彩的文章,非常感谢您撰写它!我一直关注着 Elm,但您的文章给了我必要的小推动力来深入研究它。
关于视图的问题:是否有办法编写一个 HTML 文档,然后连接到某些元素以显示/隐藏它们,而不是让 Elm 创建和删除 DOM 树分支?我在 React Router View 中遇到了相同的问题。
感谢您,Fred。
您指的是使用 CSS 显示/隐藏它们吗?我认为这不会太不一样:您可以更改相应的样式值,而不是根据模型中的某个值更改整个视图函数。虽然据我所知,Elm 在这方面非常快。您可以在这里了解更多信息 http://elm-lang.org/blog/blazing-fast-html-round-two
感谢您的指点,James。
我更想知道是否有一种方法可以获取现有 HTML 结构的 HTML 节点(如 React 中的 Ref 或 jQuery 元素 $(selector)),然后直接从 Elm 操作该节点的属性/特性?
在我看来,在 Elm 中操作 HTML 的唯一方法是让 Elm 生成 DOM(或虚拟 DOM),而不是依附于现有的 HTML/DOM?
我认为在 Elm 中这不可能。正如您所说,Elm 使用虚拟 DOM,据我所知,无法避免它。应用程序中任何发生变化的值都必须在模型中定义,然后 DOM 才能反映这些变化,而不是直接在 DOM 中进行更改。
部分来说,这就是 Elm 架构变得有用的原因;通过遵循这种模式,您可以避免许多通常会出现在普通 JavaScript 应用程序中的问题。
希望这解释清楚了!