开源 Elm SPA 之旅
人们经常问我是否可以向他们推荐一个开源的 Elm 单页应用程序,以便他们可以仔细阅读其代码。
Ilias van Peer推荐了我一个Realworld项目,这个项目看起来非常适合我。他们提供了后端 API、静态标记、样式和规范,你可以使用自己选择的技术为其构建一个 SPA 前端。
这就是最终成果。我做的时候特别开心!
温馨提示:这并不是一篇 Elm 的入门教程。我构建它是为了让它成为我想要维护的东西,所以我没有丝毫保留。这就是我如何利用 Elm 的全部功能来构建这个应用程序。
我在 Elm Europe 上做了一个演讲,讲述了我构建这个应用时所遵循的原则,强烈推荐大家观看!演讲内容叫做《Scaling Elm Apps》。
如果您正在寻找一种不那么枯燥乏味的 Elm 介绍,我可以推荐一本书、一个视频教程,当然还有官方指南。
用户体验路由
我采用了一种优化用户体验的路由设计。我考虑了三种用例,如下图所示:
用例:
- 用户连接速度很快
- 用户连接速度较慢
- 该用户处于离线状态
快速连接
在快速连接下,我希望用户能够无缝地从一个页面过渡到另一个页面,而不会看到中间部分加载的页面闪烁。
为了实现这一点,我让每个页面都暴露出来init : Task PageLoadError Model
。当路由器收到转换到新页面的请求时,它不会立即转换;而是首先调用Task.attempt
此init
任务来获取新页面所需的数据。
如果任务失败,结果会PageLoadError
告诉路由器应该向用户显示什么错误消息。如果任务成功,结果Model
将作为立即渲染 100% 新页面所需的初始模型。
无需闪烁部分加载的页面!
连接速度慢
当连接速度较慢时,我希望用户看到一个加载旋转器,以向他们保证即使需要一点时间,也会有事情发生。
为此,我会在用户尝试跳转到新页面时,在页眉中渲染一个加载旋转图标。在页面Task
加载过程中,它会一直停留在那里,直到页面解析完成(无论是跳转到新页面还是错误页面),旋转图标就会消失。
为了进一步完善,我给animation-delay
旋转动画添加了一段 CSS,防止旋转动画在高速网络连接下闪现。这意味着我可以在用户点击链接时立即将其添加到 DOM 中,进行过渡(并在目标页面渲染完成后再次移除),但除非中间有几百毫秒的延迟,否则旋转动画不会对用户可见。
离线
我希望至少有些功能在用户离线时能够正常工作。
我并没有使用 Service Worker(或者对于我们这些走过这条坎坷道路的人来说,是 App Cache ),但我确实希望用户能够访问像New Post这样的页面,而这些页面无需从网络获取数据即可加载。
对于他们来说,init
返回的Model
是 而不是Task PageLoadError Model
。这就够了。
模块结构
NoRedInk的生产环境中有超过 10 万行 Elm 代码,我们一路走来学到了很多东西!(我们没有 SPA,所以路由逻辑在服务器上,但其余部分都一样。)当然,每个应用程序都不一样,但我对我们的代码库扩展性感到非常满意,所以在构建这个应用程序时,我借鉴了我们的组织方案。
请记住,虽然通过限制模块公开的内容来创建保证exposing
是一项重要的技术(我在这里经常使用),但实际的文件结构却不那么重要。记住,如果您改变主意并想要重命名某些文件或移动目录,Elm 的编译器会为您提供支持。一切都会好起来!
以下是我组织此应用程序模块的方式。
模块Page.*
例如 :Page.Home
,,Page.Article
Page.Article.Editor
这些模块保存了应用程序中各个页面的逻辑。
需要从服务器获取数据的页面会公开一个init
函数,该函数返回一个Task
负责加载数据的函数。这使得路由系统可以等待页面数据加载完成,然后再切换到该页面。
模块Views.*
例如 :Views.Form
,,Views.Errors
Views.User.Follow
这些模块包含多个模块导入的可重复使用的视图Page
。
有些,比如Views.User
,非常简单。其他的,比如Views.Article.Feed
,则非常复杂。每个框架都根据其特定需求公开了相应的 API。
该Views.Page
模块公开了一个frame
将每个页面包装在页眉和页脚中的功能。
模块Data.*
例如 :Data.User
,,Data.Article
Data.Article.Comment
这些模块描述了常见的数据结构,并公开了将它们转换为其他数据结构的方法。Data.User
描述了User
,以及将序列化和反序列化为User
JSON 的编码器和解码器。
诸如 、 和 之类的标识符CommentId
(Username
分别Slug
用于唯一标识评论、用户和文章)被实现为联合类型。例如,如果我们使用type alias Username = String
,我们可能会错误地将 a 传递Username
给一个预期为 的 API 调用Slug
,而它仍然能够编译通过。我们可以通过将标识符实现为联合类型来排除此类错误。
模块Request.*
例如 :Request.User
,,Request.Article
Request.Article.Comments
这些模块公开一些函数,用于向应用服务器发出 HTTP 请求。它们公开Http.Request
一些值,以便调用者可以将它们组合在一起,例如在需要访问多个端点来加载所有数据的页面上。
我不会在这些模块之外的任何地方使用原始 API 端点 URL 字符串。只有Request.*
模块才应该知道实际的端点 URL。
模块Route
这公开了将浏览器位置栏中的 URL 转换为应用程序中的逻辑“页面”的功能,以及影响位置栏更改的功能。
与模块从不暴露原始 API URL 字符串类似Request
,此模块也从不暴露原始地址栏 URL 字符串。相反,它暴露一个名为 的联合类型,Route
调用者使用它来指定他们想要的页面。
模块Ports
将所有端口集中到一个端口,port module
更易于跟踪。大多数大型应用程序最终都会有多个端口,但在这个应用程序中,我只想要两个。查看index.html
它们连接的 10 行 JavaScript 代码。
在 NoRedInk,我们对端口和标志的策略是,使用Value
JavaScript 输入任何值,然后在 Elm 中对其进行解码。这样,我们就可以完全控制如何处理数据中的任何意外情况。我在这里遵循了这一策略。
模块Main
这将启动一切,并调用Cmd.map
和Html.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