简单代码与过于简单的代码不同:Elm vs JavaScript
有一些语言、框架和库致力于让你通过编写几行代码就能完成相对复杂的任务。JavaScript就是一个很好的例子。要用这种语言对我网站的某个页面进行http调用,你只需编写一行代码:
await fetch("https://segunda.tech/about")
大多数人可能并不认为这段代码很难或复杂,但其中可能隐藏着一些难以处理的错误场景。为了分析这个问题,我将向您展示一个使用纯JavaScript 的小页面实现,并讨论其中可能存在的问题。接下来,我将向您展示如何使用Elm编程语言实现相同的解决方案,并分析相同的要点。
练习:检索神奇宝贝名称列表
为了举例说明本文要讨论的问题,我用HTML和纯JavaScript(使用Ajax)实现了显示宝可梦名称列表所需的最低要求。为此,我使用了PokéAPI的服务。检索前 5 个宝可梦列表的端点非常简单:只需调用 URL https://pokeapi.co/api/v2/pokemon?limit=5
,返回的将是包含以下结果的json数据。
{
"count": 1118,
"next": "https://pokeapi.co/api/v2/pokemon?offset=5&limit=5",
"previous": null,
"results": [
{
"name": "bulbasaur",
"url": "https://pokeapi.co/api/v2/pokemon/1/"
},
{
"name": "ivysaur",
"url": "https://pokeapi.co/api/v2/pokemon/2/"
},
{
"name": "venusaur",
"url": "https://pokeapi.co/api/v2/pokemon/3/"
},
{
"name": "charmander",
"url": "https://pokeapi.co/api/v2/pokemon/4/"
},
{
"name": "charmeleon",
"url": "https://pokeapi.co/api/v2/pokemon/5/"
}
]
}
在这个练习中,目标是异步检索这些数据,并在html页面上仅列出名称字段的内容(在结果内)。
使用纯HTML和JavaScript实现解决方案
有几种方法可以利用这些技术来解决这个问题。下面我将介绍我的实现。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>List of Pokémons using HTML and JavaScript</title>
<meta name="author" content="Marcio Frayze David">
</head>
<body>
<p id="loading-message">
Loading Pokémons names, please wait...
</p>
<ul id="pokemon-names-list">
</ul>
<script>
(async function() {
await fetch("https://pokeapi.co/api/v2/pokemon?limit=5")
.then(data => data.json())
.then(dataJson => dataJson.results)
.then(results => results.map(pokemon => pokemon.name))
.then(names => addNamesToDOM(names))
hideLoadingMessage()
})();
function addNamesToDOM(names) {
let pokemonNamesListElement = document.getElementById('pokemon-names-list')
names.forEach(name => addNameToDOM(pokemonNamesListElement, name))
}
function addNameToDOM(pokemonNamesListElement, name) {
let newListElement = document.createElement('li')
newListElement.innerHTML = name
pokemonNamesListElement.append(newListElement)
}
function hideLoadingMessage() {
document.getElementById('loading-message').style.visibility = 'hidden'
}
</script>
</body>
</html>
这个想法是,在Ajax调用结束时,不再显示加载消息,而是在id 为pokemons-names-list的标签内加载包含 Pokémon 名称的列表。我使用JSFiddle将此页面发布到线上,以便您可以看到预期的行为。
我知道几乎没有人会写这样的代码。我没有使用任何框架或外部库,并且做了一些很多人认为不好的做法(例如将JavaScript代码直接放在html中)。但即使我使用React、JSX和Axios等流行技术实现了这个解决方案,我在这里想要讨论的潜在问题可能仍然存在。
查看上面的代码,我希望您尝试回答的问题是:
- 如果Ajax调用发生超时会发生什么?
- 如果服务器返回失败状态 http ,会发生什么情况?
- 如果服务器返回一个有效的状态http但是返回内容的格式和预期不一样,会发生什么情况?
上面的代码并没有清楚地回答这些问题。很容易想象“快乐之路”,但任何意外情况都没有得到明确处理。虽然我们永远不应该将没有处理这些情况的代码投入生产,但JavaScript语言并没有强迫我们处理它们。如果团队中的某个人忘记对其中一个潜在问题进行正确的处理,结果将是一个运行时错误。
如果你的团队运气不好,这些情况可能会在代码投入生产时出现。当这种情况不可避免地发生时,你很可能会把责任推卸给实现该系统该部分的开发人员。
但是如果我们知道必须解决这种情况,为什么语言、框架和库允许编写这种类型的代码?
什么是简单的解决方案?
解决方案简单和过于简化之间有着很大的区别。我用JavaScript编写的这个解决方案并不简单。它过于简化,因为它忽略了问题的基本方面。
像Elm这样的语言往往会迫使我们思考并实现所有潜在问题的解决方案。最终的代码可能会更大,但它可以保证运行时不会出错,因为编译器会检查并强制开发人员处理所有可能的路径,不会留下任何可预见的故障空间。
这种方法的另一个优点是我们拥有一个自文档化的代码。例如,预期返回的格式应该非常清晰,包括哪些字段是必需的,哪些是可选的。
在Elm中实现相同的解决方案
现在让我们看一个用Elm编写的针对同一问题的解决方案。如果您不了解这门语言(或类似的语言,例如Haskell或PureScript),您可能会觉得它的语法有些奇怪。不过不用担心,您无需完全理解这段代码就能理解本文的提议。
首先,我们需要一个简单的html文件来托管我们的页面。这种方法与我们使用React或Vue等工具时的做法非常相似。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>List of Pokémons using HTML and JavaScript</title>
<meta name="author" content="Marcio Frayze David">
</head>
<body>
<main></main>
<script>
Elm.Main.init({ node: document.querySelector('main') })
</script>
</body>
</html>
这次我们的html只是一个外壳。它只会加载用Elm编写的应用程序(之前已经编译过),并将其内容放在main标签 内。
最后,有趣的部分是:用Elm编写的代码。我将首先完整列出代码,然后对与本文主题更相关的部分进行突出显示和评论。
module Main exposing (..)
import Browser
import Html exposing (..)
import Http
import Json.Decode exposing (Decoder)
-- MAIN
main =
Browser.element
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
-- MODEL
type alias PokemonInfo = { name : String }
type Model
= Failure
| Loading
| Success (List PokemonInfo)
init : () -> (Model, Cmd Msg)
init _ =
(Loading, fetchPokemonNames)
-- UPDATE
type Msg
= FetchedPokemonNames (Result Http.Error (List PokemonInfo))
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
FetchedPokemonNames result ->
case result of
Ok pokemonsInfo ->
(Success pokemonsInfo, Cmd.none)
Err _ ->
(Failure, Cmd.none)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- VIEW
view : Model -> Html Msg
view model =
case model of
Failure ->
text "For some reason, the Pokémon name list could not be loaded. 😧"
Loading ->
text "Loading Pokémons names, please wait..."
Success pokemonsInfo ->
ul []
(List.map viewPokemonInfo pokemonsInfo)
viewPokemonInfo : PokemonInfo -> Html Msg
viewPokemonInfo pokemonInfo =
li [] [ text pokemonInfo.name ]
-- HTTP
fetchPokemonNames : Cmd Msg
fetchPokemonNames =
Http.get
{ url = "https://pokeapi.co/api/v2/pokemon?limit=5"
, expect = Http.expectJson FetchedPokemonNames decoder
}
pokemonInfoDecoder : Decoder PokemonInfo
pokemonInfoDecoder =
Json.Decode.map PokemonInfo
(Json.Decode.field "name" Json.Decode.string)
decoder : Decoder (List PokemonInfo)
decoder =
Json.Decode.field "results" (Json.Decode.list pokemonInfoDecoder)
我已将此页面发布到在线编辑器Ellie中,以便您查看此Web 应用的运行情况。建议您尝试修改代码,看看效果如何。这是开始尝试Elm语言的好方法。
分析Elm中的实现
我不会在本文中解释所有这些代码以及Elm语言背后的架构。但我想重点介绍一些与本文讨论背景相关的重要部分,首先是类型的定义。
类型定义
type alias PokemonInfo = { name : String }
Model type
= Loading
| Failure
| Success (PokemonInfo List)
上面的代码设置了类型别名,让阅读代码的人更清楚地了解什么是PokemonInfo(在本例中,它是一个包含一个名为name 、类型为String的字段的结构体)。这也能让我们的编译器更轻松,因为它允许你在必要时处理相应的错误,并且在构建阶段能够发送更具信息量的错误消息。
接下来,我们定义一个名为Model的类型,用于表示应用程序的当前状态。在本例中,我们的Web 应用可以处于以下三种可能状态中的一种(且只能处于一种):
- Loading:应用程序初始状态,表示http请求仍在处理中。
- Failure:表示失败状态,说明向服务器进行http调用时出现问题(可能是超时、返回消息解析失败等)。
- 成功:表示请求已执行并且其返回已成功转换。
在三个已定义的状态中,只有Success 状态会附加额外的信息:一个包含PokemonInfo类型元素的列表。请注意,这不会造成任何歧义。如果状态为成功,则必须定义一个PokemonInfo类型的列表,并且该列表必须具有有效的结构。反之亦然:如果状态为失败,则不会定义包含宝可梦名称的列表。
html页面的构建
Elm是使用虚拟DOM和声明式编程概念开发webapps的先驱之一。
在Elm的架构中,应用程序的状态与屏幕上显示的内容之间有着非常清晰的分离。视图函数负责从应用程序的当前状态挂载虚拟DOM的表示。每次状态发生变化时(例如,当你完成加载包含 Pokémon 名称的数据时),视图函数都会重新执行并创建一个新的虚拟DOM。
在我们的示例中,这发生在以下代码片段中:
view : Model -> Html Msg
view model =
case model of
Failure ->
text "For some reason, the Pokémon name list could not be loaded. 😧"
Loading ->
text "Loading Pokémons names, please wait..."
Success pokemonsInfo ->
ul []
(List.map viewPokemonInfo pokemonsInfo)
viewPokemonInfo : PokemonInfo -> Html Msg
viewPokemonInfo pokemonInfo =
li [] [ text pokemonInfo.name ]
这里我们声明了两个函数:视图和一个名为viewPokemonInfo的辅助函数。
使用类型来表示应用程序状态的一个优点是,只要一段代码使用了这种类型,编译器就会强制开发人员处理所有可能的状态。在本例中,状态包括:Loading、Failure和Success 。如果从示例的视图函数中删除Loading处理,则在尝试编译应用程序时会收到类似以下的错误消息:
Line 70, Column 3
This `case` does not have branches for all possibilities:
70|> case model of
71|> Failure ->
72|> text "For some reason, the Pokémon name list could not be loaded. 😧"
73|>
74|> Success pokemonsInfo ->
75|> ul []
76|> (List.map viewPokemonInfo pokemonsInfo)
Missing possibilities include:
Loading
I would have to crash if I saw one of those. Add branches for them!
Hint: If you want to write the code for each branch later, use `Debug.todo` as a
placeholder. Read <https://elm-lang.org/0.19.1/missing-patterns> for more
guidance on this workflow.
这为开发人员重构代码并在应用程序中包含或删除状态提供了更多的保护,确保它不会无法解决一些模糊的情况。
进行http调用
下面的代码片段负责异步进行http调用并执行返回的解析,将其转换为PokemonInfo列表。
fetchPokemonNames : Cmd Msg
fetchPokemonNames =
Http.get
{ url = "https://pokeapi.co/api/v2/pokemon?limit=5"
, expect = Http.expectJson FetchedPokemonNames decoder
}
pokemonInfoDecoder : Decoder PokemonInfo
pokemonInfoDecoder =
Json.Decode.map PokemonInfo
(Json.Decode.field "name" Json.Decode.string)
decoder : Decoder (List PokemonInfo)
decoder =
Json.Decode.field "results" (Json.Decode.list pokemonInfoDecoder)
不可否认的是,这段代码比调用fetch函数要长。但请注意,除了异步调用之外,它还验证了返回值并将其转换为List PokemonInfo,从而无需我们进行任何验证。
在执行结束时,将会发出一条FetchedPokemonNames消息以及操作结果:要么是已经解码的 Pokémon 名称列表,要么是代表发生错误的结果。
更新功能负责接收此消息并为应用程序创建新状态。
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
FetchedPokemonNames result ->
case result of
Ok pokemonsInfo ->
(Success pokemonsInfo, Cmd.none)
Err _ ->
(Failure, Cmd.none)
再次强调,我们必须处理所有可能的情况。在这个例子中,有两种情况:
- 如果结果为Ok,则表示我们的请求已成功处理。然后,应用程序将返回一个新的状态,变为Success,同时返回包含宝可梦名称的列表。
- 如果结果为Err,则表明请求过程中或执行JSON解析时出现了问题。返回新的应用程序状态,并将其更改为Failure。
每当更新函数的返回值与之前的状态不同时,视图函数就会自动再次触发,然后创建一个新的虚拟DOM,并将任何更改应用到屏幕上。为了更好地理解这个过程,你可以阅读此页面上的Elm 架构 。
结论
虽然本文只关注http请求和JavaScript,但相同的概念也适用于许多其他场景、库、框架和语言。
我的目的并非阻止人们使用JavaScript。Elm是一门很棒的语言,但我仍然在一些 Web 应用中使用JavaScript和TypeScript ,而这并非问题的焦点。我希望的是,当你使用你首选语言的函数时(无论它是原生函数还是来自第三方库),你总是会反思:这段代码是否忽略了某些场景?或者,换句话说,这是一个简单还是过于简化的解决方案?
最重要的是,在编写新功能时,请使用一个能够鼓励使用者遵循最佳实践的沟通界面。即使使用者遵循的是最小化投入的策略,也应该能够处理所有可能的情况。或者,换句话说,始终遵循最小惊讶原则。
你喜欢这篇文章吗?查看我的其他文章:https ://segunda.tech/tags/english
鏂囩珷鏉ユ簮锛�https://dev.to/marciofrayze/simple-code-is- Different-from-simplistic-code-elm-vs-javascript-1pp