开源 Elm SPA 之旅

2025-05-26

开源 Elm SPA 之旅

人们经常问我是否可以向他们推荐一个开源的 Elm 单页应用程序,以便他们可以仔细阅读其代码。

Ilias van Peer推荐了我一个Realworld项目,这个项目看起来非常适合我。他们提供了后端 API、静态标记、样式和规范,你可以使用自己选择的技术为其构建一个 SPA 前端。

这就是最终成果。我做的时候特别开心!

4,000 行 Elm 单页应用程序代码

温馨提示:这并不是一篇 Elm 的入门教程。我构建它是为了让它成为我想要维护的东西,所以我没有丝毫保留。这就是我如何利用 Elm 的全部功能来构建这个应用程序。

我在 Elm Europe 上做了一个演讲,讲述了我构建这个应用时所遵循的原则,强烈推荐大家观看!演讲内容叫做《Scaling Elm Apps》

如果您正在寻找一种不那么枯燥乏味的 Elm 介绍,我可以推荐一本书、一个视频教程,当然还有官方指南

用户体验路由

我采用了一种优化用户体验的路由设计。我考虑了三种用例,如下图所示:

用例:

  1. 用户连接速度很快
  2. 用户连接速度较慢
  3. 该用户处于离线状态

快速连接

在快速连接下,我希望用户能够无缝地从一个页面过渡到另一个页面,而不会看到中间部分加载的页面闪烁。

为了实现这一点,我让每个页面都暴露出来init : Task PageLoadError Model。当路由器收到转换到新页面的请求时,它不会立即转换;而是首先调用Task.attemptinit任务来获取新页面所需的数据。

如果任务失败,结果会PageLoadError告诉路由器应该向用户显示什么错误消息。如果任务成功,结果Model将作为立即渲染 100% 新页面所需的初始模型。

无需闪烁部分加载的页面!

连接速度慢

当连接速度较慢时,我希望用户看到一个加载旋转器,以向他们保证即使需要一点时间,也会有事情发生。

为此,我会在用户尝试跳转到新页面时,在页眉中渲染一个加载旋转图标。在页面Task加载过程中,它会一直停留在那里,直到页面解析完成(无论是跳转到新页面还是错误页面),旋转图标就会消失。

为了进一步完善,我给animation-delay旋转动画添加了一段 CSS,防止旋转动画在高速网络连接下闪现。这意味着我可以在用户点击链接时立即将其添加到 DOM 中,进行过渡(并在目标页面渲染​​完成后再次移除),但除非中间有几百毫秒的延迟,否则旋转动画不会对用户可见。

离线

我希望至少有些功能在用户离线时能够正常工作。

我并没有使用 Service Worker(或者对于我们这些走过这条坎坷道路的人来说,是 App Cache ),但我确实希望用户能够访问像New Post这样的页面,而这些页面无需从网络获取数据即可加载。

对于他们来说,init返回的Model是 而不是Task PageLoadError Model。这就够了。

模块结构

模块结构:数据、页面、请求和视图目录,以及 Main.elm、Ports.elm、Route.elm 和 Util.elm

NoRedInk的生产环境中有超过 10 万行 Elm 代码,我们一路走来学到了很多东西!(我们没有 SPA,所以路由逻辑在服务器上,但其余部分都一样。)当然,每个应用程序都不一样,但我对我们的代码库扩展性感到非常满意,所以在构建这个应用程序时,我借鉴了我们的组织方案。

请记住,虽然通过限制模块公开的内容来创建保证exposing一项重要的技术(我在这里经常使用),但实际的文件结构却不那么重要。记住,如果您改变主意并想要重命名某些文件或移动目录,Elm 的编译器会为您提供支持。一切都会好起来!

以下是我组织此应用程序模块的方式。

模块Page.*

例如 Page.Home,,Page.ArticlePage.Article.Editor

这些模块保存了应用程序中各个页面的逻辑。

需要从服务器获取数据的页面会公开一个init函数,该函数返回一个Task负责加载数据的函数。这使得路由系统可以等待页面数据加载完成,然后再切换到该页面。

模块Views.*

例如 Views.Form,,Views.ErrorsViews.User.Follow

这些模块包含多个模块导入的可重复使用的视图Page

有些,比如Views.User,非常简单。其他的,比如Views.Article.Feed,则非常复杂。每个框架都根据其特定需求公开了相应的 API。

Views.Page模块公开了一个frame将每个页面包装在页眉和页脚中的功能。

模块Data.*

例如 Data.User,,Data.ArticleData.Article.Comment

这些模块描述了常见的数据结构,并公开了将它们转换为其他数据结构的方法。Data.User描述了User,以及将序列化和反序列化为UserJSON 的编码器和解码器。

诸如 、 和 之类的标识符CommentIdUsername分别Slug用于唯一标识评论、用户和文章)被实现为联合类型。例如,如果我们使用type alias Username = String,我们可能会错误地将 a 传递Username给一个预期为 的 API 调用Slug,而它仍然能够编译通过。我们可以通过将标识符实现为联合类型来排除此类错误。

模块Request.*

例如 Request.User,,Request.ArticleRequest.Article.Comments

这些模块公开一些函数,用于向应用服务器发出 HTTP 请求。它们公开Http.Request一些值,以便调用者可以将它们组合在一起,例如在需要访问多个端点来加载所有数据的页面上。

我不会在这些模块之外的任何地方使用原始 API 端点 URL 字符串。只有Request.*模块才应该知道实际的端点 URL。

模块Route

这公开了将浏览器位置栏中的 URL 转换为应用程序中的逻辑“页面”的功能,以及影响位置栏更改的功能。

与模块从不暴露原始 API URL 字符串类似Request,此模块也从不暴露原始地址栏 URL 字符串。相反,它暴露一个名为 的联合类型,Route调用者使用它来指定他们想要的页面。

模块Ports

将所有端口集中到一个端口,port module更易于跟踪。大多数大型应用程序最终都会有多个端口,但在这个应用程序中,我只想要两个。查看index.html它们连接的 10 行 JavaScript 代码。

在 NoRedInk,我们对端口和标志的策略是,使用ValueJavaScript 输入任何值,然后在 Elm 中对其进行解码。这样,我们就可以完全控制如何处理数据中的任何意外情况。我在这里遵循了这一策略。

模块Main

这将启动一切,并调用Cmd.mapHtml.map在各个Page模块之间进行切换。

根据有关代码拆分和延迟加载等资产管理功能如何形成的讨论,我预计该文件的大部分内容在 Elm 的未来版本中将变得不再必要。

模块Util

这些是其他几个模块中使用的杂项助手。

或许这样称呼更为诚实Misc.elm

其他考虑因素

  • 通过服务器端渲染,可以使用基于 Cookie 的身份验证来提供更好的首次页面加载用户体验。然而,这种方式存在安全风险!
  • 如果我从头开始做的话,我会用elm-css它来设置样式。但是,由于 Realworld 提供了太多标记,我最终还是用了它,html-to-elm这样可以节省很多时间。
  • 有一个elm-test正在进行的测试版,我想用最新最好的版本进行测试。我考虑过等到新elm-test版本发布再发布,但最终决定,即使未经测试,它也是个有用的资源。

我希望这对你有用!

现在,回过头来继续编写《Elm 实际应用》的另一章。

文章来源:https://dev.to/rtfeldman/tour-of-an-open-source-elm-spa
PREV
仅使用 HTML、CSS 和 Javascript 的暗/亮主题切换器
NEXT
一行 CSS 居中对象